Files
urieljarethandClaude Fable 5 69c74faabb Restaurar funcionalidades desde stash + setup de despliegue Coolify
- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop
- Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED)
- docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_*
- Endpoint público /api/health (verifica BD) para healthchecks
- seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas)
- .env.example, .dockerignore, uvicorn con --proxy-headers
- Limpieza: .xlsx y __pycache__ fuera del repo

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 02:21:18 -06:00

553 lines
22 KiB
Python

"""PDF Generator for Cotizador E3 — port of pdf-generator.ts using ReportLab."""
from __future__ import annotations
import base64
import io
from datetime import datetime
from typing import Any
from reportlab.lib import colors
from reportlab.lib.pagesizes import LETTER
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from app.services.calculators import FASES_SHORT, PLANES_BUCEFALO, bucefalo_precio, detalle_modelo, calcular_totales_opcion
PRIMARY_DEFAULT = "#2563eb"
DARK_DEFAULT = "#1e293b"
MUTED = "#64748b"
BORDER = "#cbd5e1"
LIGHT_BG = "#f1f5f9"
WHITE = "#ffffff"
PAGE_W, PAGE_H = LETTER
MARGIN_TOP = 45
MARGIN_BOTTOM = 55
MARGIN_LEFT = 50
MARGIN_RIGHT = 50
CONTENT_W = PAGE_W - MARGIN_LEFT - MARGIN_RIGHT
def _hex_to_rgb(hex_color: str) -> colors.Color:
h = hex_color.lstrip("#")
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
return colors.Color(r / 255, g / 255, b / 255)
def _fmt_currency(n: float) -> str:
return f"${n:,.2f}"
def _wrap_text(c, text: str, font: str, size: float, max_width: float) -> list[str]:
"""Envuelve texto en lineas que caben en max_width (respeta saltos de linea)."""
lines: list[str] = []
for paragraph in str(text or "").split("\n"):
words = paragraph.split()
line = ""
for word in words:
test = f"{line} {word}".strip()
if c.stringWidth(test, font, size) > max_width and line:
lines.append(line)
line = word
else:
line = test
lines.append(line)
return lines
def _fmt_date(d: datetime) -> str:
months = [
"enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre",
]
return f"{d.day} de {months[d.month - 1]} de {d.year}"
def _load_logo(logo_base64: str | None, logo_mime: str | None) -> ImageReader | None:
if not logo_base64:
return None
try:
raw = base64.b64decode(logo_base64)
return io.BytesIO(raw)
except Exception:
return None
def generate_cotizacion_pdf(data: dict[str, Any]) -> bytes:
"""Generate a professional quotation PDF.
Args:
data: dict with keys matching CotizacionPDFData interface:
numero, clienteNombre, clienteEmpresa, asesorNombre, fecha, vigencia,
moneda, tipoCambio, proyecto, esquemaPago, servicios[],
planBucefaloNivel, planBucefaloPrecio, incluirBonos,
configBancaria{}, colorPrimario, colorSecundario, logoBase64, logoMime
"""
PRIMARY = data.get("colorPrimario") or PRIMARY_DEFAULT
DARK = data.get("colorSecundario") or DARK_DEFAULT
primary_rgb = _hex_to_rgb(PRIMARY)
dark_rgb = _hex_to_rgb(DARK)
muted_rgb = _hex_to_rgb(MUTED)
border_rgb = _hex_to_rgb(BORDER)
light_rgb = _hex_to_rgb(LIGHT_BG)
white_rgb = _hex_to_rgb(WHITE)
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=LETTER)
c.setTitle(f"Cotizacion {data.get('numero', '')}")
logo = _load_logo(data.get("logoBase64"), data.get("logoMime"))
servicios = data.get("servicios", [])
plan_nivel = data.get("planBucefaloNivel")
plan_precio = data.get("planBucefaloPrecio", 0)
incluir_bonos = data.get("incluirBonos", False)
cfg_bancaria = data.get("configBancaria") or {}
# ── PAGE 1: COVER ──────────────────────────────
c.setFillColor(white_rgb)
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
y = PAGE_H - 80
if logo:
try:
c.drawImage(logo, MARGIN_LEFT, y - 20, width=65, height=65, preserveAspectRatio=True, mask="auto")
y -= 85
except Exception:
logo = None
if not logo:
c.setFont("Helvetica-Bold", 28)
c.setFillColor(primary_rgb)
c.drawString(MARGIN_LEFT, y, "UJ")
c.setFont("Helvetica", 9)
c.setFillColor(muted_rgb)
c.drawString(MARGIN_LEFT + 48, y - 15, "Uriel Jareth Consulting")
y -= 40
bar_y = y
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, bar_y, CONTENT_W, 4, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 36)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT, bar_y - 40, "Cotizacion")
c.setFont("Helvetica", 18)
c.setFillColor(primary_rgb)
c.drawString(MARGIN_LEFT, bar_y - 65, data.get("numero", ""))
iy = bar_y - 100
cL = MARGIN_LEFT
cR = MARGIN_LEFT + CONTENT_W * 0.52
info_l = [
("Cliente", data.get("clienteNombre", "")),
("Empresa", data.get("clienteEmpresa", "")),
("Proyecto", data.get("proyecto", "")),
("Asesor", data.get("asesorNombre", "")),
]
info_r = [
("Fecha", _fmt_date(data.get("fecha", datetime.now()))),
("Vigencia", _fmt_date(data.get("vigencia", datetime.now()))),
("Moneda", data.get("moneda", "MXN")),
("Esquema", data.get("esquemaPago", "")),
]
for i in range(max(len(info_l), len(info_r))):
if i < len(info_l) and info_l[i][1]:
c.setFont("Helvetica", 9)
c.setFillColor(muted_rgb)
c.drawString(cL, iy, info_l[i][0])
c.setFont("Helvetica", 11)
c.setFillColor(dark_rgb)
c.drawString(cL, iy - 14, info_l[i][1])
if i < len(info_r) and info_r[i][1]:
c.setFont("Helvetica", 9)
c.setFillColor(muted_rgb)
c.drawString(cR, iy, info_r[i][0])
c.setFont("Helvetica", 11)
c.setFillColor(dark_rgb)
c.drawString(cR, iy - 14, str(info_r[i][1]))
iy -= 32
# ── PAGE 2+: RESUMEN ──────────────────────────
c.showPage()
y = PAGE_H - MARGIN_TOP
if logo:
try:
c.drawImage(logo, MARGIN_LEFT + CONTENT_W - 70, y - 5, width=22, height=22, preserveAspectRatio=True, mask="auto")
except Exception:
pass
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y - 16, 4, 16, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 15)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 12, y - 15, "Hoja Resumen")
y -= 28
cliente_ref = data.get("clienteEmpresa") or data.get("clienteNombre", "")
c.setFont("Helvetica", 8)
c.setFillColor(muted_rgb)
c.drawString(MARGIN_LEFT, y, f"En atencion a: {cliente_ref}")
c.drawString(MARGIN_LEFT + CONTENT_W * 0.45, y, f"No. Cotizacion: {data.get('numero', '')}")
y -= 11
c.drawString(MARGIN_LEFT, y, f"Asesor: {data.get('asesorNombre', '')}")
c.drawString(MARGIN_LEFT + CONTENT_W * 0.45, y, f"Fecha: {_fmt_date(data.get('fecha', datetime.now()))} | Moneda: {data.get('moneda', 'MXN')}")
y -= 18
col_nombre = MARGIN_LEFT
col_tipo = MARGIN_LEFT + CONTENT_W - 200
col_tiempo = MARGIN_LEFT + CONTENT_W - 120
col_precio = MARGIN_LEFT + CONTENT_W - 60
es_doble = bool(data.get("esDoble"))
opciones_meta = data.get("opcionesMetadata") or {}
def _draw_table_header(y_pos):
c.setFillColor(light_rgb)
c.rect(MARGIN_LEFT, y_pos - 2, CONTENT_W, 16, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 7)
c.setFillColor(dark_rgb)
c.drawString(col_nombre + 6, y_pos + 3, "Servicio")
c.drawString(col_tipo + 4, y_pos + 3, "Tipo")
c.drawString(col_tiempo + 4, y_pos + 3, "Entrega")
c.drawRightString(col_precio + 60, y_pos + 3, "Precio")
y_pos -= 4
c.setStrokeColor(border_rgb)
c.setLineWidth(0.3)
c.line(MARGIN_LEFT, y_pos, MARGIN_LEFT + CONTENT_W, y_pos)
return y_pos - 14
unicos = [s for s in servicios if s.get("tipoPago") == "unico"]
mensuales = [s for s in servicios if s.get("tipoPago") == "mensual"]
def _draw_section(srv_list, tipo_label, titulo_total, y_pos):
if not srv_list:
return y_pos
current_fase = -1
max_y = PAGE_H - MARGIN_BOTTOM - 5
for serv in srv_list:
fase = serv.get("fase", 0)
if fase != current_fase:
current_fase = fase
if y_pos - 20 < max_y - PAGE_H:
c.showPage()
y_pos = PAGE_H - MARGIN_TOP
c.setFillColor(light_rgb)
c.rect(MARGIN_LEFT, y_pos - 14, CONTENT_W, 14, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 7)
c.setFillColor(primary_rgb)
c.drawString(MARGIN_LEFT + 6, y_pos - 11, FASES_SHORT.get(fase, f"FASE {fase}"))
y_pos -= 18
c.setFont("Helvetica", 8)
c.setFillColor(dark_rgb)
nombre = serv.get("nombre", "")
c.drawString(col_nombre + 6, y_pos - 10, nombre[:60])
c.setFont("Helvetica", 7)
c.setFillColor(muted_rgb)
c.drawString(col_tipo + 4, y_pos - 10, tipo_label)
c.drawString(col_tiempo + 4, y_pos - 10, serv.get("tiempoEntrega", "")[:15])
c.setFont("Helvetica-Bold", 8)
c.setFillColor(dark_rgb)
c.drawRightString(col_precio + 60, y_pos - 10, _fmt_currency(serv.get("precio", 0)))
y_pos -= 14
detalle = detalle_modelo(serv)
if detalle:
c.setFont("Helvetica-Oblique", 6.5)
c.setFillColor(muted_rgb)
c.drawString(col_nombre + 10, y_pos - 7, detalle[:90])
y_pos -= 9
entregables = serv.get("entregables", [])
if entregables:
half = (len(entregables) + 1) // 2
for idx in range(half):
e1 = entregables[idx]
e2 = entregables[idx + half] if idx + half < len(entregables) else None
c.setFont("Helvetica", 6)
c.setFillColor(muted_rgb)
c.drawString(col_nombre + 10, y_pos - 8, f"\u2022 {e1[:50]}")
if e2:
c.drawString(col_nombre + CONTENT_W * 0.48, y_pos - 8, f"\u2022 {e2[:50]}")
y_pos -= 9
c.setStrokeColor(_hex_to_rgb("#e5e7eb"))
c.setLineWidth(0.2)
c.line(col_nombre + 6, y_pos, MARGIN_LEFT + CONTENT_W, y_pos)
y_pos -= 6
total = sum(s.get("precio", 0) for s in srv_list)
y_pos -= 4
c.setFillColor(_hex_to_rgb("#f8fafc"))
c.rect(MARGIN_LEFT, y_pos - 2, CONTENT_W, 18, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 8.5)
c.setFillColor(primary_rgb)
c.drawString(col_nombre + 6, y_pos + 3, titulo_total)
c.drawRightString(col_precio + 60, y_pos + 3, _fmt_currency(total))
y_pos -= 18
c.setFont("Helvetica", 6)
c.setFillColor(muted_rgb)
c.drawString(col_nombre + 6, y_pos, "(Precios en Moneda Nacional, no incluyen IVA)")
y_pos -= 14
return y_pos
def _draw_opcion_header(op, y_pos):
meta = opciones_meta.get(op) or {}
max_y = PAGE_H - MARGIN_BOTTOM - 5
if y_pos - 30 < max_y:
c.showPage()
y_pos = PAGE_H - MARGIN_TOP
titulo = meta.get("titulo")
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y_pos - 18, CONTENT_W, 18, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 10)
c.setFillColor(white_rgb)
c.drawString(MARGIN_LEFT + 8, y_pos - 13, f"OPCION {op}" + (f": {titulo}" if titulo else ""))
y_pos -= 24
desc = meta.get("descripcion")
if desc:
c.setFont("Helvetica", 7.5)
c.setFillColor(dark_rgb)
for line in _wrap_text(c, desc, "Helvetica", 7.5, CONTENT_W - 12):
c.drawString(MARGIN_LEFT + 6, y_pos - 8, line)
y_pos -= 10
y_pos -= 2
no_incluye = meta.get("noIncluye")
if no_incluye:
c.setFont("Helvetica-Oblique", 7)
c.setFillColor(muted_rgb)
for line in _wrap_text(c, f"No incluye: {no_incluye}", "Helvetica-Oblique", 7, CONTENT_W - 12):
c.drawString(MARGIN_LEFT + 6, y_pos - 8, line)
y_pos -= 10
y_pos -= 4
return y_pos
def _draw_comparativa(y_pos):
t1 = calcular_totales_opcion(servicios, "1")
t2 = calcular_totales_opcion(servicios, "2")
max_y = PAGE_H - MARGIN_BOTTOM - 5
if y_pos - 90 < max_y:
c.showPage()
y_pos = PAGE_H - MARGIN_TOP
y_pos -= 6
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y_pos - 10, 3, 10, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 10)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 10, y_pos - 8, "Comparativa de opciones")
y_pos -= 22
c_label = MARGIN_LEFT
c_op1 = MARGIN_LEFT + CONTENT_W * 0.45
c_op2 = MARGIN_LEFT + CONTENT_W * 0.72
t1_tit = (opciones_meta.get("1") or {}).get("titulo")
t2_tit = (opciones_meta.get("2") or {}).get("titulo")
c.setFillColor(light_rgb)
c.rect(MARGIN_LEFT, y_pos - 2, CONTENT_W, 16, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(dark_rgb)
c.drawString(c_label + 6, y_pos + 3, "Concepto")
c.drawString(c_op1, y_pos + 3, f"Opcion 1" + (f" - {t1_tit}" if t1_tit else ""))
c.drawString(c_op2, y_pos + 3, f"Opcion 2" + (f" - {t2_tit}" if t2_tit else ""))
y_pos -= 18
filas = [
("Total unico (c/IVA)", _fmt_currency(t1["totalUnico"] * 1.16), _fmt_currency(t2["totalUnico"] * 1.16)),
("Total mensual (c/IVA)", _fmt_currency(t1["totalMensual"] * 1.16), _fmt_currency(t2["totalMensual"] * 1.16)),
("Horas estimadas", f"{t1['horas']:g} h", f"{t2['horas']:g} h"),
]
for lab, v1, v2 in filas:
c.setFont("Helvetica", 8)
c.setFillColor(dark_rgb)
c.drawString(c_label + 6, y_pos - 8, lab)
c.setFont("Helvetica-Bold", 8)
c.drawString(c_op1, y_pos - 8, v1)
c.drawString(c_op2, y_pos - 8, v2)
c.setStrokeColor(_hex_to_rgb("#e5e7eb"))
c.setLineWidth(0.2)
c.line(c_label + 6, y_pos - 12, MARGIN_LEFT + CONTENT_W, y_pos - 12)
y_pos -= 14
return y_pos - 8
if es_doble:
for op in ("1", "2"):
y = _draw_opcion_header(op, y)
y = _draw_table_header(y)
u = [s for s in unicos if s.get("opcion") in (op, "ambas")]
me = [s for s in mensuales if s.get("opcion") in (op, "ambas")]
if u:
y = _draw_section(u, "Unico", f"Total Pago Unico - Opcion {op}", y)
if me:
y = _draw_section(me, "Mensual", f"Total Pago Mensual - Opcion {op}", y)
y = _draw_comparativa(y)
else:
y = _draw_table_header(y)
if unicos:
y = _draw_section(unicos, "Unico", "Total Pago Unico", y)
if mensuales:
y = _draw_section(mensuales, "Mensual", "Total Pago Mensual", y)
if plan_nivel:
label = plan_nivel.capitalize()
c.setFillColor(light_rgb)
c.rect(MARGIN_LEFT, y - 2, CONTENT_W, 14, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(dark_rgb)
c.drawString(col_nombre + 6, y + 3, f"Plan Bucefalo CRM - {label}")
c.drawRightString(col_precio + 60, y + 3, _fmt_currency(plan_precio))
y -= 20
if incluir_bonos:
y -= 6
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y - 10, 3, 10, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 10, y - 8, "Bonos (Pago en una exhibicion)")
y -= 20
bonos = [
"Bono 1: 30 min mensuales en servicios Centinela (Sitio Web)",
"Bono 2: Workshop Estrategico de Buyer Persona",
"Bono 3: Workshop de Propuestas de Valor y Oferta Irresistible",
"Bono 4: 1 ano de Membresia Premium",
"Bono 5: Un mes gratis de Bucefalo CRM",
"Bono 6: Script de Ventas con mas de 100 complementos",
]
for b in bonos:
c.setFont("Helvetica", 7)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 8, y - 8, f"\u2713 {b}")
y -= 12
# ── T&C PAGE ──────────────────────────────────
c.showPage()
y = PAGE_H - MARGIN_TOP
if logo:
try:
c.drawImage(logo, MARGIN_LEFT + CONTENT_W - 70, y - 5, width=22, height=22, preserveAspectRatio=True, mask="auto")
except Exception:
pass
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y - 16, 4, 16, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 15)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 12, y - 15, "Terminos y Condiciones")
y -= 30
def _section_title(title, yy):
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, yy - 9, 3, 9, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 10, yy - 8, title)
return yy - 20
def _draw_bullets(title, items, yy):
yy = _section_title(title, yy)
for t in items:
c.setFont("Helvetica", 7.5)
c.setFillColor(dark_rgb)
# Simple text wrapping
words = t.split()
line = ""
for word in words:
test = f"{line} {word}".strip()
if c.stringWidth(test, "Helvetica", 7.5) > CONTENT_W - 10:
c.drawString(MARGIN_LEFT + 2, yy - 8, f"\u2022 {line}")
yy -= 11
line = word
else:
line = test
if line:
c.drawString(MARGIN_LEFT + 2, yy - 8, f"\u2022 {line}")
yy -= 11
return yy - 8
y = _draw_bullets("Condiciones", [
"Esta cotizacion tiene una vigencia de 15 dias habiles.",
"Cualquier ajuste al proyecto despues de la aprobacion del contenido afectara la fecha de entrega y por consiguiente el costo.",
"El cliente debera proporcionar la informacion solicitada por Uriel Jareth Consulting en tiempo y forma.",
"Si la falta de informacion provoca un excedente en los plazos de entrega del proyecto, las horas adicionales de servicio se cotizaran por separado.",
"Los pagos correspondientes a los servicios mensuales deberan realizarse en los primeros 5 dias del mes.",
"Todo el material e informacion necesarios para la realizacion del sitio web deberan ser entregados en un plazo maximo de 40 dias naturales a partir del arranque del proyecto.",
], y)
y = _draw_bullets("Que no incluye el proyecto?", [
"Generacion de disenos, videos, traducciones, cambios de divisas y unidades, o cualquier servicio externo a lo cotizado.",
"Redaccion de entradas de Blog.",
"Integracion de Servicios de terceros ajenos a los cotizados.",
"Servicio de Recuperacion de Accesos de: Google Analytics, Google Tag Manager, Google Search Console, Google Ads, y Meta Ads.",
"Creacion de Redes Sociales (En caso de requerir el servicio incluira un costo adicional).",
], y)
y = _draw_bullets("Notas", [
"El presente proyecto debera tener un responsable oficial.",
"La Hora Centinela tiene un precio de $700.00 MXN.",
"Los archivos editables/fuente (AI, PSD) son propiedad intelectual de la agencia. Si requiere los archivos editables, estos pueden ser adquiridos abonando una tarifa de liberacion (buy-out fee).",
"Si el proyecto se pausa por razones ajenas a Uriel Jareth Consulting, esto generara costo extra del 15% al 30% para retomar el proyecto.",
], y)
y -= 8
c.setFillColor(primary_rgb)
c.rect(MARGIN_LEFT, y - 9, 3, 9, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 10, y - 8, "Datos Bancarios")
y -= 20
razon_social = cfg_bancaria.get("razon_social", "URIEL JARETH ALVARADO ORTIZ")
rfc = cfg_bancaria.get("rfc", "AAOU970201SU7")
clabe_nac = cfg_bancaria.get("clabe_interbancaria", cfg_bancaria.get("cuenta_nacional", ""))
cuenta_nac = cfg_bancaria.get("cuenta_nacional", "")
banco_nac = "BBVA" if cuenta_nac else ""
box_h = 48
c.setFillColor(light_rgb)
c.setStrokeColor(border_rgb)
c.rect(MARGIN_LEFT, y - box_h, CONTENT_W, box_h, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 7)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 8, y - 12, "Transferencia Nacional")
c.setFont("Helvetica", 6.5)
if cuenta_nac:
c.drawString(MARGIN_LEFT + 8, y - 24, f"Cuenta: {cuenta_nac}")
if clabe_nac:
c.drawString(MARGIN_LEFT + CONTENT_W * 0.45, y - 24, f"CLABE: {clabe_nac}")
c.drawString(MARGIN_LEFT + 8, y - 35, f"Razon Social: {razon_social}")
c.drawString(MARGIN_LEFT + CONTENT_W * 0.45, y - 35, f"RFC: {rfc}")
if banco_nac:
c.drawString(MARGIN_LEFT + 8, y - 46, f"Banco: {banco_nac}")
y -= box_h + 8
c.setFillColor(light_rgb)
c.rect(MARGIN_LEFT, y - box_h, CONTENT_W, box_h, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 7)
c.setFillColor(dark_rgb)
c.drawString(MARGIN_LEFT + 8, y - 12, "Transferencia Internacional")
c.setFont("Helvetica", 6.5)
c.drawString(MARGIN_LEFT + 8, y - 24, f"Beneficiario: {razon_social}")
c.drawString(MARGIN_LEFT + 8, y - 35, "Banco: BBVA Mexico")
c.drawString(MARGIN_LEFT + CONTENT_W * 0.45, y - 35, "SWIFT: BCMRMXMMPYM")
# ── FOOTER ON ALL PAGES ──────────────────────
page_count = c.getPageNumber()
for i in range(1, page_count + 1):
c.setPageSize(LETTER)
c.setFont("Helvetica", 6)
c.setFillColor(muted_rgb)
c.setStrokeColor(border_rgb)
c.setLineWidth(0.5)
c.line(MARGIN_LEFT, MARGIN_BOTTOM - 10, MARGIN_LEFT + CONTENT_W, MARGIN_BOTTOM - 10)
c.drawCentredString(PAGE_W / 2, MARGIN_BOTTOM - 22, "Uriel Jareth Consulting")
c.drawCentredString(PAGE_W / 2, MARGIN_BOTTOM - 32, "urieljareth.com | [email protected] | (445) 182 9943")
c.save()
return buf.getvalue()