← Voltar ao blog

PDF da fatura no Magento 2: totais, itens e layout

Rodrigo Biazzi

O PDF de fatura padrão do Magento é funcional e simples. Mais cedo ou mais tarde alguém pede outro layout, um texto legal no rodapé, um total customizado, o SKU em outro lugar ou uma fonte que imprima o idioma deles corretamente. O código do PDF é antigo, escrito em cima do Zend_Pdf com coordenadas absolutas, e não foi feito para ser estendido com facilidade. Mesmo assim, a maioria das mudanças pode ser feita de forma limpa se você souber qual ponto de extensão usar.

Este artigo cobre a personalização do PDF da fatura no Magento Open Source e no Adobe Commerce 2.4.8, das opções baseadas em configuração até mudanças de código, e quando faz mais sentido trocar a geração de PDF por completo.

De onde vem o PDF

O PDF da fatura é montado por Magento\Sales\Model\Order\Pdf\Invoice, que estende Magento\Sales\Model\Order\Pdf\AbstractPdf. O método getPdf() recebe uma lista de faturas e retorna um objeto Zend_Pdf. Os controllers do admin, tanto para uma fatura individual quanto para a ação em massa "Print Invoices", chamam esse método e enviam o resultado do render() ao navegador.

Dentro do getPdf(), para cada fatura ele cria uma página, insere o logo e o endereço da loja, as informações e endereços do pedido, o cabeçalho da tabela de itens, cada item por meio de um renderer e, por fim, os totais. Os métodos que fazem isso ficam no AbstractPdf: insertLogo(), insertAddress(), insertOrder(), insertTotals() e drawLineBlocks().

Dois detalhes úteis antes de começar:

  • A maioria desses métodos é protected. Plugins não interceptam métodos protected, então mudanças de layout exigem uma classe que estenda o model do PDF.
  • O link "Print Invoice" da conta do cliente no storefront é uma página de impressão em HTML, não esse PDF. Personalizar um não muda o outro.

Comece pela configuração

Antes de escrever código, veja o que o admin já oferece em Stores, Configuration, Sales, Sales, Invoice and Packing Slip Design:

  • Logo for PDF Print-outs. Um logo separado para PDFs. Use uma imagem de alta resolução com as proporções que você quer. Ela é escalada para caber numa área fixa.
  • Address. Texto livre impresso no canto superior direito. Muitos lojistas colocam aqui o registro da empresa ou o número fiscal, o que evita código para o pedido mais comum.

Também em Sales, PDF Print-outs, você pode escolher exibir o order id no cabeçalho de faturas, shipments e credit memos. Essas configurações funcionam por website ou store view, então setups multi store podem ter logos e endereços diferentes.

Adicionando um total com pdf.xml

Os totais do PDF são declarados em arquivos etc/pdf.xml e mesclados entre os módulos. Cada total lê um campo da fatura e o imprime com um rótulo. Se você adicionou uma taxa como descrito em totals collectors de taxa customizada, é assim que ela chega ao PDF:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/pdf_file.xsd">
    <totals>
        <total name="handling_fee">
            <title translate="true">Handling Fee</title>
            <source_field>handling_fee_amount</source_field>
            <font_size>7</font_size>
            <display_zero>false</display_zero>
            <sort_order>350</sort_order>
        </total>
    </totals>
</config>

O model de total padrão chama getDataUsingMethod() no documento de origem com o source_field. No PDF da fatura, a origem é a fatura, não o pedido. Uma coluna de taxa que existe só em sales_order não imprime nada. Ela precisa estar em sales_invoice também, o que de qualquer forma é necessário para os totais da fatura ficarem corretos.

O sort order é comparado com os totais do core declarados em Magento_Sales/etc/pdf.xml, Magento_Tax/etc/pdf.xml e outros. Leia esses arquivos na sua versão e escolha um número que ponha a sua linha onde você quer. Para um total que precisa de lógica própria, por exemplo um valor calculado a partir de vários campos, adicione um nó model apontando para uma classe que estenda Magento\Sales\Model\Order\Pdf\Total\DefaultTotal e sobrescreva o getTotalsForDisplay().

Mudando como os itens são desenhados

Os itens são desenhados por renderers, também declarados no pdf.xml, por tipo de documento e tipo de produto:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/pdf_file.xsd">
    <renderers>
        <page type="invoice">
            <renderer product_type="default">Vendor\Pdf\Model\Order\Pdf\Items\Invoice\DefaultInvoice</renderer>
        </page>
    </renderers>
</config>

Sua classe estende Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoice e sobrescreve o draw(). O draw() do core monta um array de linhas, onde cada coluna é uma entrada com o texto, a posição x (feed), a fonte e o alinhamento, e depois o passa para o drawLineBlocks(), que cuida das quebras de página. A forma mais fácil de personalizar é copiar o método do core para a sua classe e mudar as colunas, por exemplo para adicionar um atributo de produto embaixo do nome ou quebrar SKUs longos de outro jeito.

