"""Genera Avance-Balam-2026-09-15.xlsx a partir del corte del 6-ago (2.º). Corte del martes 15-sep: cubre del 10-ago al 15-sep. Lo que pasó en el periodo: capa de escritura real (primera cotización en BIND desde un ticket el 21-ago, folio 1026; cotización en lote el 7-sep), el camino cotización→ prefactura construido y BLOQUEADO por BIND (26-ago: IsFiscalInvoice=true timbra al crear, false produce remisión, el tipo Prefactura solo existe en su UI), la plataforma publicada en https://paputec.mx/balam/ el 3-sep como ambiente provisional mientras llega Azure, y seis sesiones "Seguimiento PAF" con Balam (21, 24 y 26-ago; 3, 9 y 10-sep). El corte del 6-ago no se toca. Las horas se leen de `Seguimiento-horas.csv` (fuente de la verdad); el script aborta si el reparto por fila no cuadra con las horas nuevas del CSV. Las 39.25 h nuevas están marcadas `reconstruida` (evidencia: commits, correos e informes de reunión) y pendientes de validación por Johann. """ import csv import os from collections import defaultdict import openpyxl from openpyxl.styles import Alignment, Font, PatternFill BASE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(BASE, "Avance-Balam-2026-08-06b.xlsx") DST = os.path.join(BASE, "Avance-Balam-2026-09-15.xlsx") CSV_HORAS = os.path.join(BASE, "Seguimiento-horas.csv") CAFE, GRIS = "331F0E", "595959" V_FILL, V_FONT = "C6EFCE", "006100" A_FILL, A_FONT = "FFEB9C", "9C6500" R_FILL, R_FONT = "FFC7CE", "9C0006" G_FILL, G_FONT = "F2F2F2", "595959" C_PCT, C_EST, C_TRAB, C_STAT = 4, 5, 6, 7 CORTE_ANTERIOR = 54.83 # 2.º corte del 6-ago, ya reportado # --- CSV: totales por etapa y por actividad ----------------------------------- por_act, por_etapa = defaultdict(float), defaultdict(float) nuevas = 0.0 with open(CSV_HORAS, encoding="utf-8", newline="") as fh: for r in csv.DictReader(fh): if not (r.get("fecha") or "").strip(): continue h = float(r["horas"]) por_act[r["actividad_id"]] += h por_etapa[r["etapa"]] += h if r["fecha"] > "2026-08-06": nuevas += h E0, E1, E3 = por_etapa["0"], por_etapa["1"], por_etapa["3"] AJ = por_act["AJ-01"] # ajuste Jira→BIND (vive dentro de la etapa 2 del CSV) E2 = por_etapa["2"] - AJ # Etapa 2 propiamente (frontend + emisión) TOTAL = E0 + E1 + E2 + AJ + E3 assert abs(TOTAL - sum(por_etapa.values())) < 0.01 assert abs(TOTAL - CORTE_ANTERIOR - nuevas) < 0.01, "El CSV no arranca del corte del 6-ago" PCT_GLOBAL = round(TOTAL / ((119 + 146) / 2), 2) # --- Reparto por fila de las horas nuevas (10-ago → 15-sep) -------------------- # Trazabilidad CSV → fila del Excel: # E1-05 +4.50 (bitácora, cruce del buzón, WhatsApp con Pedro, correo Azure, # corte de horas) → "Demostración semanal + reporte de horas" (gestión) # E1-09 +2.50 (publicación en paputec.mx) → 2.25 a "Configuración de # infraestructura en Azure" (ambiente provisional) y 0.25 a "CI/CD" # (deploy.sh reproducible + secretos fuera del repo) # AJ-01 +9.25 → 4.50 a "Disparador…" (sondeo FAC-106 + cableado 20-ago 1.50, # investigación hasta el folio 1026 el 21-ago 3.00) y 4.75 a # "Trazabilidad… y prueba observada" (definiciones 10-ago 1.50, sesión # 12-ago 1.00, Excel de campos 0.75, sesiones 21-ago 0.75 y 3-sep 0.75) # E2-02 +8.00 (capa de escritura de cotización 11/12-ago 4.50, ítem por # código 3-sep 1.50, lote 7-sep 2.00) → "Creación de cotizaciones" # E2-03 +13.50 (prefactura 23-ago, prefactura→factura 25-ago, incidente e # investigación 26-ago, sesiones 24 y 26-ago, 9 y 10-sep) → # "Conversión cotización→factura" # E3-01 +1.00 → "Endurecimiento de seguridad"; E3-02 +0.50 → "Documentación técnica" DELTAS_TRAB = { "Demostración semanal": 4.50, "Configuración de infraestructura en Azure": 2.25, "CI/CD": 0.25, "Disparador por estatus": 4.50, "Trazabilidad ticket": 4.75, "Creación de cotizaciones desde la plataforma": 8.00, "Conversión cotización→factura": 13.50, "Endurecimiento de seguridad": 1.00, "Documentación técnica": 0.50, } assert abs(sum(DELTAS_TRAB.values()) - nuevas) < 0.01, ( f"El reparto ({sum(DELTAS_TRAB.values()):.2f}) no suma las {nuevas:.2f} h nuevas del CSV") ENCABEZADOS = { "Integracion de plataformas Balam": (TOTAL, PCT_GLOBAL), "Etapa 0": (E0, 1.0), "Etapa 1": (E1, 0.95), "Etapa 2": (E2, 0.70), "» Ajuste dentro de la Etapa 2": (AJ, 0.85), "Etapa 3": (E3, 0.05), } # (pct, estatus, fill, font, bold) — solo lo que cambió respecto al 6-ago. FILAS = { "Demostración semanal": (0.6, "🟡 En curso · el seguimiento pasó a Pedro (sesiones 'Seguimiento PAF' " "21, 24 y 26-ago; 3, 9 y 10-sep) · corte de horas al 15-sep", A_FILL, A_FONT, False), "Capa de escritura controlada": (0.9, "✅ Implementada: dry-run + confirmación humana + auditoría en " "WriteOperations para cotización (21-ago) y prefactura (23-ago) · " "el timbrado no existe en el API de BIND (es humano, en su pantalla)", V_FILL, V_FONT, False), "Configuración de infraestructura en Azure": (0.5, "🟡 Publicada en https://paputec.mx/balam/ (3-sep) como " "ambiente provisional · Azure sigue con Balam (requisitos " "enviados a Noé el 25-ago; datos del tenant revisados el 3-sep)", A_FILL, A_FONT, False), "CI/CD": (0.3, "🟡 Publicación reproducible (deploy.sh) con secretos fuera del repo · CI real depende de Azure", A_FILL, A_FONT, False), "Disparador por estatus": (0.6, "🟡 Cotización real desde el ticket (21-ago, folio 1026) y en lote (7-sep), " "con disparo manual · el disparo automático por estatus espera el ambiente definitivo", A_FILL, A_FONT, False), "Trazabilidad ticket": (0.6, "🟡 Vínculo ticket→cotización→prefactura en la BD · pruebas observadas con " "Balam el 12, 21, 24 y 26-ago y el 3-sep", A_FILL, A_FONT, False), "Creación de cotizaciones desde la plataforma": (0.9, "✅ Cotización individual y en lote con dry-run y " "confirmación humana · ítem del catálogo BIND por código y " "concepto = título del ticket (definición del 3-sep)", V_FILL, V_FONT, False), "Conversión cotización→factura": (0.6, "🔴 Prefactura por API bloqueada por BIND: IsFiscalInvoice=true " "TIMBRA al crear (incidente 26-ago, CFDI 2386) y false produce remisión; " "el tipo Prefactura solo existe en su UI · en curso la captura del " "endpoint interno (sesiones 9 y 10-sep)", R_FILL, R_FONT, True), "Endurecimiento de seguridad": (0.4, "🟡 Aplicado en la publicación: contenedor de solo lectura, rate limit, " "bloqueo de login, CSP/HSTS, respaldos diarios", A_FILL, A_FONT, False), "Documentación técnica": (0.3, "🟡 Runbook de publicación, operación y rollback (deploy/paputec/README.md)", A_FILL, A_FONT, False), } def pinta(celda, fill, font_color, bold=False, size=9, align="left"): celda.fill = PatternFill("solid", fgColor=fill) celda.font = Font(color=font_color, bold=bold, size=size) celda.alignment = Alignment(horizontal=align, vertical="center", wrap_text=True) wb = openpyxl.load_workbook(SRC) ws = wb.active ws["A2"] = ("Plan y avance al 15-sep-2026 · plataforma publicada en paputec.mx (provisional), cotización real " "desde el ticket (individual y en lote); prefactura por API bloqueada por BIND · horas del " "10-ago al 15-sep reconstruidas, pendientes de validar") ws["A2"].font = Font(size=10, italic=True, color=GRIS) # Las notas del corte anterior van fusionadas A:G y algunas empiezan con # "Etapa 1: …", que colisiona con los prefijos de encabezado: se localizan y # limpian ANTES del bucle principal, y este se detiene donde empezaban. primera_nota = None for row in range(5, ws.max_row + 1): v = str(ws.cell(row=row, column=1).value or "") if v.startswith("✅ Completada ·"): primera_nota = row if primera_nota and row >= primera_nota: for rango in list(ws.merged_cells.ranges): if rango.min_row == row: ws.unmerge_cells(str(rango)) ws.cell(row=row, column=1).value = None tope = primera_nota if primera_nota else ws.max_row + 1 aplicado = set() for row in range(5, tope): nombre = str(ws.cell(row=row, column=1).value or "").strip() if not nombre: continue for prefijo, (trab, pct) in ENCABEZADOS.items(): if nombre.startswith(prefijo): if trab is not None: c = ws.cell(row=row, column=C_TRAB, value=round(trab, 2)) c.number_format = "0.00" p = ws.cell(row=row, column=C_PCT, value=pct) p.number_format = "0%" if pct >= 1: pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center") elif pct > 0: pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center") break for prefijo, delta in DELTAS_TRAB.items(): if nombre.startswith(prefijo): actual = ws.cell(row=row, column=C_TRAB).value actual = float(actual) if isinstance(actual, (int, float)) else 0.0 c = ws.cell(row=row, column=C_TRAB, value=round(actual + delta, 2)) c.number_format = "0.00" aplicado.add(prefijo) break for prefijo, (pct, txt, fill, fcolor, bold) in FILAS.items(): if nombre.startswith(prefijo): if pct is not None: p = ws.cell(row=row, column=C_PCT, value=pct) p.number_format = "0%" if pct >= 1: pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center") elif pct > 0: pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center") else: p.font = Font(size=10, color=GRIS) s = ws.cell(row=row, column=C_STAT, value=txt) pinta(s, fill, fcolor, bold) break faltan = set(DELTAS_TRAB) - aplicado assert not faltan, f"Filas no encontradas en el Excel: {faltan}" # --- Notas: se escribe el bloque del 15-sep donde estaba el del 6-ago --------- notas = [ ("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · " "(adelantada) = ejecutada antes de su fecha plan", True), (f"Horas trabajadas al 15-sep: Etapa 0 {E0:.2f} h (cerrada y validada) · Etapa 1 {E1:.2f} h " f"(rango 32–39) · Etapa 2 {E2:.2f} h · ajuste Jira {AJ:.2f} h (de 7–10 aprobadas) · " f"Etapa 3 {E3:.2f} h · total {TOTAL:.2f} h. Las {nuevas:.2f} h posteriores al corte del 6-ago " "están reconstruidas con evidencia (commits, correos, informes de reunión) y pendientes de " "validar. Base de facturación a $600/h + IVA.", True), ("Etapa 1: la capa de escritura quedó implementada (cotización y prefactura con dry-run, " "confirmación humana y auditoría). Solo Azure y CI/CD siguen dependiendo de Balam; mientras " "tanto la plataforma está publicada en https://paputec.mx/balam/ desde el 3-sep (BIND y Jira " "en solo lectura fuera de sesión).", False), ("Integración Jira → BIND: primera cotización real desde un ticket el 21-ago (folio 1026, tras " "5 intentos por dos semánticas indocumentadas del API: Type=2 y VAT como tasa), tres tickets " "cotizados en la sesión del 3-sep y cotización en lote desde el 7-sep. Rebasa las 7–10 h " "aprobadas por las semánticas indocumentadas y las sesiones acompañadas (12, 21-ago, 3-sep).", False), ("Etapa 2: la prefactura por API NO es posible con lo que BIND expone (26-ago: IsFiscalInvoice=true " "timbra al crear —CFDI 2386, a cancelar— y false produce remisión; el tipo Prefactura solo " "existe en su UI). Sesiones del 9 y 10-sep con Araceli y Pedro para capturar el endpoint " "interno y decidir el camino (réplica, detección al timbrado o soporte de BIND).", False), ("En espera de Balam: acceso a Azure (requisitos enviados el 25-ago), cancelación del CFDI 2386 " "y de la cotización 1026, y la definición del camino de prefactura.", False), ] r = primera_nota if primera_nota else ws.max_row + 2 for txt, bold in notas: c = ws.cell(row=r, column=1, value=txt) c.font = Font(italic=True, size=9, color=CAFE, bold=bold) c.alignment = Alignment(wrap_text=True, vertical="top") try: ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) except ValueError: pass # ya estaba fusionada desde el archivo origen ws.row_dimensions[r].height = 40 if len(txt) > 220 else (28 if len(txt) > 120 else 14) r += 1 ws.print_area = f"A1:G{r}" wb.save(DST) print("OK ->", DST) print(f" E0 {E0:.2f} · E1 {E1:.2f} · E2 {E2:.2f} · Jira {AJ:.2f} · E3 {E3:.2f} · TOTAL {TOTAL:.2f} h " f"({nuevas:.2f} nuevas) · avance global {PCT_GLOBAL:.0%}")