Crear facturas en PDF en Python con borb

En este tutorial, veremos ejemplos sobre cómo crear una factura en PDF en Python usando pText, así como también cómo incrustar archivos y agregar esquemas.

Introducción

El Portable Document Format (PDF) no es un formato WYSIWYG (lo que ves es lo que obtienes). Fue desarrollado para ser independiente de la plataforma, independiente del sistema operativo subyacente y los motores de renderizado.

Para lograr esto, el PDF se construyó para interactuar a través de algo más parecido a un lenguaje de programación y se basa en una serie de instrucciones y operaciones para lograr un resultado. De hecho, PDF está basado en un lenguaje de secuencias de comandos: Posdata, que fue el primer lenguaje de descripción de página independiente del dispositivo.

En esta guía, utilizaremos borracho, una biblioteca de Python dedicada a leer, manipular y generar documentos PDF. Ofrece un modelo de bajo nivel (que le permite acceder a las coordenadas y el diseño exactos si elige usarlos) y un modelo de alto nivel (donde puede delegar los cálculos precisos de márgenes, posiciones, etc. a un administrador de diseño) .

Echaremos un vistazo a cómo crear una factura en PDF en Python usando borb.

Instalación de borb {#instalación de borb}

borb puede descargarse desde la fuente en GitHub, o instalarse a través de pip:

1
$ pip install borb

Crear una factura en PDF en Python con borb

borb tiene dos clases clave intuitivas: Documento y Página, que representan un documento y las páginas que contiene. Además, la clase ‘PDF’ representa una API para cargar y guardar los ‘Documentos’ que creamos.

Vamos a crear un Documento() y Página() como un lienzo en blanco al que podemos agregar la factura:

1
2
3
4
5
6
7
8
9
from borb.pdf.document import Document
from borb.pdf.page.page import Page

# Create document
pdf = Document()

# Add page
page = Page()
pdf.append_page(page)

Como no queremos lidiar con el cálculo de coordenadas, podemos delegar esto a un PageLayout que administra todo el contenido y sus posiciones:

1
2
3
4
5
6
# New imports
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from decimal import Decimal

page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)

Aquí, estamos usando un SingleColumnLayout ya que todo el contenido debe estar en una sola columna; no tendremos un lado izquierdo y otro derecho de la factura. También estamos reduciendo el margen vertical aquí. El valor predeterminado es recortar el 10 % superior de la altura de la página como margen, y lo estamos reduciendo al 2 %, ya que querremos usar este espacio para el logotipo o el nombre de la empresa.

Hablando de eso, agreguemos el logotipo de la empresa al diseño:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# New import
from borb.pdf.canvas.layout.image.image import Image


page_layout.add(    
        Image(        
        "https://s3.wikihtp.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",        
        width=Decimal(128),        
        height=Decimal(128),    
        ))

Aquí, estamos agregando un elemento al diseño: una Imagen (). A través de su constructor, estamos agregando una URL que apunta al recurso de la imagen y configurando su “ancho” y “alto”.

Debajo de la imagen, agregaremos la información de nuestra empresa imaginaria (nombre, dirección, sitio web, teléfono), así como la información de la factura (número de factura, fecha, fecha de vencimiento). Un formato común para la brevedad (que, por cierto, también hace que el código sea más limpio) es usar una tabla para almacenar datos de facturas. Vamos a crear un método de ayuda independiente para crear la información de la factura en una tabla, que luego podemos usar para simplemente agregar una tabla a la factura en nuestro método principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# New imports
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable as Table
from borb.pdf.canvas.layout.text.paragraph import Paragraph
from borb.pdf.canvas.layout.layout_element import Alignment
from datetime import datetime
import random

