Files
balam/planeacion/generar_avance_2026_09_16.py
T

248 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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%}")