Files
balam/planeacion/generar_plan_completo_2026_07_28.py
T
Johann 64f7146d86 Propuesta v1.3 (Jira→BIND integrado) y plan-avance con Etapas 2-3 por actividad diaria
Reemplaza el enfoque de adenda separada: la propuesta completa sube a v1.3
con la integración Jira→BIND incorporada en §1-2 (bloque de ajuste con
entregables, candados fiscales y exclusiones), §4 (fila Ajuste 9-12 h /
$5,400-$7,200; total 121-148 h / $72,600-$88,800), §7 (supuestos del token
PAF y del flujo) y anexos A/B. PDF reconstruido con fuentes embebidas
(antes dependía de fuentes de Windows) y fixes visuales: flechas en títulos
serif, tabla de inversión sin partir, label A.4 sin huérfano.

El Excel para Erika (Avance-Balam-2026-07-28.xlsx) extiende el formato del
27-jul con Etapas 2-3 y el ajuste Jira desglosados en actividades diarias
cuyos rangos suman exactamente los contractuales; se rebalancearon los
rangos de Etapa 1 (sumaban 34-42 vs 32-39 contractual) recortando las tres
actividades con horas reales muy por debajo del estimado, con asserts que
lo verifican. Verificado con 5 agentes (paridad MD↔HTML, cifras vs
bitácora/CSV, formato y calendario, cobertura de entregables, PDF visual).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:02:07 -06:00

