0001. Microservicio dedicado de generación de documentos (svc-documents)¶
Estado: Propuesto Fecha: 2026-08-31 Autor: Duval Alcivar
Contexto¶
El ERP y CRM requieren generar documentos en múltiples formatos: PDFs facturas, contratos, reportes; Excels para exportaciones contables e inventariales; y documentos Word para plantillas editables. Actualmente, cada microservicio que necesita generar un documento implementa su propia lógica de renderizado, lo que genera:
- Duplicación de código: cada equipo re implementa conversión de datos a PDF/Excel con distintas librerías y patrones, creando inconsistencias visuales entre módulos.
- Acoplamiento tecnológico: si se cambia la librería de PDF o el motor de renderizado, hay que tocar cada microservicio individualmente.
- Escalabilidad desbalanceada: la generación de documentos pesados (reportes de 50+ páginas, Excels con miles de filas) consume CPU y memoria de forma impredecible, afectando la latencia de operaciones transaccionales críticas del dominio.
- Dificultad para estandarizar: no hay un formato visual único de empresa, porque cada módulo genera documentos con estilos diferentes.
Decisión¶
Se crea el microservicio svc-documents como el único responsable de la generación y renderizado de documentos en todo el ecosistema ERP/CRM.
Stack tecnológico:
- Lenguaje: Python 3.12+ — el mismo stack del resto del backend (consistencia de equipo, reutilización de conocimiento, deployment unificado).
- Framework: FastAPI — ya es el estándar del proyecto (ver ADR 0001 y 0005); ofrece async nativo para manejar generación concurrente sin bloquear el event loop; OpenAPI automático para integración con clientes.
- Despliegue: contenedor Docker independiente, con su propio pool de workers, desacoplado del ciclo de vida de otros microservicios.
Interfaces de comunicación:
- REST interno (sincrónico): para documentos simples que requieren respuesta inmediata (tickets, facturas de baja complejidad).
- Cola de mensajes (asincrónico): para generación pesada (reportes mensuales, Excels masivos) que puede tardar; el cliente recibe un ID de job y consulta el estado vía polling o WebSocket.
Librerías recomendadas:
| Formato | Librería | Justificación |
|---|---|---|
WeasyPrint |
Renderiza HTML/CSS a PDF con soporte completo de CSS3, tablas, headers/footers con paginación, y fuentes embebidas. Es la opción más madura para PDFs con diseño complejo (facturas con tablas, logos, códigos de barras). Alternativa: reportlab (más control bajo pero más código; solo justificable si se necesita generación programática extrema sin HTML). |
|
| Excel | openpyxl |
Lectura/escritura nativa de .xlsx con soporte de fórmulas, estilos, gráficos, validaciones, y protección de celdas. Es la librería estándar de facto para Excel en Python. Para generación desde datos tabulares simples también se puede usar xlsxwriter (solo escritura, pero más rápido para archivos grandes). |
| Word | python-docx |
Creación y manipulación de .docx con soporte de estilos, tablas, imágenes, y headers/footers. Es la única opción madura y mantenida para Word en Python. Solo se usará para plantillas editables que el cliente descarga y modifica. |
| HTML a PDF | Jinja2 + WeasyPrint |
Las plantillas de documentos se renderizan como HTML con Jinja2 y se convierten a PDF con WeasyPrint. Permite que diseño/branding defina plantillas HTML estándar sin tocar código Python. |
| Código de barras/QR | python-barcode + qrcode |
Generación de GS1-128, Code128, QR para facturas y etiquetas de inventario. Librerías ligeras y dependientes solo de PIL/Pillow. |
Estructura interna — monolito modular por dominio (vertical slice):
El servicio se organiza por dominio de negocio, no por capa técnica:
router, service, repository, models y schemas de un mismo dominio viven
juntos en su propia carpeta (patrón común, ver estructura-microservicios.md
y ADR 0001 y 0005 globales). La generación por formato es un dominio distinto
para aislar la lógica de cada renderizado, pero todos comparten el mismo
backend de contratos JSON y la cola de jobs.
svc-documents/
├── app/
│ ├── main.py # arranque FastAPI; registra los routers de cada dominio
│ ├── core/
│ │ ├── config.py # variables de entorno (Pydantic Settings)
│ │ ├── security.py # valida JWT emitido por svc-identidad
│ │ ├── exceptions.py # excepciones de negocio -> HTTP errors
│ │ └── logging.py # logging estructurado (JSON)
│ ├── api/
│ │ └── deps.py # get_db(), get_current_user() — transversal
│ ├── domains/
│ │ ├── pdf/ # renderizado de PDF (WeasyPrint + Jinja2)
│ │ │ ├── router.py # endpoints REST síncronos
│ │ │ ├── service.py # reglas de negocio del renderizado PDF
│ │ │ ├── repository.py # acceso a datos / plantillas del PDF
│ │ │ ├── models.py # entidades SQLAlchemy
│ │ │ └── schemas.py # contratos Pydantic de entrada/salida
│ │ ├── excel/ # exportaciones contables/inventariales (openpyxl)
│ │ │ └── ... (misma forma)
│ │ ├── word/ # plantillas editables del cliente (python-docx)
│ │ │ └── ... (misma forma)
│ │ └── jobs/ # generación asíncrona pesada (cola de mensajes)
│ │ ├── router.py # endpoints de estado de job (polling/WebSocket)
│ │ ├── service.py # orquestación de jobs pesados
│ │ └── worker.py # consumidor de la cola de generación
│ ├── shared/ # SOLO lo que de verdad cruza 2+ dominios de ESTE servicio
│ │ ├── templates/ # branding: logos, fuentes, CSS base (Jinja2)
│ │ └── renderer_base.py # abstracción común por formato (if aplica)
│ └── events/
│ ├── publisher.py # outbox: publica eventos de documentos generados
│ └── subscriber.py # consume eventos de otros servicios (si aplica)
├── alembic/
│ ├── versions/ # migraciones versionadas (una por cambio)
│ └── env.py
├── templates/ # branding compartido: logos, fuentes, CSS base
├── tests/
│ ├── unit/<dominio>/ # mismo árbol que domains/, por dominio
│ └── integration/<dominio>/
├── pyproject.toml # + poetry.lock (ADR 0005 global)
├── .env.example # nunca .env real committeado
└── Dockerfile
Cada microservicio (ERP, CRM) envía datos en JSON estructurado y recibe
un documento renderizado; no necesita saber cómo se genera internamente.
La relación con svc-identidad para validar el token y la empresa sigue el
flujo del ADR 0006 global; la generación pesada consume de una cola de
mensajes (Redis Streams o RabbitMQ, pendiente de fijar).
Alternativas descartadas¶
- Generar documentos dentro de cada microservicio — descartado porque duplica librerías y lógica en cada proyecto, dificulta estandarizar el formato visual, y acopla el rendimiento de generación al servicio de negocio principal.
- Usar una librería de bajo nivel como
reportlabexclusivamente — descartada porque exige definir coordenadas absolutas para cada elemento del PDF; para documentos con diseño complejo (facturas con múltiples tablas, headers dinámicos), el costo de mantenimiento de plantillas en código es prohibitivo comparado con HTML/CSS. - Usar un servicio externo (ej. DocRaptor, PDF.co) — descartado por dependencia de terceros, costos recurrentes, y restricciones de datos sensibles (facturas con datos tributarios no pueden salir del perímetro de la empresa).
- **Librería
pdfkit(wkhtmltopdf)` — descartada porque depende de un binario del sistema operativo, tiene soporte limitado de CSS moderno, y su mantenimiento es irregular. WeasyPrint es pura Python, más fácil de containerizar y más activo en desarrollo.
Consecuencias¶
- Ganancia: un solo punto de mantenimiento para toda la lógica de documentos; cambiar la fuente o el logo de factura se hace en un solo lugar.
- Ganancia: escalabilidad independiente; si los reportes pesados saturan CPU, se escalan los workers de svc-documents sin afectar los servicios de negocio.
- Ganancia: estandarización visual de todos los documentos de la empresa con plantillas HTML compartidas.
- Sacrificio: latencia adicional de red para documentos síncronos (mitigable con comunicación interna vía gRPC o HTTP keep-alive en la misma red de Kubernetes).
- Pendiente: definir el contrato de datos de entrada (schema JSON) para cada tipo de documento; este diseño se documentará en ADRs específicos cuando se implementen los primeros templates.
- Pendiente: configurar cola de mensajes (Redis Streams o RabbitMQ) para la generación asíncrona de reportes pesados.
Microservicio dedicado dentro de core/ porque es una capacidad
transversal que ningún dominio de negocio posee en exclusiva: ERP y CRM
lo consumen por igual.