Skip to main content

Professional CFDI 4.0 XML to PDF converter for Mexico SAT

Project description

CFDI PDF

PyPI version Python Version License: MIT Tests Coverage

Biblioteca profesional para convertir CFDI 4.0 XML a PDF con templates modernos y extensibles.

El PDF generado siempre se nombra con el UUID del timbre fiscal ({uuid}.pdf) y el tamaño de página es carta (8.5 × 11 in), estándar en México.

Características

  • CFDI 4.0 Completo: Soporte total para la versión 4.0 del SAT
  • Tamaño carta: Todos los templates generan PDFs en tamaño carta (estándar SAT México)
  • Nombre por UUID: El archivo PDF siempre se nombra {uuid}.pdf para trazabilidad
  • QR SAT Oficial: Generación del código QR con especificaciones oficiales
  • 3 Templates incluidos: minimal, corporativo y clasico
  • Templates Extensibles: Sistema de templates basado en Jinja2 + HTML/CSS
  • Tipado Estricto: Pydantic v2 para validación robusta — Python 3.12+
  • Seguridad: Protección contra XXE, XML bombs y otros ataques
  • UTF-8 Completo: Manejo correcto de acentos, ñ y caracteres especiales
  • Catálogos SAT: Todos los catálogos oficiales del SAT
  • CLI Incluida: Herramienta de línea de comandos para conversiones rápidas

Instalación

pip install cfdi-pdf

Requiere Python 3.12 o superior.

Uso Rápido

CLI (Línea de Comandos)

# Un archivo — guarda {uuid}.pdf en el mismo directorio que el XML
cfdi-pdf factura.xml

# Directorio de salida personalizado
cfdi-pdf factura.xml --output-dir ./pdfs

