68aedc7863
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
248 lines
14 KiB
Python
248 lines
14 KiB
Python
"""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%}")
|