def _build_invoice_information():    
    table_001 = Table(number_of_rows=5, number_of_columns=3)
    
    table_001.add(Paragraph("[Street Address]"))    
    table_001.add(Paragraph("Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))    
    now = datetime.now()    
    table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
    
    table_001.add(Paragraph("[City, State, ZIP Code]"))    
    table_001.add(Paragraph("Invoice #", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
    table_001.add(Paragraph("%d" % random.randint(1000, 10000)))   
    
    table_001.add(Paragraph("[Phone]"))    
    table_001.add(Paragraph("Due Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
    table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year))) 
    
    table_001.add(Paragraph("[Email Address]"))    
    table_001.add(Paragraph(" "))
    table_001.add(Paragraph(" "))

    table_001.add(Paragraph("[Company Website]"))
    table_001.add(Paragraph(" "))
    table_001.add(Paragraph(" "))

    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))          
    table_001.no_borders()
    return table_001

Aquí, estamos haciendo una Tabla simple con 5 filas y 3 columnas. Las filas corresponden a la dirección postal, ciudad/estado, teléfono, dirección de correo electrónico y sitio web de la empresa. Cada fila tendrá valores 0..3 (columnas). Cada elemento de texto se agrega como un ‘Párrafo’, que hemos alineado a la derecha a través de ‘Alignment.RIGHT’, y aceptamos argumentos de estilo como ‘fuente’.

Finalmente, hemos agregado relleno a todas las celdas para asegurarnos de no colocar el texto de forma incómoda cerca de los elementos de confusión de las celdas.

Ahora, de regreso a nuestro método principal, podemos llamar a _build_invoice_information() para completar una tabla y agregarla a nuestro diseño:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(    
    Image(        
        "https://s3.wikihtp.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",        
        width=Decimal(128),        
        height=Decimal(128),    
        ))

# Invoice information table  
page_layout.add(_build_invoice_information())  
  
# Empty paragraph for spacing  
page_layout.add(Paragraph(" "))

Ahora, construyamos este documento PDF rápidamente para ver cómo se ve. Para esto, usaremos el módulo PDF:

1
2
3
4
5
# New import
from borb.pdf.pdf import PDF

with open("output.pdf", "wb") as pdf_file_handle:
    PDF.dumps(pdf_file_handle, pdf)

borb factura 1

¡Excelente! Ahora también querremos agregar la información de facturación y envío. Se colocará convenientemente en una tabla, al igual que la información de la empresa. En aras de la brevedad, también optaremos por crear una función auxiliar separada para generar esta información, y luego simplemente podemos agregarla en nuestro método principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# New imports
from borb.pdf.canvas.color.color import HexColor, X11Color

def _build_billing_and_shipping_information():  
    table_001 = Table(number_of_rows=6, number_of_columns=2)  
    table_001.add(  
        Paragraph(  
            "BILL TO",  
            background_color=HexColor("263238"),  
            font_color=X11Color("White"),  
        )  
    )  
    table_001.add(  
        Paragraph(  
            "SHIP TO",  
            background_color=HexColor("263238"),  
            font_color=X11Color("White"),  
        )  
    )  
    table_001.add(Paragraph("[Recipient Name]"))        # BILLING  
    table_001.add(Paragraph("[Recipient Name]"))        # SHIPPING  
    table_001.add(Paragraph("[Company Name]"))          # BILLING  
    table_001.add(Paragraph("[Company Name]"))          # SHIPPING  
    table_001.add(Paragraph("[Street Address]"))        # BILLING  
    table_001.add(Paragraph("[Street Address]"))        # SHIPPING  
    table_001.add(Paragraph("[City, State, ZIP Code]")) # BILLING  
    table_001.add(Paragraph("[City, State, ZIP Code]")) # SHIPPING  
    table_001.add(Paragraph("[Phone]"))                 # BILLING  
    table_001.add(Paragraph("[Phone]"))                 # SHIPPING  
    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))  
    table_001.no_borders()  
    return table_001

Hemos establecido el background_color de los párrafos iniciales en #263238 (gris-azul) para que coincida con el color del logotipo, y el font_color en Blanco.

Llamemos a esto también en el método principal:

1
2
3
4
5
6
7
8
# Invoice information table
page_layout.add(_build_invoice_information())

# Empty paragraph for spacing
page_layout.add(Paragraph(" "))

# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())

Una vez que ejecutamos el script nuevamente, esto da como resultado un nuevo archivo PDF que contiene más información:

factura borb 2

Con nuestra información básica resuelta (información de la empresa e información de facturación/envío), querremos agregar una descripción detallada. Estos serán los bienes/servicios que nuestra supuesta empresa le ofreció a alguien y también se suelen hacer en forma de tabla debajo de la información que ya hemos agregado.

Nuevamente, creemos una función auxiliar que genere una tabla y la complete con datos, que simplemente podemos agregar a nuestro diseño más adelante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# New import
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable as Table
from borb.pdf.canvas.layout.table.table import TableCell


def _build_itemized_description_table(self):  
    table_001 = Table(number_of_rows=15, number_of_columns=4)  
    for h in ["DESCRIPTION", "QTY", "UNIT PRICE", "AMOUNT"]:  
        table_001.add(  
            TableCell(  
                Paragraph(h, font_color=X11Color("White")),  
                background_color=HexColor("016934"),  
            )  
        )  
  
    odd_color = HexColor("BBBBBB")  
    even_color = HexColor("FFFFFF")  
    for row_number, item in enumerate([("Product 1", 2, 50), ("Product 2", 4, 60), ("Labor", 14, 60)]):  
        c = even_color if row_number % 2 == 0 else odd_color  
        table_001.add(TableCell(Paragraph(item[0]), background_color=c))  
        table_001.add(TableCell(Paragraph(str(item[1])), background_color=c))  
        table_001.add(TableCell(Paragraph("$ " + str(item[2])), background_color=c))  
        table_001.add(TableCell(Paragraph("$ " + str(item[1] * item[2])), background_color=c))  
      
    # Optionally add some empty rows to have a fixed number of rows for styling purposes
    for row_number in range(3, 10):  
        c = even_color if row_number % 2 == 0 else odd_color  
        for _ in range(0, 4):  
            table_001.add(TableCell(Paragraph(" "), background_color=c))  
  
    table_001.add(TableCell(Paragraph("Subtotal", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 1,180.00", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Discounts", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,),col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 177.00", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Taxes", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 100.30", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Total", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT  ), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 1163.30", horizontal_alignment=Alignment.RIGHT)))  
    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))  
    table_001.no_borders()  
    return table_001

