"""Genera Avance-Balam-2026-09-16.xlsx a partir del corte del 15-sep. Corte del miércoles 16-sep: sin sesiones ni desarrollo nuevos desde el 15-sep; solo se publicó el Excel de avance en Google Sheets (Drive/Balam) y se regeneró este corte. El corte del 15-sep no se toca. Además, a petición de Johann (16-sep), las columnas Inicio/Fin dejan de mostrar las fechas PLAN y muestran las fechas REALES: primer y último día trabajado de cada tarea según `Seguimiento-horas.csv`, la bitácora y los commits ("—" = sin iniciar; las tareas en curso llevan como Fin el último día trabajado). Las fechas plan quedan en Avance-Balam-2026-08-06b.xlsx. Las horas se leen de `Seguimiento-horas.csv`; el script aborta si el reparto no cuadra con las horas nuevas del CSV. """ 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-09-15.xlsx") DST = os.path.join(BASE, "Avance-Balam-2026-09-16.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" C_PCT, C_TRAB, C_STAT = 4, 6, 7 CORTE_ANTERIOR = 94.08 # corte del 15-sep # --- CSV: totales por etapa y por actividad ----------------------------------- por_act, por_etapa = defaultdict(float), defaultdict(float) nuevas, desde_6ago = 0.0, 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-09-15": nuevas += h if r["fecha"] > "2026-08-06": desde_6ago += h E0, E1, E3 = por_etapa["0"], por_etapa["1"], por_etapa["3"] AJ = por_act["AJ-01"] E2 = por_etapa["2"] - AJ TOTAL = E0 + E1 + E2 + AJ + E3 assert abs(TOTAL - CORTE_ANTERIOR - nuevas) < 0.01, "El CSV no arranca del corte del 15-sep" PCT_GLOBAL = round(TOTAL / ((119 + 146) / 2), 2) # E1-05 +0.50 (Excel en Google Sheets + corte del 16-sep) → gestión y cortes. DELTAS_TRAB = {"Demostración semanal": 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), } # --- Fechas reales por fila (dd/mm/yyyy o "—") --------------------------------- # Trazabilidad: CSV (fecha por actividad), REGISTRO.md y commits de ambos repos. SIN_INICIAR = ("—", "—") FECHAS_REALES = { "Integracion de plataformas Balam": ("01/07/2026", "16/09/2026"), # en curso "Etapa 0": ("01/07/2026", "24/07/2026"), # validada por Balam el 24-jul "Sesión de arranque (kickoff)": ("01/07/2026", "01/07/2026"), "Sesión de Discovery": ("06/07/2026", "07/07/2026"), # facturación 6-jul, cobranza 7-jul "Entrega y validación de accesos": ("06/07/2026", "06/07/2026"), "Validación técnica de la API de BIND": ("06/07/2026", "06/07/2026"), # 117 GETs, adelantada un día "Prototipo visual navegable": ("08/07/2026", "10/07/2026"), "Documento de hallazgos": ("10/07/2026", "10/07/2026"), # PDF + correo del 10-jul "Sesión de validación del prototipo": ("23/07/2026", "24/07/2026"), # guion 23-jul, sesión 24-jul "Etapa 1": ("13/07/2026", "03/09/2026"), # técnica cerrada 1-ago; publicación provisional 3-sep "Demostración semanal": ("15/07/2026", "16/09/2026"), # gestión y cortes, en curso "Repositorio privado GitHub": ("13/07/2026", "16/07/2026"), # push del scaffold 16-jul "Backend base": ("13/07/2026", "19/07/2026"), # B0–B2 el 19-jul "Arquitectura multi-tenant": ("14/07/2026", "01/08/2026"), # RLS 21-jul, verificación balam_app 1-ago "Cliente de la API de BIND": ("15/07/2026", "15/07/2026"), "Capa de escritura controlada": ("19/07/2026", "23/08/2026"), # diseño 19-jul; cotización 11-ago; prefactura 23-ago "Sincronización de clientes": ("19/07/2026", "19/07/2026"), # B3 "Configuración de infraestructura en Azure": ("03/09/2026", "03/09/2026"), # publicación provisional en paputec.mx "Sincronización de facturas": ("21/07/2026", "01/08/2026"), # B4 21-jul; scheduler B5 1-ago "Sesión de reglas y dudas": ("21/07/2026", "22/07/2026"), # agenda 21-jul, sesión 22-jul "Definición de alcance Jira": ("22/07/2026", "07/08/2026"), # v1.3 aprobada 30-jul; definiciones cerradas 7-ago "Modelo de facturas": ("21/07/2026", "01/08/2026"), # mapper 21-jul; seeder demo 1-ago "CI/CD": ("03/09/2026", "03/09/2026"), # deploy.sh + secretos fuera del repo "Etapa 2": ("01/08/2026", "10/09/2026"), # en curso "» Ajuste dentro de la Etapa 2": ("27/07/2026", "03/09/2026"), # sesión con Pedro 27-jul → tickets cotizados 3-sep "Cliente de la API de Jira": ("29/07/2026", "06/08/2026"), "Disparador por estatus": ("20/08/2026", "07/09/2026"), # sondeo FAC-106 → folio 1026 (21-ago) → lote 7-sep "Trazabilidad ticket": ("10/08/2026", "03/09/2026"), # definiciones → pruebas observadas "Andamiaje del frontend": ("16/07/2026", "01/08/2026"), # scaffold 16-jul; login JWT 1-ago "Layout base": ("01/08/2026", "06/08/2026"), "Listado de facturas": ("01/08/2026", "01/08/2026"), "Detalle de factura": ("01/08/2026", "01/08/2026"), # PDF; XML pendiente "Catálogo de clientes": ("01/08/2026", "01/08/2026"), "Tipo de cambio del DOF": SIN_INICIAR, "Creación de cotizaciones desde la plataforma": ("06/08/2026", "07/09/2026"), # wizard UI 6-ago → lote 7-sep "Conversión cotización→factura": ("06/08/2026", "10/09/2026"), # simulación 6-ago → captura del endpoint 10-sep "Exportación de facturas": ("01/08/2026", "01/08/2026"), # CSV "Pruebas integrales": SIN_INICIAR, "Etapa 3": ("03/09/2026", "03/09/2026"), # solo endurecimiento y runbook "Módulo de cobranza": SIN_INICIAR, "Alertas internas": SIN_INICIAR, "Tablero directivo": SIN_INICIAR, "Reporte operativo": SIN_INICIAR, "Endurecimiento de seguridad": ("03/09/2026", "03/09/2026"), "Documentación técnica": ("03/09/2026", "03/09/2026"), "Sesión de capacitación": SIN_INICIAR, "Soporte posterior": SIN_INICIAR, } 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 16-sep, publicado en " "Google Sheets", 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 16-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 16-sep reconstruidas, pendientes de validar") ws["A2"].font = Font(size=10, italic=True, color=GRIS) ws.cell(row=4, column=2, value="Inicio (real)") ws.cell(row=4, column=3, value="Fin (real)") # Notas del corte anterior: se limpian antes del bucle y se reescriben abajo. 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() fechas_aplicadas = set() for row in range(5, tope): nombre = str(ws.cell(row=row, column=1).value or "").strip() if not nombre: continue for prefijo, (ini, fin) in FECHAS_REALES.items(): if nombre.startswith(prefijo): for col, val in ((2, ini), (3, fin)): cel = ws.cell(row=row, column=col, value=val) cel.alignment = Alignment(horizontal="center", vertical="center") if val == "—": cel.font = Font(size=9, color=GRIS) fechas_aplicadas.add(prefijo) break for prefijo, (trab, pct) in ENCABEZADOS.items(): if nombre.startswith(prefijo): 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%" pinta(p, V_FILL if pct >= 1 else A_FILL, V_FONT if pct >= 1 else 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): p = ws.cell(row=row, column=C_PCT, value=pct) p.number_format = "0%" pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center") 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}" faltan_fechas = set(FECHAS_REALES) - fechas_aplicadas assert not faltan_fechas, f"Filas sin fecha real aplicada: {faltan_fechas}" notas = [ ("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · " "(adelantada) = ejecutada antes de su fecha plan", True), (f"Horas trabajadas al 16-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 {desde_6ago:.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), ("Fechas: Inicio y Fin son REALES (primer y último día trabajado según el registro de horas, la " "bitácora y los commits); las tareas en curso llevan como Fin el último día trabajado y '—' " "significa sin iniciar. El calendario plan original está en el corte del 6-ago.", 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 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%}")