336 lines
15 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-07-28.xlsx — plan y avance COMPLETO para Erika.
Pedido por Erika el 27-28 jul: el conteo de horas de cada etapa (base de la
facturación) en el mismo formato de los exceles anteriores, extendido a las
Etapas 2 y 3 desglosadas por actividades diarias.
Etapas 0-1 (hechas/en curso) se leen del Excel interno
(`Plan-actividades-avance-2026-07-27.xlsx`), que a su vez sale de
`Seguimiento-horas.csv` — una sola fuente de datos. Las Etapas 2-3 y el ajuste
Jira → BIND se planifican aquí con los rangos contractuales de la propuesta
v1.3 (§2 y §4): E2 38–46 h · E3 24–29 h · Jira 9–12 h · total 121–148 h.
"""
import os
import re
from datetime import date
import openpyxl
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
BASE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(BASE, "Plan-actividades-avance-2026-07-27.xlsx")
DST = os.path.join(BASE, "Avance-Balam-2026-07-28.xlsx")
# Marca Balam
CAFE, AMARILLO, CREMA = "331F0E", "F7BD0C", "FDF3D8"
V_FILL, V_FONT = "C6EFCE", "006100" # verde
A_FILL, A_FONT = "FFEB9C", "9C6500" # amarillo
R_FILL, R_FONT = "FFC7CE", "9C0006" # rojo
GRIS = "595959"
BORDE = "D9D9D9"
thin = Side(style="thin", color=BORDE)
box = Border(left=thin, right=thin, top=thin, bottom=thin)
# Columnas del origen (interno 27-jul)
O_NOMBRE, O_INI, O_FIN, O_PCT, O_EST, O_TRAB, O_STAT = 1, 3, 4, 7, 9, 10, 11
# Estatus del interno que quedaron amarrados a la "sesión del 28-jul" —
# la sesión se movió a revisión interna de Balam (REGISTRO #58).
REEMPLAZOS_STAT = {
"🔴 Diseño listo · impl. en pausa (def. Balam 28-jul)":
"🔴 Diseño listo · impl. en pausa (flujo en revisión interna de Balam)",
"🟡 Conectividad lista · falta regla 28-jul":
"🟡 Conectividad lista · falta regla (revisión interna de Balam)",
}
# Los rangos por actividad del interno sumaban 34–42 h en Etapa 1, fuera del
# rango contractual de 32–39 (tope de facturación $23,400). Se recortan las
# tres actividades cuyas horas reales quedaron muy por debajo del estimado,
# para que la descomposición sume exactamente 32–39.
REEMPLAZOS_EST = {
"Sincronización de clientes": "1.5–2", # antes 2.5–3 (real: 0.5)
"Sincronización de facturas y cotizaciones": "4–4.5", # antes 4.5–5.5 (real: 0.5)
"Modelo de facturas + migraciones + datos de prueba": "3–3.5" # antes 3.5–4.5 (real: 0.5)
}
def limpia_fecha(txt):
"""'vie 24/07/26' -> '24/07/2026'."""
m = re.search(r"(\d{2})/(\d{2})/(\d{2})", str(txt or ""))
return f"{m.group(1)}/{m.group(2)}/20{m.group(3)}" if m else ""
# --- Lee Etapas 0-1 del interno ----------------------------------------------
wsrc = openpyxl.load_workbook(SRC).active
hechas = [] # filas E0/E1 (encabezados de etapa + actividades)
trab_por_etapa = {}
for row in range(2, wsrc.max_row + 1):
nombre = str(wsrc.cell(row=row, column=O_NOMBRE).value or "").strip()
if not nombre or nombre.startswith(("✅", "▬", "Horas:", "Etapa 1:", "🔴", "Pendientes",
"API de Jira", "Núcleo")):
continue
if nombre.startswith("Integracion"):
trab_por_etapa["TOTAL"] = float(wsrc.cell(row=row, column=O_TRAB).value or 0)
continue
es_etapa = nombre.startswith("Etapa")
if es_etapa:
trab_por_etapa[nombre] = float(wsrc.cell(row=row, column=O_TRAB).value or 0)
stat = str(wsrc.cell(row=row, column=O_STAT).value or "").strip()
est = str(wsrc.cell(row=row, column=O_EST).value or "")
hechas.append({
"nombre": nombre,
"ini": limpia_fecha(wsrc.cell(row=row, column=O_INI).value),
"fin": limpia_fecha(wsrc.cell(row=row, column=O_FIN).value),
"pct": wsrc.cell(row=row, column=O_PCT).value or 0,
"est": REEMPLAZOS_EST.get(nombre, est),
"trab": wsrc.cell(row=row, column=O_TRAB).value,
"stat": REEMPLAZOS_STAT.get(stat, stat),
"etapa": es_etapa,
})
E0, E1 = trab_por_etapa["Etapa 0"], trab_por_etapa["Etapa 1"]
TOTAL = trab_por_etapa["TOTAL"]
assert abs((E0 + E1) - TOTAL) < 0.01, f"Horas no cuadran: {E0}+{E1} != {TOTAL}"
def suma_est(filas):
lo = hi = 0.0
for f in filas:
est = f["est"] if isinstance(f, dict) else f
if not est or est == "—":
continue
p = est.replace("–", "-").split("-")
lo += float(p[0])
hi += float(p[-1])
return lo, hi
# Las actividades de E0 y E1 deben sumar exactamente su rango contractual.
_secciones = {}
_actual = None
for f in hechas:
if f["etapa"]:
_actual = f["nombre"]
else:
_secciones.setdefault(_actual, []).append(f)
assert suma_est(_secciones["Etapa 0"]) == (18, 22), \
f"Etapa 0: actividades suman {suma_est(_secciones['Etapa 0'])} != (18, 22)"
assert suma_est(_secciones["Etapa 1"]) == (32, 39), \
f"Etapa 1: actividades suman {suma_est(_secciones['Etapa 1'])} != (32, 39)"
# --- Plan de Etapas 2-3 y ajuste Jira (actividades diarias) -------------------
PROX = "⚪ Próxima"
PROX_FLUJO = "⚪ Próxima · requiere flujo de facturación definido"
PROX_JIRA = "⚪ Próxima · sujeta a aprobación (v1.3) y flujo definido"
# (nombre, ini, fin, est_min, est_max, estatus)
ETAPA2 = ("Etapa 2 — Facturación (consulta y emisión), catálogo y multimoneda",
"29/07/2026", "12/08/2026", 38, 46, [
("Andamiaje del frontend (Angular): proyecto, login y guardias por rol",
"29/07/2026", "30/07/2026", "4–5", PROX),
("Layout base con la identidad de marca; gestión de estado y caché",
"30/07/2026", "31/07/2026", "3–4", PROX),
("Listado de facturas: filtros, búsqueda, paginación y ordenamiento",
"31/07/2026", "03/08/2026", "6–7", PROX),
("Detalle de factura con descarga de PDF/XML desde BIND",
"04/08/2026", "04/08/2026", "3–4", PROX),
("Catálogo de clientes con lista blanca configurable (editable por rol Administración)",
"05/08/2026", "05/08/2026", "3–4", PROX),
("Tipo de cambio del DOF: proceso programado y captura al facturar",
"06/08/2026", "06/08/2026", "2", PROX),
("Creación de cotizaciones desde la plataforma (con simulación dry-run)",
"06/08/2026", "07/08/2026", "5–6", PROX_FLUJO),
("Conversión cotización→factura con confirmación humana (MXN con IVA / USD sin IVA)",
"10/08/2026", "11/08/2026", "5–6", PROX_FLUJO),
("Exportación de facturas y clientes a CSV/XLSX",
"11/08/2026", "11/08/2026", "2", PROX),
("Pruebas integrales del flujo de emisión + manual de usuario",
"12/08/2026", "12/08/2026", "5–6", PROX),
])
ETAPA3 = ("Etapa 3 — Cobranza, tablero, reportes y cierre",
"13/08/2026", "21/08/2026", 24, 29, [
("Módulo de cobranza: antigüedad de cartera 30/60/90 y vistas por vencer/vencidas",
"13/08/2026", "14/08/2026", "5–6", PROX),
("Alertas internas configurables + aplicación de la lista blanca",
"14/08/2026", "17/08/2026", "4–5", PROX),
("Tablero directivo con indicadores de cuentas por cobrar",
"17/08/2026", "18/08/2026", "4–5", PROX),
("Reporte operativo configurable (frecuencia, destinatarios, CSV/XLSX/PDF)",
"19/08/2026", "19/08/2026", "3–4", PROX),
("Endurecimiento de seguridad, secretos, respaldos y plan de recuperación",
"20/08/2026", "20/08/2026", "3–4", PROX),
("Documentación técnica: README, runbook y ADRs",
"20/08/2026", "21/08/2026", "2", PROX),
("Sesión de capacitación grabada + transferencia y cierre",
"21/08/2026", "21/08/2026", "3", PROX),
("Soporte posterior al lanzamiento (2 semanas, corrección de defectos — sin costo)",
"24/08/2026", "04/09/2026", "—", "⚪ Incluida en el cierre · sin costo"),
])
JIRA = ("Integración Jira → BIND (ajuste propuesta v1.3, en aprobación)",
"04/08/2026", "07/08/2026", 9, 12, [
("Cliente de la API de Jira (token PAF): lectura de tickets FAC y control de límites",
"04/08/2026", "05/08/2026", "3–4", PROX_JIRA),
("Disparador por estatus → generación automática de la cotización en BIND",
"05/08/2026", "06/08/2026", "4–5", PROX_JIRA),
("Trazabilidad ticket↔cotización↔factura, alimentación del tablero y prueba observada",
"07/08/2026", "07/08/2026", "2–3", PROX_JIRA),
])
def suma_rangos(acts):
lo = hi = 0
for _, _, _, est, _ in acts:
if est == "—": # filas sin costo (soporte incluido) no suman
continue
p = est.split("–")
lo += float(p[0])
hi += float(p[-1])
return lo, hi
for titulo, _, _, lo, hi, acts in (ETAPA2, ETAPA3, JIRA):
slo, shi = suma_rangos(acts)
assert (slo, shi) == (lo, hi), f"{titulo}: actividades {slo}–{shi} != rango {lo}–{hi}"
TOT_MIN = 18 + 32 + ETAPA2[3] + ETAPA3[3] + JIRA[3] # 121
TOT_MAX = 22 + 39 + ETAPA2[4] + ETAPA3[4] + JIRA[4] # 148
PCT_GLOBAL = round(TOTAL / ((TOT_MIN + TOT_MAX) / 2), 2)
# --- Construcción -------------------------------------------------------------
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Plan y avance"
ws["A1"] = "Proyecto Balam · Plataforma de Automatización Financiera"
ws["A1"].font = Font(bold=True, size=14, color=CAFE)
ws["A2"] = ("Plan y avance al 28-jul-2026 · horas trabajadas al corte del lunes 27-jul · "
"Etapas 2-3 y ajuste Jira desglosados por actividad diaria")
ws["A2"].font = Font(size=10, italic=True, color=GRIS)
ENC = ["Actividad", "Inicio", "Fin", "Avance", "Horas\nestimadas", "Horas\ntrabajadas", "Estatus"]
ANCHOS = [56, 12, 12, 9, 11, 11, 40]
FILA_ENC = 4
for i, (txt, ancho) in enumerate(zip(ENC, ANCHOS), start=1):
c = ws.cell(row=FILA_ENC, column=i, value=txt)
c.font = Font(bold=True, size=10, color=CAFE)
c.fill = PatternFill("solid", fgColor=AMARILLO)
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
c.border = box
ws.column_dimensions[get_column_letter(i)].width = ancho
ws.row_dimensions[FILA_ENC].height = 26
def pinta_fila(r, nombre, ini, fin, pct, est, trab, stat, etapa=False):
c = ws.cell(row=r, column=1, value=nombre)
c.font = Font(bold=etapa, size=10, color=CAFE)
c.alignment = Alignment(vertical="center", wrap_text=True, indent=0 if etapa else 1)
for col, val in ((2, ini), (3, fin)):
cc = ws.cell(row=r, column=col, value=val)
cc.font = Font(size=9, color=GRIS)
cc.alignment = Alignment(horizontal="center", vertical="center")
cp = ws.cell(row=r, column=4, value=pct)
cp.number_format = "0%"
cp.alignment = Alignment(horizontal="center", vertical="center")
if pct >= 1:
cp.font = Font(bold=True, size=10, color=V_FONT)
cp.fill = PatternFill("solid", fgColor=V_FILL)
elif pct > 0:
cp.font = Font(bold=True, size=10, color=A_FONT)
cp.fill = PatternFill("solid", fgColor=A_FILL)
else:
cp.font = Font(size=10, color=GRIS)
ce = ws.cell(row=r, column=5, value=est)
ce.font = Font(bold=etapa, size=10, color=CAFE)
ce.alignment = Alignment(horizontal="center", vertical="center")
ct = ws.cell(row=r, column=6, value=trab if trab not in (None, "") else "—")
if isinstance(trab, (int, float)):
ct.number_format = "0.00"
ct.font = Font(bold=etapa, size=10, color=CAFE)
ct.alignment = Alignment(horizontal="center", vertical="center")
cs = ws.cell(row=r, column=7, value=stat)
cs.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
cs.font = Font(size=9, color=GRIS)
if stat.startswith("✅"):
cs.font = Font(size=9, color=V_FONT); cs.fill = PatternFill("solid", fgColor=V_FILL)
elif stat.startswith("🟡"):
cs.font = Font(size=9, color=A_FONT); cs.fill = PatternFill("solid", fgColor=A_FILL)
elif stat.startswith("🔴"):
cs.font = Font(size=9, bold=True, color=R_FONT); cs.fill = PatternFill("solid", fgColor=R_FILL)
for col in range(1, 8):
ws.cell(row=r, column=col).border = box
if etapa and col in (1, 2, 3):
ws.cell(row=r, column=col).fill = PatternFill("solid", fgColor=CREMA)
r = FILA_ENC + 1
pinta_fila(r, "Integracion de plataformas Balam", "01/07/2026", "21/08/2026",
PCT_GLOBAL, f"{TOT_MIN:.0f}–{TOT_MAX:.0f}", TOTAL, "", etapa=True)
r += 1
for f in hechas:
pinta_fila(r, f["nombre"], f["ini"], f["fin"], f["pct"], f["est"], f["trab"],
f["stat"], etapa=f["etapa"])
r += 1
for titulo, ini, fin, lo, hi, acts in (ETAPA2, ETAPA3, JIRA):
pinta_fila(r, titulo, ini, fin, 0, f"{lo}–{hi}", None, "", etapa=True)
r += 1
for nombre, aini, afin, est, stat in acts:
pinta_fila(r, nombre, aini, afin, 0, est, None, stat)
r += 1
# --- Leyenda y notas ----------------------------------------------------------
r += 1
leyenda = "✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima"
notas = [
(leyenda, False),
(f"Horas trabajadas al corte del lunes 27-jul: {E0:.2f} h en Etapa 0 (cerrada y "
f"validada, rango 18–22 h) y {E1:.2f} h en Etapa 1 ({TOTAL:.2f} h en total). "
"Las horas por etapa son la base de la facturación (tarifa $600/h + IVA).", True),
("Etapas 2 y 3 desglosadas por actividad diaria con sus rangos estimados "
"(38–46 h y 24–29 h). Las fechas planificadas asumen media jornada en promedio "
"y están sujetas a la definición del flujo de facturación (en revisión interna "
"de Balam) y al acceso a Azure. El soporte post-lanzamiento (2 semanas, sin "
"costo) corre del 24-ago al 4-sep, después del cierre.", False),
("Integración Jira → BIND (9–12 h): ajuste no contemplado en la propuesta "
"original, incorporado en la propuesta ajustada v1.3; se ejecuta tras su "
"aprobación por escrito y una vez definido el flujo. De aprobarse, corre en "
"paralelo con la Etapa 2 la semana del 4-ago.", False),
(f"Total del plan con el ajuste: {TOT_MIN:.0f}–{TOT_MAX:.0f} h. El avance global "
f"({PCT_GLOBAL:.0%}) se calcula sobre el plan completo; el corte anterior (80%) "
"se calculaba solo sobre las Etapas 0-1.", False),
("En Etapa 1 siguen abiertos: capa de escritura (en pausa por la definición del "
"flujo), datos de prueba, y Azure/CI-CD (accesos del lado de Balam).", False),
]
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")
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws.row_dimensions[r].height = 28 if len(txt) > 120 else 14
r += 1
ws.freeze_panes = f"A{FILA_ENC + 1}"
ws.sheet_view.showGridLines = False
ws.print_area = f"A1:G{r}"
ws.page_setup.orientation = "landscape"
ws.page_setup.fitToWidth = 1
ws.sheet_properties.pageSetUpPr.fitToPage = True
wb.save(DST)
print("OK ->", DST)
print(f" E0 {E0:.2f} h · E1 {E1:.2f} h · trabajadas {TOTAL:.2f} h · "
f"plan {TOT_MIN:.0f}–{TOT_MAX:.0f} h · avance global {PCT_GLOBAL:.0%} · "
f"filas E2 {len(ETAPA2[5])} · E3 {len(ETAPA3[5])} · Jira {len(JIRA[5])}")