# Procesamiento por lotes
cfdi-pdf enero/*.xml --output-dir ./pdfs

# Con logo y template
cfdi-pdf factura.xml --template corporativo --logo logo.png

# Ver templates disponibles
cfdi-pdf --list-templates

# Salida detallada
cfdi-pdf factura.xml --verbose

El comando imprime la ruta completa del PDF generado:

✓ factura.xml → /ruta/al/cce4d168-1234-5678-9abc-def012345678.pdf

API Python

from cfdi_pdf import CFDIPDF

pdf = CFDIPDF(template="minimal")

# Convierte y guarda {uuid}.pdf en el mismo directorio del XML
output_path = pdf.render(xml_path="factura.xml")
print(output_path)  # PosixPath('/ruta/cce4d168-...-uuid.pdf')

# Especificar directorio de salida
output_path = pdf.render(xml_path="factura.xml", output_dir="./pdfs")

# Con logo
output_path = pdf.render(
    xml_path="factura.xml",
    output_dir="./pdfs",
    logo_path="logo.png",
)

Desde string XML

from cfdi_pdf import CFDIPDF

pdf = CFDIPDF()

with open("factura.xml") as f:
    xml_content = f.read()

# Guarda {uuid}.pdf en el directorio indicado
output_path = pdf.render_from_string(xml_content, output_dir="./pdfs")

Obtener bytes (sin escribir a disco)

from cfdi_pdf import CFDIPDF

pdf = CFDIPDF()

# Retorna (bytes, nombre_archivo) sin tocar el sistema de archivos
pdf_bytes, filename = pdf.render_bytes(xml_path="factura.xml")

# filename = "cce4d168-1234-5678-9abc-def012345678.pdf"
# Útil para APIs web, S3, etc.
with open(filename, "wb") as f:
    f.write(pdf_bytes)

Templates Disponibles

Template Descripción Compatibilidad
minimal Diseño limpio y moderno (por defecto) WeasyPrint, Chromium
corporativo Azul corporativo #1a3a5c, encabezado destacado WeasyPrint, Chromium
clasico Blanco/negro, tipografía Times, máxima compatibilidad WeasyPrint, Chromium, wkhtmltopdf

Todos los templates:

  • Tamaño carta (8.5 × 11 in / 215.9 × 279.4 mm)
  • Sin propiedades CSS experimentales (compatible con motores de impresión estrictos)
  • Layout con tablas HTML en secciones fiscales (compatible con wkhtmltopdf)
  • Zebra striping con clases explícitas (no :nth-child)

Seleccionar template

from cfdi_pdf import CFDIPDF

pdf = CFDIPDF(template="corporativo")
output_path = pdf.render(xml_path="factura.xml")
cfdi-pdf factura.xml --template clasico

API de Referencia

CFDIPDF

CFDIPDF(
    template: str = "minimal",
    locale: str = "es_MX",
    currency_format: bool = True,
    custom_template_paths: list[str | Path] | None = None,
)
Método Descripción Retorna
render(xml_path, output_dir, template, logo_path) Convierte XML a PDF, guarda en disco Path al PDF
render_from_string(xml_content, output_dir, template, logo_path) Igual pero desde string Path al PDF
render_bytes(xml_path, template, logo_path) Sin escribir a disco (bytes, filename)
parse(xml_path) Parsea XML sin generar PDF CFDI
parse_string(xml_content) Parsea string XML CFDI
list_templates() Lista templates disponibles list[str]

Nota: El nombre del archivo siempre es {uuid}.pdf (UUID en minúsculas del timbre fiscal). Si el CFDI no tiene timbre fiscal, se lanza CFDIPDFError.

Variables disponibles en templates Jinja2

Variable Tipo Descripción
cfdi CFDI Objeto completo del CFDI
formatted.uuid str UUID formateado
formatted.fecha str Fecha de expedición
formatted.fecha_timbrado str | None Fecha de timbrado
formatted.total str Total formateado con moneda
formatted.sub_total str Subtotal formateado
formatted.descuento str | None Descuento formateado
formatted.sello_cfd str | None Sello CFD completo, dividido en líneas de 64 chars
formatted.sello_sat str | None Sello SAT completo, dividido en líneas de 64 chars
catalogs.tipo_comprobante str Descripción del tipo
catalogs.forma_pago str Descripción forma de pago
catalogs.metodo_pago str Descripción método de pago
catalogs.uso_cfdi str Descripción uso CFDI
catalogs.regimen_fiscal_emisor str Régimen del emisor
catalogs.regimen_fiscal_receptor str Régimen del receptor
catalogs.moneda str Descripción de moneda
qr_data str QR SAT en base64 (PNG)
logo_data str Logo en base64 data URL
cadena_original str Cadena original del timbre
format_currency(amount, moneda) callable Formatea importe
format_tax_rate(rate) callable Formatea tasa de impuesto

Crear Template Personalizado

  1. Crea un directorio con el nombre de tu template:
mis_templates/
└── mi_empresa/
    ├── template.html
    └── styles.css
  1. styles.css — usa siempre tamaño carta:
@page {
    size: letter;          /* 8.5 × 11 in — tamaño carta México */
    margin: 15mm 15mm 20mm 15mm;
}
  1. template.html — usa las variables Jinja2 estándar:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <h1>{{ catalogs.tipo_comprobante }}</h1>
    <p>UUID: {{ formatted.uuid }}</p>
    <p>Emisor: {{ cfdi.emisor.nombre }}</p>
    <p>Total: {{ formatted.total }}</p>
    {% if qr_data %}
        <img src="data:image/png;base64,{{ qr_data }}" alt="QR SAT">
    {% endif %}
</body>
</html>
  1. Registra la ruta y usa tu template:
from pathlib import Path
from cfdi_pdf import CFDIPDF

pdf = CFDIPDF(
    template="mi_empresa",
    custom_template_paths=[Path("./mis_templates")],
)
output_path = pdf.render(xml_path="factura.xml")

Estructura del Proyecto

src/cfdi_pdf/
├── api.py              # API principal (CFDIPDF)
├── cli.py              # Interfaz de línea de comandos
├── exceptions.py       # Excepciones personalizadas
├── models/             # Modelos Pydantic v2
│   ├── cfdi.py
│   ├── emisor.py
│   ├── receptor.py
│   ├── concepto.py
│   ├── impuestos.py
│   └── timbre.py
├── parser/             # Parser XML seguro
│   ├── xml_parser.py
│   └── sanitizer.py
├── qr/                 # Generador QR SAT oficial
│   └── generator.py
├── render/             # Motor de renderizado
│   ├── engine.py
│   └── template.py
├── sat/                # Catálogos SAT
│   ├── catalogs.py
│   └── helpers.py
├── utils/              # Utilidades
│   └── formatters.py
└── templates/          # Templates incluidos
    ├── minimal/
    │   ├── template.html
    │   └── styles.css
    ├── corporativo/
    │   ├── template.html
    │   └── styles.css
    └── clasico/
        ├── template.html
        └── styles.css

