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 = ("Nota de conciliación. 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 = ("Solicitud. 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()