mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 18:23:22 +00:00
275 lines
11 KiB
Python
Executable file
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()
|