Requisitos

  • Python 3.12+
  • WeasyPrint >= 60.0
  • Pydantic >= 2.0.0
  • Jinja2 >= 3.1.0
  • qrcode >= 7.4
  • Pillow >= 10.0.0
  • lxml >= 4.9.0

Desarrollo

Configuración del Entorno

git clone https://github.com/jesusmrv81/xmltopdf.git
cd cfdi-pdf

python -m venv venv
source venv/bin/activate      # Linux/macOS
# venv\Scripts\activate       # Windows

pip install -e ".[dev]"

Ejecutar Tests

# Todos los tests
pytest

# Con cobertura HTML
pytest --cov=cfdi_pdf --cov-report=html

# Tests específicos
pytest tests/test_parser.py -v

Linting y Formateo

ruff check .          # linter
ruff check --fix .    # auto-fix
black .               # formatter
mypy src              # type checker

Build

python -m build
twine upload dist/*   # requiere credenciales PyPI

Especificaciones Técnicas

CFDI 4.0

  • Namespace: http://www.sat.gob.mx/cfd/4
  • Timbre Fiscal Digital: Versión 1.1
  • Catálogos SAT: Completos y actualizados

QR SAT

URL de verificación: https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx

Parámetros: id (UUID), re (RFC emisor), rr (RFC receptor), tt (total 6 decimales), fe (últimos 8 chars del sello).

Seguridad

  • Protección XXE: resolve_entities=False en lxml
  • XML Bombs: no_network=True, huge_tree=False
  • Sandboxing: SandboxedEnvironment de Jinja2
  • Sanitización: Regex compilado contra caracteres inválidos XML 1.0

Compatibilidad CSS

Los templates evitan intencionalmente propiedades CSS experimentales para máxima compatibilidad con motores de impresión:

Propiedad Estado
@page size: letter Soportado en todos
display: table Soportado en todos
display: flex Solo en zonas decorativas no-fiscales
display: grid No usado
CSS variables (var(--)) No usado
:nth-child en tablas No usado — zebra con clases explícitas
border-radius (clasico) No usado

Roadmap

  • Soporte para complemento Pagos 2.0
  • Soporte para complemento Nómina 1.2
  • Soporte para Carta Porte 3.1
  • Validación contra esquemas XSD del SAT
  • API REST lista para deploy
  • Integración con PACs

Licencia

MIT License — ver archivo LICENSE para detalles.

Soporte


Nota: Esta biblioteca no está afiliada oficialmente con el SAT. Verifica siempre la validez de los CFDIs en el portal oficial del SAT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cfdi_pdf-0.1.3.tar.gz (45.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cfdi_pdf-0.1.3-py3-none-any.whl (49.3 kB view details)

Uploaded Python 3

File details

Details for the file cfdi_pdf-0.1.3.tar.gz.

File metadata

  • Download URL: cfdi_pdf-0.1.3.tar.gz
  • Upload date:
  • Size: 45.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cfdi_pdf-0.1.3.tar.gz
Algorithm Hash digest
SHA256 438c0cc4dd828fe2612641c9de1928efeecbe840ccfa1071381cd9636bb1f33c
MD5 7fc4ebf0a8f6bcfd171ca8a496626b7e
BLAKE2b-256 21f4624e95d0b1bf7d135a99fe1c9b2036c51c2aa351c50a29b9b81f34657f87

See more details on using hashes here.

Provenance

The following attestation bundles were made for cfdi_pdf-0.1.3.tar.gz:

Publisher: ci.yml on jesusmrv81/xmltopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cfdi_pdf-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: cfdi_pdf-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 49.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cfdi_pdf-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 052ffe6cd9a322c1298a2aa86004eee873979ce6d63803aca83288361bff2be6
MD5 657240d401f7f88cb0f058cb38270104
BLAKE2b-256 f34ae81eafc4ff4fa17999f55a24cdc138fd0cac22ceccd2245b4221c346a769

See more details on using hashes here.

Provenance

The following attestation bundles were made for cfdi_pdf-0.1.3-py3-none-any.whl:

Publisher: ci.yml on jesusmrv81/xmltopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page