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.
This commit is contained in:
JohannVelazquez
2026-07-28 22:21:04 -06:00
parent bee2d02f50
commit 820b8d41c8
8 changed files with 507 additions and 108 deletions
+190 -36
View File
@@ -1,16 +1,28 @@
"""Genera Plan-actividades-avance-2026-07-27.xlsx a partir del del 21-jul.
Aplica las horas y estatus reales al 27-jul: sesión de reglas (22-jul) y
validación del prototipo (24-jul, APROBADO) completadas; sesión de API de Jira
(27-jul) en curso; capa de escritura EN PAUSA a la espera de la definición del
flujo de facturación (sesión 28-jul con Arturo/Araceli).
Reutiliza el estilo/semáforo del archivo del 21-jul."""
import re
from datetime import date
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
SRC = "/Users/johann/Desktop/Proyectos personales/BALAM/planeacion/Plan-actividades-avance-2026-07-21.xlsx"
DST = "/Users/johann/Desktop/Proyectos personales/BALAM/planeacion/Plan-actividades-avance-2026-07-27.xlsx"
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"
@@ -21,17 +33,115 @@ G_FILL, G_FONT = "F2F2F2", "595959" # gris
C_PCT, C_BLQ, C_EST, C_TRAB, C_STAT = 7, 8, 9, 10, 11
# Prefijo de la actividad -> overrides de datos (pct=%, trab=horas, comienzo/fin=fechas texto)
# --- 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.67, trab=40),
"Etapa 0": dict(pct=1.0, trab=23),
"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, trab=1.05,
comienzo="vie 24/07/26", fin="vie 24/07/26"),
"Etapa 1": dict(pct=0.82, trab=17),
"Demostración semanal": dict(trab=1.75),
"Sesión de reglas y dudas": dict(pct=1.0, trab=0.80),
"Definición de alcance Jira": dict(pct=0.40, trab=0.20),
"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
@@ -39,37 +149,51 @@ 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": ("🟡 En curso · falta seeder", A_FILL, A_FONT, False),
"Capa de escritura controlada": ("🔴 En pausa · def. Balam 28-jul", R_FILL, R_FONT, True),
"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": ("🟡 En curso · API Jira 27-jul", A_FILL, A_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) Aplica overrides de datos por prefijo de nombre
# 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=ov["trab"])
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)
@@ -103,7 +227,7 @@ for row in range(2, ws.max_row + 1):
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.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):
@@ -115,32 +239,62 @@ for row in range(2, ws.max_row + 1):
pinta(stat, fill, font, bold)
break
# 4) Encabezado de la columna de estatus y pie
# 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)")
# 5) Reescribe leyenda/notas (borra las viejas de la línea de HOY del 21-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 ·") or v.startswith("▬ La línea roja"):
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),
("Núcleo de la Etapa 1 construido y verificado contra producción: autenticación y roles, "
"bitácora, multi-tenant con RLS, cliente de BIND, sincronización de clientes/facturas/"
"cotizaciones, modelo de facturas con estados y cartera por moneda.", 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),
("API de Jira: token recibido el 27-jul; la integración inicia una vez definido el flujo.", CAFE, False),
(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 = "Avance al 27-jul-2026 · Horas conforme al control local (pendiente conciliar con Jira)"
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.")