MarinhoBrandão.com
"O risco que se corre ao se introduzir novas tecnologias é menor do que aquele que se corre ao não introduzi-las." ;)

Geraldo Reports Engine - Tutorial

Publicado por marinho, há 3 anos | django projetos python

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:

  1. Descompacte o arquivo;
  2. Entre na pasta e execute python setup.py install
  3. 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)
  4. 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:

  1. Sim, nós fazemos uso de muitas coisas que a biblioteca ReportLab oferece;
  2. Todo relatório deve herdar a classe "Report";
  3. 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;
  4. 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:

  1. A lista "groups" indica todos os agrupamentos do relatório, podem ser quantos você quiser. Os de cima possuem prioridade sobre os de baixo;
  2. 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;
  3. 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;
  4. 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

  1. Site oficial: http://geraldo.sourceforge.net/
  2. Repositórigo em Git: http://github.com/marinho/django-geraldo
  3. Overview: http://github.com/marinho/django-geraldo/wikis/home
  4. Screenshots: http://picasaweb.google.com/marinho/GeraldoReportsEngine
  5. Bug reports: http://geraldo.sourceforge.net/