panels-origin/tmp/pdfs/build_estado_cuenta.py
2026-08-25 20:21:49 -05:00

275 lines
11 KiB
Python
Executable file

from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfgen import canvas
from reportlab.platypus import Paragraph
from pypdf import PdfReader
WORK = Path(__file__).resolve().parent
OUTPUT = WORK / "estado_cuenta_comtecsa_2026-08-22.pdf"
PAGE_W, PAGE_H = letter
MARGIN = 42
CONTENT_W = PAGE_W - (2 * MARGIN)
INK = colors.HexColor("#172033")
MUTED = colors.HexColor("#64748B")
LINE = colors.HexColor("#D8E0EA")
NAVY = colors.HexColor("#163A5F")
BLUE = colors.HexColor("#2378A5")
PALE = colors.HexColor("#EEF5F9")
GREEN = colors.HexColor("#16805C")
GREEN_BG = colors.HexColor("#E8F7F1")
AMBER = colors.HexColor("#9A5C00")
AMBER_BG = colors.HexColor("#FFF3D6")
WHITE = colors.white
def money(value):
return "$" + f"{value:,.2f}"
def right_text(pdf, text, x, y, font="Helvetica", size=9, color=INK):
pdf.setFont(font, size)
pdf.setFillColor(color)
pdf.drawRightString(x, y, text)
def status_pill(pdf, text, x, y, width, fill, text_color):
pdf.setFillColor(fill)
pdf.roundRect(x, y, width, 16, 8, fill=1, stroke=0)
pdf.setFillColor(text_color)
pdf.setFont("Helvetica-Bold", 7.3)
pdf.drawCentredString(x + width / 2, y + 5, text)
def wrapped_paragraph(pdf, text, x, y, width, height, font_size=8.5, leading=11, color=INK):
style = ParagraphStyle(
name="body",
fontName="Helvetica",
fontSize=font_size,
leading=leading,
textColor=color,
alignment=TA_LEFT,
)
para = Paragraph(text, style)
_, used_h = para.wrap(width, height)
para.drawOn(pdf, x, y + height - used_h)
def draw_summary_box(pdf, x, y, w, h, label, amount, detail, dark=False):
if dark:
fill = NAVY
border = NAVY
label_color = colors.HexColor("#CFE4F2")
amount_color = WHITE
detail_color = colors.HexColor("#CFE4F2")
else:
fill = WHITE
border = LINE
label_color = MUTED
amount_color = INK
detail_color = MUTED
pdf.setFillColor(fill)
pdf.setStrokeColor(border)
pdf.setLineWidth(0.8)
pdf.roundRect(x, y, w, h, 8, fill=1, stroke=1)
pdf.setFillColor(label_color)
pdf.setFont("Helvetica-Bold", 7.2)
pdf.drawString(x + 11, y + h - 16, label.upper())
pdf.setFillColor(amount_color)
pdf.setFont("Helvetica-Bold", 16)
pdf.drawString(x + 11, y + h - 36, amount)
pdf.setFillColor(detail_color)
pdf.setFont("Helvetica", 7.2)
pdf.drawString(x + 11, y + 9, detail)
def build_pdf():
pdf = canvas.Canvas(str(OUTPUT), pagesize=letter)
pdf.setTitle("Estado de cuenta COMTECSA - 22 agosto 2026")
pdf.setAuthor("MRDEV")
pdf.setSubject("Conciliación de facturas pendientes y servicio por facturar")
pdf.setFillColor(NAVY)
pdf.rect(0, PAGE_H - 8, PAGE_W * 0.56, 8, fill=1, stroke=0)
pdf.setFillColor(BLUE)
pdf.rect(PAGE_W * 0.56, PAGE_H - 8, PAGE_W * 0.30, 8, fill=1, stroke=0)
pdf.setFillColor(colors.HexColor("#70B68D"))
pdf.rect(PAGE_W * 0.86, PAGE_H - 8, PAGE_W * 0.14, 8, fill=1, stroke=0)
pdf.setFillColor(NAVY)
pdf.roundRect(MARGIN, 704, 52, 52, 10, fill=1, stroke=0)
pdf.setFillColor(WHITE)
pdf.setFont("Helvetica-Bold", 15)
pdf.drawCentredString(MARGIN + 26, 724, "MR")
pdf.setFillColor(INK)
pdf.setFont("Helvetica-Bold", 15)
pdf.drawString(106, 738, "MRDEV")
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica", 8.5)
pdf.drawString(106, 717, "Proveedor de servicios")
right_text(pdf, "ESTADO DE CUENTA", PAGE_W - MARGIN, 736, "Helvetica-Bold", 21, NAVY)
right_text(pdf, "Corte al 22 de agosto de 2026", PAGE_W - MARGIN, 717, "Helvetica", 8.5, MUTED)
right_text(pdf, "Referencia: EDC-COMTECSA-20260822", PAGE_W - MARGIN, 704, "Helvetica", 7.4, MUTED)
pdf.setStrokeColor(NAVY)
pdf.setLineWidth(1.4)
pdf.line(MARGIN, 686, PAGE_W - MARGIN, 686)
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica-Bold", 7)
pdf.drawString(MARGIN, 667, "CLIENTE")
pdf.drawString(398, 667, "MONEDA")
pdf.setFillColor(INK)
pdf.setFont("Helvetica-Bold", 11.2)
pdf.drawString(MARGIN, 650, "COMERCIALIZADORA DE TECNOLOGÍAS (COMTECSA)")
pdf.setFont("Helvetica-Bold", 10.5)
pdf.drawString(398, 650, "PESOS MEXICANOS")
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica", 7.6)
pdf.drawString(MARGIN, 637, "Atención: Contabilidad / Cuentas por pagar")
pdf.drawString(398, 637, "Importes en MXN")
gap = 9
box_w = (CONTENT_W - 2 * gap) / 3
card_y = 560
draw_summary_box(pdf, MARGIN, card_y, box_w, 62, "Facturado pendiente", money(23200), "Facturas A 113 y A 114")
draw_summary_box(pdf, MARGIN + box_w + gap, card_y, box_w, 62, "Por facturar", money(11600), "Servicio del 1 al 15 de agosto")
draw_summary_box(pdf, MARGIN + 2 * (box_w + gap), card_y, box_w, 62, "Saldo total abierto", money(34800), "Tres periodos pendientes", dark=True)
pdf.setFillColor(NAVY)
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(MARGIN, 540, "Detalle de movimientos pendientes")
table_top = 526
columns = [("DOCUMENTO", 64), ("PERIODO DEL SERVICIO", 132), ("SITUACIÓN", 116), ("CARGO", 72), ("ABONOS", 68), ("SALDO", 76)]
xs = [MARGIN]
for _, width in columns:
xs.append(xs[-1] + width)
header_h = 28
row_h = 44
total_h = 32
pdf.setFillColor(PALE)
pdf.rect(MARGIN, table_top - header_h, CONTENT_W, header_h, fill=1, stroke=0)
pdf.setStrokeColor(LINE)
pdf.setLineWidth(0.6)
pdf.line(MARGIN, table_top, PAGE_W - MARGIN, table_top)
pdf.line(MARGIN, table_top - header_h, PAGE_W - MARGIN, table_top - header_h)
for idx, (label, _) in enumerate(columns):
pdf.setFillColor(NAVY)
pdf.setFont("Helvetica-Bold", 6.6)
if idx >= 3:
pdf.drawRightString(xs[idx + 1] - 7, table_top - 18, label)
else:
pdf.drawString(xs[idx] + 7, table_top - 18, label)
rows = [
("A 113", "01-15 jul 2026", "Servicio facturado", "Pendiente de pago", 11600, 0, 11600, "pending"),
("A 114", "16-30 jul 2026", "Servicio facturado", "Pendiente de pago", 11600, 0, 11600, "pending"),
("Por emitir", "01-15 ago 2026", "Servicio por facturar", "Por confirmar", 11600, 0, 11600, "unbilled"),
]
y = table_top - header_h
for row in rows:
y_next = y - row_h
pdf.setStrokeColor(LINE)
pdf.line(MARGIN, y_next, PAGE_W - MARGIN, y_next)
pdf.setFillColor(NAVY)
pdf.setFont("Helvetica-Bold", 8.5)
pdf.drawString(xs[0] + 7, y_next + 26, row[0])
pdf.setFillColor(INK)
pdf.setFont("Helvetica-Bold", 8.2)
pdf.drawString(xs[1] + 7, y_next + 27, row[1])
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica", 7)
pdf.drawString(xs[1] + 7, y_next + 14, row[2])
if row[7] == "pending":
status_pill(pdf, row[3], xs[2] + 7, y_next + 14, 91, AMBER_BG, AMBER)
else:
status_pill(pdf, row[3], xs[2] + 7, y_next + 14, 76, PALE, NAVY)
right_text(pdf, money(row[4]), xs[4] - 7, y_next + 21, "Helvetica", 8.2, INK)
right_text(pdf, money(row[5]), xs[5] - 7, y_next + 21, "Helvetica", 8.2, INK)
right_text(pdf, money(row[6]), xs[6] - 7, y_next + 21, "Helvetica-Bold", 8.2, INK)
y = y_next
pdf.setFillColor(colors.HexColor("#FBFCFD"))
pdf.rect(MARGIN, y - total_h, CONTENT_W, total_h, fill=1, stroke=0)
pdf.setStrokeColor(NAVY)
pdf.setLineWidth(1.2)
pdf.line(MARGIN, y - total_h, PAGE_W - MARGIN, y - total_h)
pdf.setFillColor(INK)
pdf.setFont("Helvetica-Bold", 8.4)
pdf.drawString(MARGIN + 7, y - 20, "TOTALES")
right_text(pdf, money(34800), xs[4] - 7, y - 20, "Helvetica-Bold", 8.4, INK)
right_text(pdf, money(0), xs[5] - 7, y - 20, "Helvetica-Bold", 8.4, INK)
right_text(pdf, money(34800), xs[6] - 7, y - 20, "Helvetica-Bold", 8.4, INK)
pdf.setFillColor(NAVY)
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(MARGIN, 304, "Pago aplicado recientemente")
paid_y = 253
pdf.setFillColor(GREEN_BG)
pdf.setStrokeColor(colors.HexColor("#BFE3D5"))
pdf.setLineWidth(0.8)
pdf.roundRect(MARGIN, paid_y, CONTENT_W, 40, 7, fill=1, stroke=1)
status_pill(pdf, "PAGADA", MARGIN + 10, paid_y + 12, 51, GREEN_BG, GREEN)
pdf.setFillColor(colors.HexColor("#0C5E43"))
pdf.setFont("Helvetica-Bold", 8.5)
pdf.drawString(MARGIN + 73, paid_y + 23, "Factura A 109")
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica", 6.8)
pdf.drawString(MARGIN + 73, paid_y + 10, "Periodo 16-30 jun 2026")
for label, amount, x in [("CARGO", money(11600), 367), ("ABONO", money(11600), 457), ("SALDO", money(0), 554)]:
right_text(pdf, label, x, paid_y + 25, "Helvetica-Bold", 6.2, MUTED)
right_text(pdf, amount, x, paid_y + 11, "Helvetica-Bold", 7.8, colors.HexColor("#164E3C"))
note_y = 177
note_h = 58
pdf.setFillColor(PALE)
pdf.roundRect(MARGIN, note_y, CONTENT_W, note_h, 6, fill=1, stroke=0)
pdf.setFillColor(BLUE)
pdf.rect(MARGIN, note_y, 4, note_h, fill=1, stroke=0)
note_text = ("<b>Nota de conciliación.</b> La factura A 109 se reconoce como pagada y no forma parte del saldo abierto. "
"El importe del periodo del 1 al 15 de agosto de 2026 corresponde a un servicio pendiente de facturación; "
"aún no cuenta con número de factura ni CFDI emitido.")
wrapped_paragraph(pdf, note_text, MARGIN + 13, note_y + 7, CONTENT_W - 25, note_h - 14, 8.1, 10.2, colors.HexColor("#334155"))
req_y = 104
req_h = 56
pdf.setFillColor(WHITE)
pdf.setStrokeColor(LINE)
pdf.setLineWidth(0.8)
pdf.roundRect(MARGIN, req_y, CONTENT_W, req_h, 6, fill=1, stroke=1)
req_text = ("<b>Solicitud.</b> Agradecemos confirmar la fecha programada de pago de las facturas A 113 y A 114, "
"así como la procedencia de emitir la factura correspondiente al periodo del 1 al 15 de agosto de 2026.")
wrapped_paragraph(pdf, req_text, MARGIN + 12, req_y + 7, CONTENT_W - 24, req_h - 14, 8.2, 10.5, colors.HexColor("#334155"))
pdf.setStrokeColor(LINE)
pdf.setLineWidth(0.6)
pdf.line(MARGIN, 70, PAGE_W - MARGIN, 70)
pdf.setFillColor(MUTED)
pdf.setFont("Helvetica", 6.8)
pdf.drawString(MARGIN, 55, "Documento informativo para conciliación de saldos; no sustituye los CFDI correspondientes.")
right_text(pdf, "Página 1 de 1", PAGE_W - MARGIN, 55, "Helvetica", 6.8, MUTED)
pdf.showPage()
pdf.save()
reader = PdfReader(str(OUTPUT))
if len(reader.pages) != 1:
raise RuntimeError(f"Se esperaba 1 pagina, se obtuvieron {len(reader.pages)}")
extracted = reader.pages[0].extract_text() or ""
required = ["MRDEV", "COMERCIALIZADORA DE TECNOLOGÍAS", "A 113", "A 114", "Por emitir", "$34,800.00", "A 109"]
missing = [item for item in required if item not in extracted]
if missing:
raise RuntimeError(f"Faltan textos requeridos en el PDF: {missing}")
print(OUTPUT)
if __name__ == "__main__":
build_pdf()