Files
balam/planeacion/generar_avance_2026_07_27.py
T
JohannVelazquez 820b8d41c8 Valida y consolida las horas al 27-jul: 41.58 h trazables desde el CSV
Las horas quedan validadas una por una contra evidencia fechada y el Excel
deja de tener cifras escritas a mano: se generan desde Seguimiento-horas.csv
y el script aborta si el detalle no cuadra con el total.

Correcciones de fondo:
- El Excel reportaba 17 h de Etapa 1 contra 16 h en el CSV (override manual).
- Comas sin escapar en el CSV ocultaban 1.00 h del 19-jul al parseo.
- El 10-jul estaba mal etiquetado: la mayor parte fue pulido del prototipo
  (E0-05), no el entregable de cierre (E0-06). Mismo total, etiquetas reales.
- Se incorporan 3.50 h no cobradas con evidencia: diseño del plan de Etapa 1
  (2.00 h) y preparación de las sesiones del 22 y 24-jul (1.50 h).
- Ajustes a la baja de Johann en horas reconstruidas: análisis del Discovery
  1.92→1.00 h, documentación de cobranza 1.50→0.75 h, guion 0.75→0.45 h.
  Las duraciones con transcript no se tocaron. Etapa 0 vuelve al rango 18–22 h.

Presentación del avance:
- Se reporta 75 % de Etapa 1 = 6 de 8 entregables cerrados, en lugar del 82 %
  ponderado anterior, que no distinguía trabajo hecho de trabajo bloqueado.
- Capa de escritura al 40 % (diseño, ADR-001 y modelo listos; falta el
  BindWriteClient y los shapes de los POST) y Jira al 90 % (conectividad
  resuelta el 27-jul; falta qué estatus dispara qué, sujeto a la definición
  de Balam). Modelo de facturas al 85 % con horas propias: ya no aparece
  avanzado con cero horas.
- Las 7 h que acumulaba "Demostración semanal" se reparten a las filas
  técnicas que les corresponden; esa fila se queda con gestión (1.75 h).

Nuevo: Avance-Balam-2026-07-27.xlsx, versión simple para Erika, derivada del
Excel interno para que ambos no puedan contradecirse.

Bitácora #58: Erika confirma que el Excel de horas es el insumo de
FACTURACIÓN, corrige su lectura de la capa de escritura al 100 %, y vuelve a
pedir la propuesta ajustada por Jira (pendiente desde el 22-jul). La sesión
del flujo de facturación no queda agendada: Balam la revisa internamente.
2026-07-28 22:21:04 -06:00

