- 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]>
553 lines
22 KiB
Python
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()
|