Files
balam/planeacion/generar_plan_completo_2026_07_28.py
T
Johann 46f86ef827 Ajusta el alcance Jira→BIND: 7-10 h (antes 9-12) e inicio lunes 27-jul
Recalcula todas las cifras dependientes en la propuesta v1.3: fila Ajuste
$4,200-$6,000, total primera etapa 119-146 h / $71,400-$87,600 (antes
121-148 h / $72,600-$88,800). El bloque de la §2 y su Excel homólogo pasan
de "se ejecuta junto con las Etapas 2-3" a "inicia el lunes 27 de julio
(token PAF ya entregado)", reflejando que la conexión a la API de Jira no
depende de la definición del flujo de facturación, solo del token que
Pedro ya entregó ese día. Las 3 actividades del plan se re-fechan a
27-29/jul y sus rangos siguen sumando exacto el total de la etapa.

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

337 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 7–10 h · total 119–146 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)",
"27/07/2026", "29/07/2026", 7, 10, [
("Cliente de la API de Jira (token PAF): lectura de tickets FAC y control de límites",
"27/07/2026", "27/07/2026", "2–3", PROX_JIRA),
("Disparador por estatus → generación automática de la cotización en BIND",
"28/07/2026", "28/07/2026", "3–4", PROX_JIRA),
("Trazabilidad ticket↔cotización↔factura, alimentación del tablero y prueba observada",
"29/07/2026", "29/07/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] # 119
TOT_MAX = 22 + 39 + ETAPA2[4] + ETAPA3[4] + JIRA[4] # 146
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 (7–10 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. Se programó a partir "
"del lunes 27-jul —día en que Pedro entregó el token PAF— en paralelo con "
"el cierre de la Etapa 1 y el arranque de la Etapa 2.", 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])}")