301 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 Plan-actividades-avance-2026-07-27.xlsx a partir del del 21-jul.
Diferencia clave vs. versiones anteriores: las HORAS TRABAJADAS ya no se escriben a
mano — se leen de `Seguimiento-horas.csv` (fuente de la verdad) y se agregan por
actividad del plan mediante el mapa ACT_POR_TAREA. Así cada celda de horas del
Excel es trazable a una o varias líneas del CSV, y el total del encabezado de cada
etapa es la suma real de su detalle.
Corte al 27-jul: validación del prototipo (24-jul, APROBADO) y sesión de reglas
(22-jul) completadas; API de Jira (27-jul) recibida; capa de escritura EN PAUSA a
la espera de la definición del flujo de facturación (sesión 28-jul).
"""
import csv
import os
import re
from collections import defaultdict
from datetime import date
import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
BASE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(BASE, "Plan-actividades-avance-2026-07-21.xlsx")
DST = os.path.join(BASE, "Plan-actividades-avance-2026-07-27.xlsx")
CSV_HORAS = os.path.join(BASE, "Seguimiento-horas.csv")
HOY = date(2026, 7, 27)
CAFE, CREMA, GRIS_B, AMARILLO = "331F0E", "FDF3D8", "D9D9D9", "F7BD0C"
V_FILL, V_FONT = "C6EFCE", "006100" # verde
A_FILL, A_FONT = "FFEB9C", "9C6500" # amarillo
R_FILL, R_FONT = "FFC7CE", "9C0006" # rojo
G_FILL, G_FONT = "F2F2F2", "595959" # gris
C_PCT, C_BLQ, C_EST, C_TRAB, C_STAT = 7, 8, 9, 10, 11
# --- Horas por fila del Excel --------------------------------------------------
# Las actividades del CSV no mapean 1:1 con las filas del plan (el CSV agrupa por
# actividad contractual; el Excel desglosa por tarea de proyecto). Se reparten por
# fila usando la evidencia de la bitácora, de modo que cada fila muestre el trabajo
# que realmente le corresponde y la suma siga cuadrando con el CSV.
#
# Trazabilidad de Etapa 1 (19.75 h del CSV):
# E1-01 6.50 = backend base 3.50 (13-14 jul) + B0/B1 1.00 + B2 0.50 + B3 0.50 + B4 1.00
# E1-02 0.50 = repositorio GitHub
# E1-03 2.00 = multi-tenant 1.50 (14-15 jul) + B6 RLS 0.50
# E1-04 2.00 = cliente de BIND
# E1-05 3.30 = gestión 0.50 + corte 16-jul 1.00 + cierre 21-jul 0.25 + prep 0.75 + sesión 0.80
# E1-06 3.25 = scaffold 2.00 + entorno local 1.00 + Scalar 0.25
# E1-07 0.20 = sesión API de Jira
# E1-08 2.00 = diseño del plan de Etapa 1
HORAS_POR_FILA = {
# --- Etapa 0 (21.83 h) — una actividad del CSV por fila
# E0-02 3.83 = sesión facturación 1.08 + análisis 1.00 + sesión cobranza 1.00 + notas 0.75
# E0-07 1.50 = guion 0.45 + sesión de validación 1.05 (transcript: 1 h 4 min)
"Sesión de arranque (kickoff)": 2.00, # E0-01
"Sesión de Discovery": 3.83, # E0-02
"Entrega y validación de accesos": 1.00, # E0-03
"Validación técnica de la API de BIND": 4.50, # E0-04
"Prototipo visual navegable": 7.50, # E0-05
"Documento de hallazgos": 1.50, # E0-06
"Sesión de validación del prototipo": 1.50, # E0-07 (guion 0.45 + sesión 1.05)
# --- Etapa 1 (19.75 h)
# El trabajo de andamiaje (entorno local, scaffold, Scalar) y el diseño del plan
# no tienen fila propia en el plan original: se cargan a la fila técnica que les
# corresponde por naturaleza, no a la de seguimiento semanal.
"Repositorio privado GitHub": 0.50, # E1-02
"Backend base": 8.25, # E1-01 5.00 + E1-06 3.25 (entorno, scaffold, Scalar)
"Arquitectura multi-tenant": 3.00, # E1-03 2.00 (incl. B6 RLS) + E1-08 1.00 (diseño general)
"Cliente de la API de BIND": 2.00, # E1-04
"Sincronización de clientes": 0.50, # E1-01 parte: B3
# B4 produjo dos cosas: el sync de facturas/cotizaciones y el modelo central
# (InvoiceStatusMapper con 13 pruebas + migración DetalleSyncPendiente). Se
# reparte para que ninguna fila quede en 0 h marcada como avanzada.
"Sincronización de facturas": 0.50, # E1-01 parte: B4 (sync)
"Modelo de facturas": 0.50, # E1-01 parte: B4 (mapper + migraciones)
"Sesión de reglas y dudas": 1.55, # E1-05 parte: prep 0.75 + sesión 0.80
"Definición de alcance Jira": 0.20, # E1-07
"Demostración semanal": 1.75, # E1-05 resto: gestión 0.50 + corte 1.00 + cierre 0.25
# Análisis y diseño de B7 (pipeline Draft→DryRun→Confirm, flags, regla PPD/PUE,
# ADR-001): 1.00 h de E1-08, separada del diseño general para que la fila del
# entregable 4 muestre el trabajo que sí se hizo. La implementación sigue en pausa.
"Capa de escritura controlada": 1.00,
# --- Sin horas aún (trabajo no iniciado o dependiente de Balam)
"Configuración de infraestructura en Azure": 0.00,
"CI/CD": 0.00,
}
def horas_por_actividad(path):
"""Suma horas del CSV por actividad_id y por etapa."""
por_act = defaultdict(float)
por_etapa = defaultdict(float)
with open(path, encoding="utf-8", newline="") as fh:
rdr = csv.DictReader(fh)
for i, r in enumerate(rdr, start=2):
if not (r.get("fecha") or "").strip():
continue
if None in r or any(v is None for v in r.values()):
raise ValueError(
f"{os.path.basename(path)} línea {i}: número de campos inesperado "
f"(¿falta escapar una coma con comillas?)")
h = float(r["horas"])
por_act[r["actividad_id"]] += h
por_etapa[r["etapa"]] += h
return por_act, por_etapa
HORAS_ACT, HORAS_ETAPA = horas_por_actividad(CSV_HORAS)
TOTAL_CSV = sum(HORAS_ETAPA.values())
# % por entregables terminados (dato duro y defendible, no promedio ponderado).
# Etapa 1: 6 de 8 entregables contractuales TERMINADOS (1,2,3,5,6,7) = 75%. Los dos
# restantes van parciales y su avance se refleja en su propia fila (entregable 4 al
# 40%, entregable 8 al 85%), no promediado en el encabezado: la etapa se mide por
# entregable cerrado, que es lo verificable.
PCT_ETAPA = {"0": 1.0, "1": 0.75}
OVERRIDES = {
"Integracion de plataformas Balam": dict(pct=0.80, trab=TOTAL_CSV),
"Etapa 0": dict(pct=PCT_ETAPA["0"], trab=HORAS_ETAPA["0"]),
"Etapa 1": dict(pct=PCT_ETAPA["1"], trab=HORAS_ETAPA["1"]),
"Documento de hallazgos": dict(pct=1.0),
"Sesión de validación del prototipo": dict(pct=1.0, comienzo="vie 24/07/26",
fin="vie 24/07/26"),
# Estas dos filas absorben el andamiaje y el diseño del plan: se renombran y se
# amplía su rango estimado para que el nombre refleje el alcance que cubren.
"Backend base": dict(
nombre="Backend base: entorno, andamiaje, autenticación, roles y bitácora",
est="89"),
"Arquitectura multi-tenant": dict(
nombre="Arquitectura multi-tenant (tenant_id + RLS) y diseño de la solución",
est="34"),
"Demostración semanal": dict(est="23"),
# B7: diseño, ADR-001, entidad WriteOperation (migrada en B1) y policy
# PuedeOperarEscritura ya existen; falta BindWriteClient, el pipeline y los
# shapes de los POST (dependen de las reglas del 28-jul). Sin horas propias:
# las del modelo y la policy ya están cobradas en B1/B2 ("Backend base").
"Capa de escritura controlada": dict(pct=0.40),
"Sesión de reglas y dudas": dict(pct=1.0),
"Sincronización de clientes": dict(pct=1.0),
"Sincronización de facturas": dict(pct=1.0),
# Conectividad resuelta el 27-jul (token PAF a 1 año, API read+write, bandeja FAC,
# límites de consumo). Queda el 10 %: qué estatus dispara qué, amarrado al 28-jul.
"Definición de alcance Jira": dict(pct=0.90),
}
# Estatus a mano (gana sobre el semáforo automático): texto, fill, font, negrita
K_ESPECIAL = {
"Documento de hallazgos": ("✅ Completada", V_FILL, V_FONT, False),
"Sesión de validación del prototipo": ("✅ Validado (aprobado)", V_FILL, V_FONT, True),
"Demostración semanal": ("🟡 En curso (semanal)", A_FILL, A_FONT, False),
"Modelo de facturas": ("🟡 Modelo y migraciones ✓ · falta seeder", A_FILL, A_FONT, False),
"Capa de escritura controlada": ("🔴 Diseño listo · impl. en pausa (def. Balam 28-jul)",
R_FILL, R_FONT, True),
"Sesión de reglas y dudas": ("✅ Completada", V_FILL, V_FONT, False),
"Definición de alcance Jira": ("🟡 Conectividad lista · falta regla 28-jul",
A_FILL, A_FONT, False),
"Configuración de infraestructura en Azure": ("🟡 Pendiente (Balam)", A_FILL, A_FONT, False),
"CI/CD": ("🔴 Pendiente · dep. Azure", R_FILL, R_FONT, True),
}
def fecha(celda):
m = re.search(r"(\d{2})/(\d{2})/(\d{2})", str(celda or ""))
return date(2000 + int(m.group(3)), int(m.group(2)), int(m.group(1))) if m else None
def pinta(celda, fill, font_color, bold=False):
celda.fill = PatternFill("solid", fgColor=fill)
celda.font = Font(color=font_color, bold=bold, size=10)
celda.alignment = Alignment(horizontal="center", vertical="center")
wb = openpyxl.load_workbook(SRC)
ws = wb.active
# 1) Horas trabajadas desde el CSV + overrides de datos por prefijo de nombre
for row in range(2, ws.max_row + 1):
nombre = str(ws.cell(row=row, column=1).value or "").strip()
if not nombre:
continue
# 1a) horas por fila (reparto trazable al CSV; ver HORAS_POR_FILA)
for prefijo, h in HORAS_POR_FILA.items():
if nombre.startswith(prefijo):
ws.cell(row=row, column=C_TRAB, value=h)
break
# 1b) overrides de %, fechas, nombre y rango estimado
for prefijo, ov in OVERRIDES.items():
if nombre.startswith(prefijo):
if "pct" in ov: ws.cell(row=row, column=C_PCT, value=ov["pct"])
if "trab" in ov: ws.cell(row=row, column=C_TRAB, value=round(ov["trab"], 2))
if "comienzo" in ov: ws.cell(row=row, column=3, value=ov["comienzo"])
if "fin" in ov: ws.cell(row=row, column=4, value=ov["fin"])
if "est" in ov: ws.cell(row=row, column=C_EST, value=ov["est"])
break
# 2) Recalcula el semáforo con HOY=27-jul (mismas reglas que el del 21-jul)
for row in range(2, ws.max_row + 1):
nombre = str(ws.cell(row=row, column=1).value or "").strip()
if not nombre or nombre.startswith("") or nombre.startswith(""):
continue
es_etapa = nombre.startswith("Etapa") or nombre.startswith("Integracion")
pct = ws.cell(row=row, column=C_PCT).value or 0
fin = fecha(ws.cell(row=row, column=4).value)
ini = fecha(ws.cell(row=row, column=3).value)
stat = ws.cell(row=row, column=C_STAT)
if es_etapa:
stat.value = ""
elif pct >= 1:
stat.value = "✅ Completada"; pinta(stat, V_FILL, V_FONT)
elif fin and fin < HOY:
stat.value = "🔴 Vencida"; pinta(stat, R_FILL, R_FONT, bold=True)
elif ini and ini <= HOY:
stat.value = "🟡 En curso"; pinta(stat, A_FILL, A_FONT)
elif pct > 0:
stat.value = "🟢 Adelantada"; pinta(stat, V_FILL, V_FONT)
else:
stat.value = "⚪ Próxima"; pinta(stat, G_FILL, G_FONT)
# semáforo en la celda de %
pcel = ws.cell(row=row, column=C_PCT)
if not es_etapa:
if pct >= 1: pinta(pcel, V_FILL, V_FONT, bold=True)
elif pct > 0: pinta(pcel, A_FILL, A_FONT, bold=True)
elif fin and fin < HOY: pinta(pcel, R_FILL, R_FONT, bold=True)
pcel.number_format = "0%"
ws.cell(row=row, column=C_TRAB).number_format = "0.00"
# 3) Overrides finos de estatus (bloqueos y dependencias)
for row in range(2, ws.max_row + 1):
nombre = str(ws.cell(row=row, column=1).value or "").strip()
for prefijo, (txt, fill, font, bold) in K_ESPECIAL.items():
if nombre.startswith(prefijo):
stat = ws.cell(row=row, column=C_STAT)
stat.value = txt
pinta(stat, fill, font, bold)
break
# 4) Renombra las filas que absorbieron alcance (al final: los pasos 2-3 buscan por
# el nombre original)
for row in range(2, ws.max_row + 1):
nombre = str(ws.cell(row=row, column=1).value or "").strip()
for prefijo, ov in OVERRIDES.items():
if "nombre" in ov and nombre.startswith(prefijo):
ws.cell(row=row, column=1, value=ov["nombre"])
break
# 5) Encabezado de la columna de estatus
ws.cell(row=1, column=C_STAT, value=f"Estatus (hoy {HOY.day}-jul)")
# 6) Reescribe leyenda/notas (borra las viejas)
for row in range(ws.max_row, 1, -1):
v = str(ws.cell(row=row, column=1).value or "")
if v.startswith(("✅ Completada ·", "▬ La línea roja", "Núcleo de la Etapa 1",
"🔴 La capa de escritura", "API de Jira:", "Horas:")):
for col in range(1, C_STAT + 1):
ws.cell(row=row, column=col).value = None
ley = ws.max_row + 2
notas = [
("✅ Completada · 🟡 En curso · 🔴 Vencida/Pendiente · ⚪ Próxima", G_FONT, True),
(f"Horas: {HORAS_ETAPA['0']:.2f} h de Etapa 0 y {HORAS_ETAPA['1']:.2f} h de Etapa 1 "
f"({TOTAL_CSV:.2f} h en total), conforme al control local de horas trabajadas. "
f"Etapa 1 estimada en 3239 h.", CAFE, False),
("Etapa 1: 6 de 8 entregables contractuales terminados — autenticación y roles con "
"bitácora, multi-tenant con RLS, cliente de BIND, sincronización de clientes, "
"sincronización de facturas y cotizaciones, y modelo de facturas con estados y "
"cartera por moneda. Verificado contra producción: 1,494 facturas sincronizadas "
"con la cartera cuadrada.", CAFE, False),
("🔴 La capa de escritura (cotización→factura) queda EN PAUSA a la espera de la "
"definición del flujo de facturación (sesión del martes 28-jul con Arturo y Araceli).",
"9C0006", False),
("Pendientes menores: datos de prueba (seeder) y verificación del aislamiento RLS "
"con rol no-superusuario. Azure y CI/CD dependen de accesos del lado de Balam.", CAFE, False),
("API de Jira: token recibido el 27-jul; la integración inicia una vez definido el flujo.",
CAFE, False),
]
for i, (txt, color, bold) in enumerate(notas):
c = ws.cell(row=ley + i, column=1, value=txt)
c.font = Font(italic=True, size=9, color=color, bold=bold)
ws.freeze_panes = "A2"
ws.oddFooter.center.text = (f"Avance al 27-jul-2026 · {TOTAL_CSV:.2f} h trabajadas "
"conforme al control local (pendiente conciliar con Jira)")
wb.save(DST)
# --- Verificación: el detalle debe cuadrar con el encabezado de cada etapa -----
print("OK ->", DST)
print(f" Etapa 0: {HORAS_ETAPA['0']:.2f} h · Etapa 1: {HORAS_ETAPA['1']:.2f} h "
f"· TOTAL {TOTAL_CSV:.2f} h")
suma_detalle = sum(HORAS_POR_FILA.values())
ok = abs(suma_detalle - TOTAL_CSV) < 0.01
print(f" Suma del detalle mostrado: {suma_detalle:.2f} h "
f"({'cuadra con el CSV' if ok else f'NO CUADRA (CSV {TOTAL_CSV:.2f})'})")
if not ok:
raise SystemExit("El detalle por fila no cuadra con el CSV — revisar HORAS_POR_FILA.")