Faça seu módulo depender do Magento_Sales no module.xml para que o seu pdf.xml seja mesclado depois do do core e o seu renderer prevaleça. Bundle products têm o próprio renderer no Magento_Bundle, então se você vende bundles, confira esse renderer também.

Se você mudar as posições das colunas, mude também o cabeçalho da tabela. Ele é desenhado pelo _drawHeader() no model da fatura, que é protected, o que nos leva à próxima seção.

Mudanças de layout com uma preference

Para mudanças no cabeçalho, no cabeçalho da tabela ou no rodapé, você precisa da sua própria classe estendendo o model do PDF da fatura, registrada como preference no etc/di.xml:

<preference for="Magento\Sales\Model\Order\Pdf\Invoice" type="Vendor\Pdf\Model\Order\Pdf\Invoice"/>

Aqui vai um exemplo pequeno que adiciona um texto de rodapé vindo da configuração em toda página, depois que o core desenhou todo o resto:

<?php
declare(strict_types=1);

namespace Vendor\Pdf\Model\Order\Pdf;

use Magento\Sales\Model\Order\Pdf\Invoice as CoreInvoice;
use Magento\Store\Model\ScopeInterface;

class Invoice extends CoreInvoice
{
    public function getPdf($invoices = [])
    {
        $pdf = parent::getPdf($invoices);

        $footer = (string) $this->_scopeConfig->getValue(
            'vendor_pdf/invoice/footer',
            ScopeInterface::SCOPE_STORE
        );

        if ($footer === '') {
            return $pdf;
        }

        foreach ($pdf->pages as $page) {
            $this->_setFontRegular($page, 7);
            $page->drawText($footer, 25, 20, 'UTF-8');
        }

        return $pdf;
    }
}

Isso usa só o resultado público do getPdf() e dois helpers protected do pai, então sobrevive melhor a upgrades do que um getPdf() copiado. Quando uma lista de faturas mistura lojas, leia a config com o store id de cada fatura em vez do escopo atual. O exemplo simplifica.

Uma preference substitui a classe no sistema inteiro, e só um módulo pode vencer. Se uma extensão instalada já tem uma preference para a mesma classe, a sua e a dela entram em conflito. Rode bin/magento dev:di:info "Magento\Sales\Model\Order\Pdf\Invoice" para ver o que está configurado hoje. Os trade-offs estão em plugins vs observers vs preferences.

Fontes e caracteres especiais

Os models de PDF embutem fontes TrueType que vêm em lib/internal. Elas cobrem bem latino, cirílico e grego. Para idiomas que elas não cobrem, os caracteres saem como caixas vazias ou somem. A solução é sobrescrever os helpers de fonte _setFontRegular(), _setFontBold() e _setFontItalic() na sua preference e carregar uma fonte que cubra o alfabeto com Zend_Pdf_Font::fontWithPath(). Fontes para escritas asiáticas são grandes e deixam cada PDF bem mais pesado, então teste uma impressão em massa de muitas faturas.

O Zend_Pdf também não quebra texto sozinho. Nomes de produto e endereços longos são divididos pelo string helper do Magento com contagens fixas de caracteres. Se uma mudança fizer o texto se sobrepor, normalmente é o tamanho da divisão que precisa de ajuste.

Quando trocar o motor de PDF

Se o design pede um layout completamente diferente, coordenadas em PHP ficam lentas de trabalhar. A alternativa é renderizar um template HTML e convertê-lo com uma biblioteca como Dompdf ou mPDF instalada pelo Composer. É mais fácil de estilizar e manter, mas considere:

  • Os controllers do admin chamam render() no objeto retornado pelo getPdf(). Você precisa substituir esses controllers, ou as rotas, para devolver a sua saída.
  • Bibliotecas de HTML para PDF usam mais memória e CPU. Imprimir algumas centenas de faturas em massa pode estourar os limites do PHP.
  • Outros lugares podem usar o model de PDF, como extensões que anexam faturas a e-mails. Verifique todos.

Para a maioria das lojas, um renderer, alguns totais e uma preference para cabeçalho e rodapé bastam, com menos risco.

FAQ

Por que meu total customizado não aparece no PDF da fatura?

O source_field é lido da fatura. Confirme que o valor está salvo numa coluna de sales_invoice, que o módulo que declara o pdf.xml está ativo e que o display_zero não está escondendo um valor zero.

Posso usar um plugin em vez de uma preference para o layout do PDF?

Só para métodos públicos como o getPdf. A maioria dos métodos de desenho é protected, então mudanças em cabeçalho, tabela e endereços exigem uma preference ou um renderer customizado.

Mudar o PDF muda a impressão da fatura na conta do cliente?

Não. A página de impressão do storefront é HTML renderizado por layout e templates. Personalize separadamente no seu tema.

Por que alguns caracteres saem como caixas?

As fontes padrão não cobrem todos os alfabetos. Carregue uma fonte que cubra o seu idioma nos métodos de fonte da sua classe de PDF.

Atendo como freelancer em Magento e Adobe Commerce, incluindo documentos de vendas e PDFs. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados