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>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"""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])}")
|
||||
Reference in New Issue
Block a user