820b8d41c8
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.
176 lines
7.0 KiB
Python
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")
|