Files
balam/planeacion/generar_resumen_erika_2026_07_27.py
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

176 lines
7.0 KiB
Python

"""Genera Avance-Balam-2026-07-27.xlsx — versión SIMPLE para enviar a Erika.
Se construye desde el Excel interno (`Plan-actividades-avance-2026-07-27.xlsx`),
que a su vez lee las horas de `Seguimiento-horas.csv`. Así los dos archivos nunca
se contradicen: este es una vista reducida, no una segunda fuente de datos.
Formato pedido por Erika el 21-jul: entregable + actividad + horas por etapa.
Se quitan del interno: rangos estimados, bloques del plan (B1/B4/B9), predecesoras
y duración en días — ruido para quien solo da seguimiento.
"""
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-27.xlsx")
HOY = date(2026, 7, 27)
# 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"
# Columnas del origen
O_NOMBRE, O_INI, O_FIN, O_PCT, O_TRAB, O_STAT = 1, 3, 4, 7, 10, 11
thin = Side(style="thin", color=BORDE)
box = Border(left=thin, right=thin, top=thin, bottom=thin)
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 ""
def es_etapa(nombre):
return nombre.startswith(("Etapa", "Integracion"))
# --- Lee el interno -----------------------------------------------------------
wsrc = openpyxl.load_workbook(SRC).active
filas = []
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
filas.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,
"trab": wsrc.cell(row=row, column=O_TRAB).value,
"stat": str(wsrc.cell(row=row, column=O_STAT).value or "").strip(),
"etapa": es_etapa(nombre),
})
# --- Construye el simple ------------------------------------------------------
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Avance"
ws["A1"] = "Proyecto Balam · Plataforma de Automatización Financiera"
ws["A1"].font = Font(bold=True, size=14, color=CAFE)
ws["A2"] = f"Avance al {HOY.day}-jul-2026"
ws["A2"].font = Font(size=10, italic=True, color=GRIS)
ENC = ["Actividad", "Inicio", "Fin", "Avance", "Horas", "Estatus"]
ANCHOS = [52, 12, 12, 10, 9, 34]
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")
c.border = box
ws.column_dimensions[get_column_letter(i)].width = ancho
r = FILA_ENC + 1
for f in filas:
fill_etapa = PatternFill("solid", fgColor=CREMA) if f["etapa"] else None
c = ws.cell(row=r, column=1, value=f["nombre"])
c.font = Font(bold=f["etapa"], size=10, color=CAFE)
c.alignment = Alignment(vertical="center", wrap_text=True,
indent=0 if f["etapa"] else 1)
for col, val in ((2, f["ini"]), (3, f["fin"])):
cc = ws.cell(row=r, column=col, value=val)
cc.font = Font(size=9, color=GRIS)
cc.alignment = Alignment(horizontal="center", vertical="center")
# Avance
cp = ws.cell(row=r, column=4, value=f["pct"])
cp.number_format = "0%"
cp.alignment = Alignment(horizontal="center", vertical="center")
if f["pct"] >= 1:
cp.font = Font(bold=True, size=10, color=V_FONT)
cp.fill = PatternFill("solid", fgColor=V_FILL)
elif f["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)
# Horas
ch = ws.cell(row=r, column=5, value=f["trab"])
ch.number_format = "0.00"
ch.font = Font(bold=f["etapa"], size=10, color=CAFE)
ch.alignment = Alignment(horizontal="center", vertical="center")
# Estatus
cs = ws.cell(row=r, column=6, value=f["stat"])
cs.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
cs.font = Font(size=9, color=GRIS)
if f["stat"].startswith(""):
cs.font = Font(size=9, color=V_FONT); cs.fill = PatternFill("solid", fgColor=V_FILL)
elif f["stat"].startswith("🟡"):
cs.font = Font(size=9, color=A_FONT); cs.fill = PatternFill("solid", fgColor=A_FILL)
elif f["stat"].startswith("🔴"):
cs.font = Font(size=9, bold=True, color=R_FONT); cs.fill = PatternFill("solid", fgColor=R_FILL)
for col in range(1, 7):
ws.cell(row=r, column=col).border = box
if fill_etapa and col in (1, 2, 3):
ws.cell(row=r, column=col).fill = fill_etapa
r += 1
# --- Notas --------------------------------------------------------------------
tot = next((f["trab"] for f in filas if f["nombre"].startswith("Integracion")), 0)
e0 = next((f["trab"] for f in filas if f["nombre"].startswith("Etapa 0")), 0)
e1 = next((f["trab"] for f in filas if f["nombre"].startswith("Etapa 1")), 0)
notas = [
("", False),
(f"Horas trabajadas: {e0:.2f} h en Etapa 0 y {e1:.2f} h en Etapa 1 "
f"({tot:.2f} h en total).", True),
("Etapa 1: 6 de los 8 entregables terminados — autenticación y roles con bitácora, "
"multi-tenant, cliente de la API 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.", False),
("La capa de escritura (cotización→factura) está en pausa a la espera de la "
"definición del flujo de facturación (sesión del martes 28-jul).", False),
("Azure y CI/CD esperan la configuración de los recursos del lado de Balam.", False),
]
for txt, bold in notas:
if txt:
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=6)
ws.row_dimensions[r].height = 28 if len(txt) > 110 else 14
r += 1
ws.freeze_panes = f"A{FILA_ENC + 1}"
ws.sheet_view.showGridLines = False
ws.print_area = f"A1:F{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" {len(filas)} filas · Etapa 0 {e0:.2f} h · Etapa 1 {e1:.2f} h · total {tot:.2f} h")