Prepara la Adenda 01 (integración Jira→BIND, 9–12 h) y el Excel de horas por etapa

Los dos pendientes que Erika re-preguntó el 28-jul: la adenda formaliza la
generación automática de cotizaciones desde tickets FAC ($5,400–$7,200 + IVA,
consistente con las ~10 h comunicadas el 21-jul) como solicitud de cambio
conforme a la §10 de la propuesta v1.2; el Excel resume horas por etapa
(E0 cerrada en 21.83 h, E1 en 19.75 h, E2/E3 con rangos contractuales) con
importes para facturación. PDF con el mismo diseño del paquete v1.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Johann
2026-07-28 22:56:09 -06:00
parent 980baf0a0a
commit 67bce9d691
6 changed files with 662 additions and 0 deletions
@@ -0,0 +1,187 @@
"""Genera Horas-etapas-Balam-2026-07-28.xlsx — resumen de horas POR ETAPA para Erika.
Pedido por Erika el 27-jul (6:46 pm: "las ya completas más que nada para ver
cuántas son") y aclarado el 28-jul (8:36 am): el Excel es el insumo del conteo
de horas de cada etapa "sobre todo para la FACTURACIÓN correspondiente".
Las horas trabajadas se leen del Excel interno (`Plan-actividades-avance-2026-07-27.xlsx`),
que a su vez sale de `Seguimiento-horas.csv` — una sola fuente de datos, igual que
`generar_resumen_erika_2026_07_27.py`. Los rangos estimados y montos son los
contractuales de la propuesta v1.2 (§4) y de la Adenda 01 (Jira → BIND).
"""
import os
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, "Horas-etapas-Balam-2026-07-28.xlsx")
EMISION = date(2026, 7, 28) # fecha de emisión; horas al corte del lunes 27-jul
TARIFA = 600 # MXN/h + IVA (propuesta v1.2 §3.3)
# Marca Balam
CAFE, AMARILLO, CREMA = "331F0E", "F7BD0C", "FDF3D8"
V_FILL, V_FONT = "C6EFCE", "006100" # verde
A_FILL, A_FONT = "FFEB9C", "9C6500" # amarillo
GRIS = "595959"
BORDE = "D9D9D9"
thin = Side(style="thin", color=BORDE)
box = Border(left=thin, right=thin, top=thin, bottom=thin)
# --- Horas trabajadas desde el Excel interno (única fuente) -------------------
wsrc = openpyxl.load_workbook(SRC).active
trab = {}
for row in range(2, wsrc.max_row + 1):
nombre = str(wsrc.cell(row=row, column=1).value or "").strip()
if nombre.startswith(("Etapa 0", "Etapa 1", "Integracion")):
clave = "TOTAL" if nombre.startswith("Integracion") else nombre.split(" ")[0] + nombre.split(" ")[1]
trab[clave] = float(wsrc.cell(row=row, column=10).value or 0)
E0, E1, TOTAL = trab["Etapa0"], trab["Etapa1"], trab["TOTAL"]
assert abs((E0 + E1) - TOTAL) < 0.01, f"Horas no cuadran: {E0} + {E1} != {TOTAL}"
# --- Filas del resumen --------------------------------------------------------
# (etapa, estatus, kind_estatus, est_min, est_max, trabajadas o None)
ETAPAS = [
("Etapa 0 — Discovery, infraestructura y prototipo visual",
"✅ Completada y validada (24-jul)", "ok", 18, 22, E0),
("Etapa 1 — Plataforma base y sincronización con BIND",
"🟡 En curso · 6 de 8 entregables terminados", "curso", 32, 39, E1),
("Etapa 2 — Facturación (consulta y emisión), catálogo y multimoneda",
"⚪ Próxima", "prox", 38, 46, None),
("Etapa 3 — Cobranza, tablero, reportes y cierre",
"⚪ Próxima", "prox", 24, 29, None),
]
ADENDA = ("Adicional — Integración Jira → BIND (Adenda 01)",
"📋 En aprobación · no estaba en la propuesta original", "prox", 9, 12, None)
SUB_MIN = sum(e[3] for e in ETAPAS) # 112
SUB_MAX = sum(e[4] for e in ETAPAS) # 136
TOT_MIN, TOT_MAX = SUB_MIN + ADENDA[3], SUB_MAX + ADENDA[4] # 121, 148
def dinero(h):
return f"${h * TARIFA:,.0f}"
def rango_dinero(lo, hi):
return f"{dinero(lo)} – {dinero(hi)}"
# --- Construcción -------------------------------------------------------------
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Horas por etapa"
ws["A1"] = "Proyecto Balam · Plataforma de Automatización Financiera"
ws["A1"].font = Font(bold=True, size=14, color=CAFE)
ws["A2"] = ("Horas por etapa — emitido el 28-jul-2026 · horas trabajadas al corte "
"del lunes 27-jul-2026 · tarifa $600 MXN/h + IVA")
ws["A2"].font = Font(size=10, italic=True, color=GRIS)
ENC = ["Etapa", "Estatus", "Horas estimadas", "Horas trabajadas\n(al 27-jul)",
"Importe estimado\n(MXN, antes de IVA)"]
ANCHOS = [52, 34, 15, 15, 20]
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", wrap_text=True)
c.border = box
ws.column_dimensions[get_column_letter(i)].width = ancho
ws.row_dimensions[FILA_ENC].height = 28
def fila(r, nombre, estatus, kind, est, trabajadas, importe, bold=False, fill=None):
c = ws.cell(row=r, column=1, value=nombre)
c.font = Font(bold=bold, size=10, color=CAFE)
c.alignment = Alignment(vertical="center", wrap_text=True)
cs = ws.cell(row=r, column=2, value=estatus)
cs.font = Font(size=9, color=GRIS)
cs.alignment = Alignment(vertical="center", wrap_text=True)
if kind == "ok":
cs.font = Font(size=9, color=V_FONT); cs.fill = PatternFill("solid", fgColor=V_FILL)
elif kind == "curso":
cs.font = Font(size=9, color=A_FONT); cs.fill = PatternFill("solid", fgColor=A_FILL)
ce = ws.cell(row=r, column=3, value=est)
ce.font = Font(bold=bold, size=10, color=CAFE)
ce.alignment = Alignment(horizontal="center", vertical="center")
ct = ws.cell(row=r, column=4, value=trabajadas if trabajadas is not None else "—")
if trabajadas is not None:
ct.number_format = "0.00"
ct.font = Font(bold=bold or trabajadas is not None, size=10, color=CAFE)
ct.alignment = Alignment(horizontal="center", vertical="center")
ci = ws.cell(row=r, column=5, value=importe)
ci.font = Font(bold=bold, size=10, color=CAFE)
ci.alignment = Alignment(horizontal="right", vertical="center")
for col in range(1, 6):
ws.cell(row=r, column=col).border = box
if fill:
ws.cell(row=r, column=col).fill = PatternFill("solid", fgColor=fill)
ws.row_dimensions[r].height = 26
r = FILA_ENC + 1
for nombre, estatus, kind, lo, hi, t in ETAPAS:
fila(r, nombre, estatus, kind, f"{lo} – {hi}", t, rango_dinero(lo, hi))
r += 1
fila(r, "Subtotal — propuesta v1.2", "", "sub", f"{SUB_MIN} – {SUB_MAX}", TOTAL,
rango_dinero(SUB_MIN, SUB_MAX), bold=True, fill=CREMA)
r += 1
nombre, estatus, kind, lo, hi, t = ADENDA
fila(r, nombre, estatus, kind, f"{lo} – {hi}", t, rango_dinero(lo, hi))
r += 1
fila(r, "Total con Adenda 01", "", "tot", f"{TOT_MIN} – {TOT_MAX}", TOTAL,
rango_dinero(TOT_MIN, TOT_MAX), bold=True, fill=CREMA)
r += 2
# --- Notas --------------------------------------------------------------------
notas = [
f"Horas trabajadas al corte del lunes 27-jul-2026, del control de horas del proyecto "
f"(mismas cifras del Excel de avance enviado el 27-jul): Etapa 0 {E0:.2f} h · "
f"Etapa 1 {E1:.2f} h · total {TOTAL:.2f} h.",
f"La Etapa 0 cerró en {E0:.2f} h, dentro del rango contractual de 18–22 h "
f"(a $600/h: {dinero(E0)} antes de IVA).",
"Referencia de facturación (propuesta v1.2, §3.3): la facturación inicial es de 30 h — "
"$18,000 + IVA — y cubre la Etapa 0 completa más el inicio de la Etapa 1; se emite en "
"cuanto se confirme el visto bueno. Los avances posteriores se facturan semanalmente "
"(viernes) por horas efectivamente trabajadas, con pago a 30 días naturales.",
"La integración Jira → BIND no estaba contemplada en la propuesta original; se "
"formaliza en la Adenda 01 (compartida el 28-jul) y se ejecuta tras su aprobación "
"por escrito.",
"Los rangos de horas operan con tope por etapa: al aproximarse al máximo, el trabajo "
"se detiene y se revisa el alcance antes de continuar.",
]
for txt in notas:
c = ws.cell(row=r, column=1, value=txt)
c.font = Font(italic=True, size=9, color=CAFE)
c.alignment = Alignment(wrap_text=True, vertical="top")
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=5)
ws.row_dimensions[r].height = 28 if len(txt) > 120 else 16
r += 1
ws.freeze_panes = f"A{FILA_ENC + 1}"
ws.sheet_view.showGridLines = False
ws.print_area = f"A1:E{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" E0 {E0:.2f} h · E1 {E1:.2f} h · total {TOTAL:.2f} h · "
f"v1.2 {SUB_MIN}–{SUB_MAX} h · con adenda {TOT_MIN}–{TOT_MAX} h")