6d4b781486
El wizard de 5 pasos de la VISTA 3 quedó navegable en balam-plataforma con datos reales de solo lectura (cotizaciones de /api/quotes y ticket FAC vivo): selección de cotización, datos de factura con regla PPD/PUE y advertencia SAT, IVA por regla con totales reactivos, prefactura simulada con marca de agua y aprobación humana visual; el timbrado queda deshabilitado hasta B7 (commits 2164f6c, 90f7633 y 0600f2f en el repo del cliente). Excel de avance del 2.º corte generado desde script (el corte matutino no se toca); la fila de conversión cotización→factura pasa a 40% como interfaz en simulación.
193 lines
8.7 KiB
Python
193 lines
8.7 KiB
Python
"""Genera Avance-Balam-2026-08-06b.xlsx a partir del corte de la tarde del 6-ago.
|
||
|
||
Segundo corte del jueves 6-ago (sesión vespertina, previa a la demo del vie
|
||
7-ago): el wizard de emisión asistida quedó construido en la plataforma como
|
||
interfaz navegable de simulación — 5 pasos fieles al prototipo aprobado, con
|
||
las cotizaciones reales de BIND y el ticket FAC vivo (solo lectura), prefactura
|
||
simulada con marca de agua y aprobación humana visual. El timbrado queda
|
||
deshabilitado hasta que Balam autorice la escritura (B7). El corte de la
|
||
mañana (Avance-Balam-2026-08-06.xlsx) no se toca.
|
||
|
||
Las horas se leen de `Seguimiento-horas.csv` (fuente de la verdad, corte
|
||
54.83 h); el script aborta si el reparto por fila no cuadra con el CSV.
|
||
"""
|
||
import csv
|
||
import os
|
||
from collections import defaultdict
|
||
|
||
import openpyxl
|
||
from openpyxl.styles import Alignment, Font, PatternFill
|
||
|
||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||
SRC = os.path.join(BASE, "Avance-Balam-2026-08-06.xlsx")
|
||
DST = os.path.join(BASE, "Avance-Balam-2026-08-06b.xlsx")
|
||
CSV_HORAS = os.path.join(BASE, "Seguimiento-horas.csv")
|
||
|
||
CAFE, GRIS = "331F0E", "595959"
|
||
V_FILL, V_FONT = "C6EFCE", "006100"
|
||
A_FILL, A_FONT = "FFEB9C", "9C6500"
|
||
R_FILL, R_FONT = "FFC7CE", "9C0006"
|
||
G_FILL, G_FONT = "F2F2F2", "595959"
|
||
|
||
C_PCT, C_EST, C_TRAB, C_STAT = 4, 5, 6, 7
|
||
|
||
# --- CSV: totales por etapa y por actividad -----------------------------------
|
||
por_act, por_etapa = defaultdict(float), defaultdict(float)
|
||
with open(CSV_HORAS, encoding="utf-8", newline="") as fh:
|
||
for r in csv.DictReader(fh):
|
||
if not (r.get("fecha") or "").strip():
|
||
continue
|
||
por_act[r["actividad_id"]] += float(r["horas"])
|
||
por_etapa[r["etapa"]] += float(r["horas"])
|
||
|
||
E0, E1 = por_etapa["0"], por_etapa["1"]
|
||
AJ = por_act["AJ-01"] # ajuste Jira→BIND (vive dentro de la etapa 2 del CSV)
|
||
E2 = por_etapa["2"] - AJ # Etapa 2 propiamente (frontend)
|
||
TOTAL = E0 + E1 + E2 + AJ
|
||
assert abs(TOTAL - sum(por_etapa.values())) < 0.01
|
||
PCT_GLOBAL = round(TOTAL / ((119 + 146) / 2), 2)
|
||
|
||
# --- Reparto por fila del delta corte matutino→vespertino (1.00 h nueva) -------
|
||
# Trazabilidad:
|
||
# E2-01 +1.00 (wizard de emisión asistida, 5 pasos en simulación)
|
||
# → fila "Conversión cotización→factura" (el wizard ES su interfaz;
|
||
# la conversión real sigue bloqueada por B7)
|
||
DELTAS_TRAB = {
|
||
"Conversión cotización→factura": 1.00,
|
||
}
|
||
assert abs(sum(DELTAS_TRAB.values()) - 1.00) < 0.01, "El reparto no suma la 1.00 h nueva"
|
||
|
||
ENCABEZADOS = {
|
||
"Integracion de plataformas Balam": (TOTAL, PCT_GLOBAL),
|
||
"Etapa 0": (E0, 1.0),
|
||
"Etapa 1": (E1, 0.875),
|
||
"Etapa 2": (E2, 0.55),
|
||
"» Ajuste dentro de la Etapa 2": (AJ, 0.55),
|
||
"Etapa 3": (None, 0),
|
||
}
|
||
|
||
# (pct, estatus, fill, font, bold) — solo lo que cambió respecto al corte matutino.
|
||
FILAS = {
|
||
"Conversión cotización→factura": (0.4, "🟡 Interfaz completa en simulación (6-ago): wizard de 5 pasos con "
|
||
"cotizaciones y tickets reales, prefactura simulada y aprobación humana "
|
||
"visual · el timbrado real espera la autorización de escritura (B7)",
|
||
A_FILL, A_FONT, False),
|
||
}
|
||
|
||
|
||
def pinta(celda, fill, font_color, bold=False, size=9, align="left"):
|
||
celda.fill = PatternFill("solid", fgColor=fill)
|
||
celda.font = Font(color=font_color, bold=bold, size=size)
|
||
celda.alignment = Alignment(horizontal=align, vertical="center", wrap_text=True)
|
||
|
||
|
||
wb = openpyxl.load_workbook(SRC)
|
||
ws = wb.active
|
||
|
||
ws["A2"] = ("Plan y avance al 6-ago-2026 (2.º corte del día) · lectura de la bandeja Jira (FAC) "
|
||
"completa y wizard de emisión asistida en simulación; timbrado y escritura en espera de Balam")
|
||
ws["A2"].font = Font(size=10, italic=True, color=GRIS)
|
||
|
||
# Las notas del corte anterior van fusionadas A:G y algunas empiezan con
|
||
# "Etapa 1: …", que colisiona con los prefijos de encabezado: se localizan y
|
||
# limpian ANTES del bucle principal, y este se detiene donde empezaban.
|
||
primera_nota = None
|
||
for row in range(5, ws.max_row + 1):
|
||
v = str(ws.cell(row=row, column=1).value or "")
|
||
if v.startswith("✅ Completada ·"):
|
||
primera_nota = row
|
||
if primera_nota and row >= primera_nota:
|
||
for rango in list(ws.merged_cells.ranges):
|
||
if rango.min_row == row:
|
||
ws.unmerge_cells(str(rango))
|
||
ws.cell(row=row, column=1).value = None
|
||
tope = primera_nota if primera_nota else ws.max_row + 1
|
||
|
||
aplicado = set()
|
||
for row in range(5, tope):
|
||
nombre = str(ws.cell(row=row, column=1).value or "").strip()
|
||
if not nombre:
|
||
continue
|
||
|
||
for prefijo, (trab, pct) in ENCABEZADOS.items():
|
||
if nombre.startswith(prefijo):
|
||
if trab is not None:
|
||
c = ws.cell(row=row, column=C_TRAB, value=round(trab, 2))
|
||
c.number_format = "0.00"
|
||
p = ws.cell(row=row, column=C_PCT, value=pct)
|
||
p.number_format = "0%"
|
||
if pct >= 1:
|
||
pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center")
|
||
elif pct > 0:
|
||
pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center")
|
||
break
|
||
|
||
for prefijo, delta in DELTAS_TRAB.items():
|
||
if nombre.startswith(prefijo):
|
||
actual = ws.cell(row=row, column=C_TRAB).value
|
||
actual = float(actual) if isinstance(actual, (int, float)) else 0.0
|
||
c = ws.cell(row=row, column=C_TRAB, value=round(actual + delta, 2))
|
||
c.number_format = "0.00"
|
||
aplicado.add(prefijo)
|
||
break
|
||
|
||
for prefijo, (pct, txt, fill, fcolor, bold) in FILAS.items():
|
||
if nombre.startswith(prefijo):
|
||
if pct is not None:
|
||
p = ws.cell(row=row, column=C_PCT, value=pct)
|
||
p.number_format = "0%"
|
||
if pct >= 1:
|
||
pinta(p, V_FILL, V_FONT, bold=True, size=10, align="center")
|
||
elif pct > 0:
|
||
pinta(p, A_FILL, A_FONT, bold=True, size=10, align="center")
|
||
else:
|
||
p.font = Font(size=10, color=GRIS)
|
||
s = ws.cell(row=row, column=C_STAT, value=txt)
|
||
pinta(s, fill, fcolor, bold)
|
||
break
|
||
|
||
faltan = set(DELTAS_TRAB) - aplicado
|
||
assert not faltan, f"Filas no encontradas en el Excel: {faltan}"
|
||
|
||
# --- Notas: se escribe el bloque vespertino donde estaba el matutino ----------
|
||
notas = [
|
||
("✅ Completada · 🟡 En curso · 🔴 Pendiente/bloqueada · ⚪ Próxima · "
|
||
"(adelantada) = ejecutada antes de su fecha plan", True),
|
||
(f"Horas trabajadas al 2.º corte del jueves 6-ago: Etapa 0 {E0:.2f} h (cerrada y validada) · "
|
||
f"Etapa 1 {E1:.2f} h (rango 32–39) · Etapa 2 {E2:.2f} h · ajuste Jira {AJ:.2f} h "
|
||
f"(de 7–10 aprobadas) · total {TOTAL:.2f} h. Base de facturación a $600/h + IVA.", True),
|
||
("Etapa 1: 7 de 8 entregables contractuales terminados. Solo queda la capa de "
|
||
"escritura, en pausa por la definición del flujo de facturación.", False),
|
||
("Integración Jira → BIND (7–10 h, v1.3 aprobada por el Ing. Noé el 30-jul): la "
|
||
"LECTURA quedó completa el 6-ago — la plataforma muestra la bandeja de solicitudes "
|
||
"y el detalle de cada ticket de FAC en vivo (solo lectura), con los campos del "
|
||
"formulario mapeados y los adjuntos descargables. El disparador de cotización y la "
|
||
"trazabilidad esperan las definiciones de negocio (request type, estatus disparador "
|
||
"y origen de monto/conceptos) y la prueba de escritura acompañada.", False),
|
||
("Etapa 2: al frontend de consulta y al chrome del prototipo se sumó el wizard de "
|
||
"emisión asistida (6-ago): 5 pasos sobre cotizaciones y tickets reales, con regla "
|
||
"PPD/PUE y advertencia SAT, IVA automático, prefactura simulada y aprobación humana "
|
||
"visual. El timbrado queda deshabilitado hasta que Balam autorice la escritura (B7).", False),
|
||
("En espera de Balam: acceso a Azure (Contributor — con el Ing. Noé desde el "
|
||
"28-jul), fecha de la sesión del flujo de facturación, y las tres definiciones de "
|
||
"la integración Jira.", False),
|
||
]
|
||
r = primera_nota if primera_nota else ws.max_row + 2
|
||
for txt, bold in notas:
|
||
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")
|
||
try:
|
||
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
|
||
except ValueError:
|
||
pass # ya estaba fusionada desde el archivo origen
|
||
ws.row_dimensions[r].height = 28 if len(txt) > 120 else 14
|
||
r += 1
|
||
|
||
ws.print_area = f"A1:G{r}"
|
||
wb.save(DST)
|
||
|
||
print("OK ->", DST)
|
||
print(f" E0 {E0:.2f} · E1 {E1:.2f} · E2 {E2:.2f} · Jira {AJ:.2f} · TOTAL {TOTAL:.2f} h "
|
||
f"· avance global {PCT_GLOBAL:.0%}")
|