Geraldo Reports Engine - Tutorial

Atualização:
Agora o Geraldo Reports possui um site oficial na SourceForge, portanto para documentação e relatar bugs, faça uso do novo site: **[http://geraldo.sourceforge.net/](http://geraldo.sourceforge.net/)**A geração de relatórios é algo fundamental na construção de sistemas corporativos.
E cedo ou tarde o desenvolvedor se depara com uma situação que não pode ser resolvida com HTML impresso pelo navegador. Algumas das razões para isso são por exemplo a ausência de um controle mais rígido sobre as dimensões e margens da página e a repetição do cabeçalho e rodapé a cada página, dentre outras coisas.
Quando é necessário exportar para PDF, atualmente as opções em geral são: converter o HTML para PDF usando xhtml2pdf (você terá problemas com CSS e dimensionamento de tabelas, no mínimo), impressora PDF (não pode ser automatizada com facilidade e a formação varia com o navegador usado), xserver (se conseguir, será com muito sofrimento) ou escrever tudo na unha (é isso que tenho feito ultimamente, mas é horrível para manter).
Então basicamente há motivos muito plausíveis pra necessitar de algo que torne mais fácil o processo de criação e geração de relatórios.
Geraldo Reports Engine
O Geraldo é uma solução para isso. Comecei a criá-lo na quinta-feira passada, após um período de busca de soluções de relatórios para alguns projetos que tenho trabalhado.
Pense nos formulários dinâmicos do Django ("django.forms"): não há um editor visual para construí-los, mas a declaração de classes e métodos ali é simples o suficiente para tornar o trabalho produtivo e poderoso. A idéia do Geraldo é a mesma: não há um editor visual (ainda), mas nem por isso ele deixa de ser produtivo. Na prática, você vai declarar algumas classes, instanciar a classe do relatório e chama um gerador pra dar saída dos relatórios (que pode ser para arquivo ou memória), para depois faz o que quiser com esses relatórios (como por exemplo: mantê-los como arquivos estáticos ou dar saída através de HttpResponse).
O Geraldo foi construído em poucos dias e não tem intenção de ficar grande. Dificilmente será necessário ampliar a API da primeira versão pois com o que já temos é possível fazer (quase) qualquer coisa, basta um pouco de imaginação e conhecimento do mesmo.
O Geraldo é uma aplicação plugável, com 10 arquivos e nenhum deles chega a mil linhas, e é compatível com Django 0.96 ou superior. É possível também usá-lo fora do Django.
Como instalar o Geraldo
Primeiro, você deve baixar o pacote desta URL:
http://django-plus.googlecode.com/files/geraldo-latest.tar.gz
A instalação segue o padrão do Python:
- Descompacte o arquivo;
- Entre na pasta e execute python setup.py install
- Se assegure de que você tem instalada uma versão recente da ReportLab (testado com a versão 2.1) e da Python Imaging Library (testado com a versão 1.1.6, mas é necessária somente para trabalhar com componentes gráficos)
- Adicione o pacote "geraldo" à setting "INSTALLED_APPS" do seu projeto.
Como construir um relatório com o Geraldo
Vamos supor que você tenha um projeto com a aplicação "faturamento", com as seguintes classes de modelo:
class Produto(models.Model):
nome = models.CharField(max_length=100)
class Cliente(models.Model):
nome = models.CharField(max_length=100)
class NotaFiscal(models.Model):
cliente = models.ForeignKey('Cliente')
endereco_entrega = models.CharField(max_length=70, blank=True)
data_lancamento = models.DateField(blank=True, default=date.today())
data_fatura = models.DateField(blank=True, null=True)
data_entrega = models.DateField(blank=True, null=True)
valor_icms = models.DecimalField(max_digits=12, decimal_places=2, blank=True, default=0)
valor_produtos = models.DecimalField(max_digits=12, decimal_places=2, blank=True, default=0)
valor_total = models.DecimalField(max_digits=12, decimal_places=2, blank=True, default=0)
class NotaFiscalItem(models.Model):
nota_fiscal = models.ForeignKey('NotaFiscal')
produto = models.ForeignKey('Produto')
valor_unitario = models.DecimalField(max_digits=12, decimal_places=2)
quantidade = models.DecimalField(max_digits=12, decimal_places=2)
valor_total = models.DecimalField(max_digits=12, decimal_places=2, blank=True, default=0)
Vamos então criar um relatório de listagem das notas ficais. Para isso vamos precisar de criar uma URL, como esta por exemplo:
urlpatterns = patterns('faturamento.views',
url('^relatorio/$', 'relatorio'),
)
O próximo passo é criar a view, que pode ser assim:
from django.http import HttpResponse
from reports import RelatorioListagem
from geraldo.generators import PDFGenerator
from models import NotaFiscal
def relatorio(request):
resp = HttpResponse(mimetype='application/pdf')
notas_fiscais = NotaFiscal.objects.order_by('cliente','id')
rel = RelatorioListagem(queryset=notas_fiscais)
rel.generate_by(PDFGenerator, filename=resp)
return resp
Observe que a resposta que a view está devolvendo ao navegador está no formato PDF:
resp = HttpResponse(mimetype='application/pdf')
Em seguida, carregamos a listagem de Notas Fiscais aqui, veja:
notas_fiscais = NotaFiscal.objects.order_by('cliente','id')
Agora entra o trabalho do relatório:
rel = RelatorioListagem(queryset=notas_fiscais)
A classe "RelatorioListagem" (que vamos criar logo a seguir) recebe a queryset e retorna uma instância à variável "rel". O argumento "queryset" pode ser também uma lista, portanto, não há necessidade que seja um objeto dotipo QuerySet.
E então entra em cena o gerador para PDF, que é chamado pelo método "generate_by" e recebe o atributo "filename", que pode ser um nome de arquivo ou uma instância de saída (exemplos: uma instância de arquivo ou de HttpResponse):
rel.generate_by(PDFGenerator, filename=resp)
O próximo passo agora é criar o arquivo "reports.py" com o seguinte código dentro:
# -*- coding: utf-8 -*-
from geraldo import Report, ReportBand, ObjectValue, ReportBand, landscape
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import A5
class RelatorioListagem(Report):
title = 'Listagem de Notas Fiscais'
print_if_empty = True
page_size = landscape(A5)
class band_detail(ReportBand):
height = 0.5*cm
elements=(
ObjectValue(attribute_name='id', left=0.5*cm),
ObjectValue(attribute_name='data_lancamento', left=3*cm,
get_value=lambda instance: instance.data_lancamento.strftime('%d/%m/%Y')),
)
Uma explicação resumida e inicial:
- Sim, nós fazemos uso de muitas coisas que a biblioteca ReportLab oferece;
- Todo relatório deve herdar a classe "Report";
- O atributo "print_if_empty" determina que se a queryset estiver vazia, o relatório será impresso assim mesmo. Se este atributo não for indicado, o relatório seria gerado sem nenhuma página;
- O atributo "page_size" recebe uma tupla do tamanho da página, que pode ser importada do ReportLab ou informada manualmente. Caso use a função "landscape" as dimensões serão invertidas;
Um relatório pode ter as seguintes "bandas":
- band_detail - é o carro chefe do relatório. Ela é repetida uma vez para cada item da queryset;
- band_page_header - é impressa no topo de cada uma das páginas do relatório;
- band_page_footer - é impressa no rodapé de cada uma das páginas do relatório;
- band_begin - é impressa logo abaixo do cabeçalho da primeira página do relatório;
- band_summary - é impressa ao final do relatório, abaixo da última banda de detalhe gerada;
Cada uma dessas "bandas" pode receber uma instância de "ReportBand" e em geral possui uma altura ("height") e uma lista de elementos.("elements").
Os elementos de uma banda podem ser gráficos ou widgets, como é o caso da classe "ObjectValue", que é a representação de um valor (um atributo, método sem argumento ou propriedade) dos objetos da queryset.
Portanto, a classe acima dará saída a este relatório:
Nada animador, certo?
Pois então acrescente agora as bandas de cabeçalho e rodapé de página, e o seu arquivo "reports.py" ficará assim:
# -*- coding: utf-8 -*-
from geraldo import Report, ReportBand, ObjectValue, ReportBand, landscape,\
SystemField, BAND_WIDTH, Label
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import A5
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
class RelatorioListagem(Report):
title = 'Listagem de Notas Fiscais'
print_if_empty = True
page_size = landscape(A5)
class band_detail(ReportBand):
height = 0.5*cm
elements=(
ObjectValue(attribute_name='id', left=0.5*cm),
ObjectValue(attribute_name='data_lancamento', left=3*cm,
get_value=lambda instance: instance.data_lancamento.strftime('%d/%m/%Y')),
)
class band_page_header(ReportBand):
height = 1.3*cm
elements = [
SystemField(expression='%(report_title)s', top=0.1*cm, left=0, width=BAND_WIDTH,
style={'fontName': 'Helvetica-Bold', 'fontSize': 14, 'alignment': TA_CENTER}),
Label(text="ID", top=0.8*cm, left=0.5*cm),
Label(text=u"Data Lançamento", top=0.8*cm, left=3*cm),
SystemField(expression=u'Página %(page_number)d de %(page_count)d', top=0.1*cm,
width=BAND_WIDTH, style={'alignment': TA_RIGHT}),
]
borders = {'bottom': True}
class band_page_footer(ReportBand):
height = 0.5*cm
elements = [
Label(text='Geraldo Reports', top=0.1*cm),
SystemField(expression=u'Impresso em %(now:Y, b d)s às %(now:H:i)s', top=0.1*cm,
width=BAND_WIDTH, style={'alignment': TA_RIGHT}),
]
borders = {'top': True}
As novidades destas duas "bandas" são:
- O widget "SystemField" representa variáveis de sistema. Exemplos: o título do relatório, o número da página atual, o número total de páginas, a data da impressão, o nome do autor, etc. mas isso é feito através de uma formatação de string, informada no argumento "expression";
- Alinhamentos e estilo. Usamos algumas diretivas de alinhamento e estilos do ReportLab;
- Bordas: indicamos as bordas do cabeçalho e do rodapé.
Agora veja o resultado:
Melhorou agora? Pois bem, agora vamos criar um agrupamento. Acrescente o seguinte bloco ao final do arquivo:
groups = [
ReportGroup(attribute_name='cliente',
band_header=ReportBand(
height=0.7*cm,
elements=[
ObjectValue(attribute_name='cliente', left=0, top=0.1*cm, width=20*cm,
get_value=lambda instance: 'Cliente: ' + (instance.cliente.nome),
style={'fontName': 'Helvetica-Bold', 'fontSize': 12})
],
borders={'bottom': True},
)
),
]
Naturalmente, você deve também incluir a classe "ReportGroup" à primeira linha de importação, assim:
from geraldo import Report, ReportBand, ObjectValue, ReportBand, landscape,\
SystemField, BAND_WIDTH, Label, ReportGroup
Esclarecendo sobre as novidades:
- A lista "groups" indica todos os agrupamentos do relatório, podem ser quantos você quiser. Os de cima possuem prioridade sobre os de baixo;
- A classe "ReportGroup" usa o argumento "attribute_name" para agrupar os dados da queryset. Aqui é importante vocẽ estar ciente de que a queryset deve ser ordenada previamente por esses campos, pois ele não as ordena automaticamente;
- A classe "ReportGroup"possui também os atributos "band_header" e "band_footer" para indicar "bandas" para o cabeçalho e o rodapé do agrupamento. Imagine que o cabeçalho em geral é usado para mostrar a descrição principal do grupo e o rodapé é usado para apresentar totalizações e outras coisas mais;
- Qualquer widget suporta o atributo "get_value", que trata-se de uma função para a formatação customizada do valor que será impresso. Porém, cada widget possui seus próprios argumentos, isso porque cada um deles possui um comportamento diferente;
Veja o resultado:
Ficou bacana hein? Agora vamos para a quarta e última parte: SubRelatórios.
SubRelatórios existem para vocẽ mostrar elementos de "um para muitos" agregados ao item da queryset.
Exemplo: estamos falando de uma lista de Notas Fiscais, e como sabemos, uma Nota Fiscal possui muitos ítems. Portanto, um SubRelatório neste caso é útil para listar os ítens da Nota Fiscal.
Acrescente este último bloco ao final do arquivo:
subreports = [
SubReport(
queryset_string = '%(object)s.notafiscalitem_set.all()',
detail_band = ReportBand(
height=0.5*cm,
elements=[
ObjectValue(attribute_name='produto', top=0, left=1*cm),
ObjectValue(attribute_name='valor_unitario', top=0, left=5*cm),
]
),
),
]
Da mema forma como foi com a classe "ReportGroup", você deve acrescentar a classe "SubReport" à linha de importação:
from geraldo import Report, ReportBand, ObjectValue, ReportBand, landscape,\
SystemField, BAND_WIDTH, Label, ReportGroup, SubReport
Aqui novamente não há segredo: a classe "SubReport" representa um relatório mais limitado, que possui uma única banda (a de detalhe), com seus elementos, mas o elemento surpresa aqui é este atributo:
queryset_string = '%(object)s.notafiscalitem_set.all()',
Como pode notar, o atributo "queryset_string" trata-se de uma string, e a ele é passado o objeto através da macro "%(object)s", para identificar que o caminho aponta para uma ramificação do objeto corrente na geração do relatório. Você poderia também usar de outras formas para indicar um caminho. Assim por exemplo:
'Message.objects.filter(user__id=%(object)s.id)'
Enfim, use a sua imaginação! :P
Agora veja o resultado final do nosso relatório:
E aí, gostou? Bom, há diversos outros recursos, mas esses aí já são um bom começo!
Referências
- Site oficial: http://geraldo.sourceforge.net/
- Repositórigo em Git: http://github.com/marinho/django-geraldo
- Overview: http://github.com/marinho/django-geraldo/wikis/home
- Screenshots: http://picasaweb.google.com/marinho/GeraldoReportsEngine
- Bug reports: http://geraldo.sourceforge.net/



