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:
@@ -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="8–9"),
|
||||
"Arquitectura multi-tenant": dict(
|
||||
nombre="Arquitectura multi-tenant (tenant_id + RLS) y diseño de la solución",
|
||||
est="3–4"),
|
||||
"Demostración semanal": dict(est="2–3"),
|
||||
# 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 32–39 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.")
|
||||
|
||||
Reference in New Issue
Block a user