En la práctica, sustituiría las cadenas codificadas relacionadas con el subtotal, los impuestos y los precios totales con cálculos de los precios reales; sin embargo, esto depende en gran medida de la implementación subyacente de sus modelos de ‘Producto’, por lo que hemos agregó un sustituto para la abstracción. Una vez que agregamos esta tabla al documento también, podemos reconstruirla y echarle un vistazo.

Todo el método principal ahora debería verse algo así como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Create document
pdf = Document()

# Add page
page = Page()
pdf.append_page(page)

page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)

page_layout.add(
        Image(
        "https://s3.wikihtp.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
        width=Decimal(128),
        height=Decimal(128),
        ))


# Invoice information table
page_layout.add(_build_invoice_information())

# Empty paragraph for spacing
page_layout.add(Paragraph(" "))

# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())

# Itemized description
page_layout.add(_build_itemized_description_table())

with open("output2.pdf", "wb") as pdf_file_handle:
    PDF.dumps(pdf_file_handle, pdf)

Ejecutar este fragmento de código da como resultado:

factura borb 3

Creación de un esquema

Nuestro PDF está terminado y listo para servirse; sin embargo, podemos mejorarlo con dos pequeñas adiciones. Primero, podemos agregar un Esquema, que ayuda a los lectores como Adobe a navegar y generar un menú para sus archivos PDF:

1
2
3
4
5
# New import
from borb.pdf.page.page import DestinationType

# Outline  
pdf.add_outline("Your Invoice", 0, DestinationType.FIT, page_nr=0)

La función add_outline() acepta algunos argumentos:

  • título: el título que se mostrará en el menú lateral
  • nivel: qué tan profundo en el árbol estará algo. El nivel 0 es el nivel raíz.
  • Varios argumentos que componen un "destino"

Los destinos se pueden considerar como objetivos para los hipervínculos. Puede vincular a una página completa (que es lo que estamos haciendo en este ejemplo), pero también puede vincular a partes específicas de una página (por ejemplo, exactamente en la coordenada y 350).

Además, debe especificar cómo el lector debe presentar esa página; por ejemplo, ¿simplemente desea desplazarse a esa página y no hacer zoom? ¿Desea mostrar solo un área de destino, con el lector completamente ampliado en esa área en particular?

En esta línea de código, le pedimos al lector que muestre la página 0 (la primera página) y se asegure de que se ajuste a la ventana del lector (acercando/alejando si es necesario).

Una vez que haya agregado el esquema, debería verlo aparecer en el lector de su elección:

borb factura 4

Con varias páginas, puede crear un esquema más complejo y vincularlo a través de add_outline() para facilitar la navegación.

Incorporación de documentos JSON en facturas PDF

Dado que los archivos PDF no son muy compatibles con la computadora (en términos de lectura y decodificación inequívoca), a veces, es posible que deseemos agregar más formatos compatibles con la computadora si alguien desea procesar las facturas automáticamente.

Un estándar de factura originario de Alemania llamado ZUGFeRD (posteriormente adoptado por la UE) nos permite hacer facturas en PDF con más formatos de archivo legibles por computadora, como XML, que describe la factura y es fácilmente analizable. Además de estos, también puede incorporar otros documentos relacionados con su factura, como términos y acuerdos, una política de reembolso, etc.

Para incrustar cualquier tipo de archivo adicional en un archivo PDF, usando borb, podemos usar la función append_embedded_file().

Primero avancemos y creemos un diccionario para almacenar los datos de nuestra factura en JSON, que luego guardaremos en un archivo invoice_json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import json

# Creating a JSON file
invoice_json = {  
"items": [  
    {  
        "Description": "Product1",  
        "Quantity": 2,  
        "Unit Price": 50,  
        "Amount": 100,  
    },  
    {  
        "Description": "Product2",  
        "Quantity": 4,  
        "Unit Price": 60,  
        "Amount": 100,  
    },  
    {  
        "Description": "Labor",  
        "Quantity": 14,  
        "Unit Price": 60,  
        "Amount": 100,  
    },  
],  
"Subtotal": 1180,  
"Discounts": 177,  
"Taxes": 100.30,  
"Total": 1163.30,  
}  
invoice_json_bytes = bytes(json.dumps(invoice_json, indent=4), encoding="latin1")

Ahora, simplemente podemos insertar este archivo en nuestra factura en PDF:

1
pdf.append_embedded_file("invoice.json", invoice_json_bytes)

Una vez que ejecutamos el script nuevamente y almacenamos el documento, vamos:

borb factura 5

Conclusión

En esta guía, hemos visto cómo crear una factura en Python usando borb. Luego agregamos un esquema al archivo PDF para facilitar la navegación y observamos cómo agregar archivos adjuntos/archivos incrustados para acceder mediante programación al contenido del PDF. el PDF.