Actualiza cierre de Etapa 0 y control de horas
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
"""Actualiza el plan oficial con el corte reconstruido de horas al 15-jul-2026."""
|
||||
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
|
||||
PATH = Path(__file__).with_name("Plan-actividades-avance-2026-07-10-ajustado.xlsx")
|
||||
|
||||
|
||||
def find_row(ws, task):
|
||||
for row in range(2, ws.max_row + 1):
|
||||
if ws.cell(row, 1).value == task:
|
||||
return row
|
||||
raise ValueError(f"No se encontró la tarea: {task}")
|
||||
|
||||
|
||||
def copy_row_style(ws, source_row, target_row, max_col=11):
|
||||
for col in range(1, max_col + 1):
|
||||
source = ws.cell(source_row, col)
|
||||
target = ws.cell(target_row, col)
|
||||
if source.has_style:
|
||||
target._style = copy(source._style)
|
||||
target.number_format = source.number_format
|
||||
target.alignment = copy(source.alignment)
|
||||
|
||||
|
||||
workbook = openpyxl.load_workbook(PATH)
|
||||
sheet = workbook["Plan"]
|
||||
|
||||
# Actividades separadas después del plan original. Las inserciones son idempotentes.
|
||||
if not any(
|
||||
sheet.cell(row, 1).value == "Definición de alcance Jira → cotización BIND"
|
||||
for row in range(2, sheet.max_row + 1)
|
||||
):
|
||||
source_row = find_row(sheet, "Sesión de reglas de negocio con Arturo")
|
||||
sheet.insert_rows(source_row + 1)
|
||||
copy_row_style(sheet, source_row, source_row + 1)
|
||||
values = [
|
||||
"Definición de alcance Jira → cotización BIND",
|
||||
"2 días",
|
||||
"mié 22/07/26",
|
||||
"jue 23/07/26",
|
||||
None,
|
||||
"Johan y Balam",
|
||||
0,
|
||||
]
|
||||
for column, value in enumerate(values, start=1):
|
||||
sheet.cell(source_row + 1, column, value)
|
||||
|
||||
if not any(
|
||||
sheet.cell(row, 1).value == "CI/CD + gestión de secretos"
|
||||
for row in range(2, sheet.max_row + 1)
|
||||
):
|
||||
source_row = find_row(sheet, "Repositorio + CI/CD + gestión de secretos")
|
||||
sheet.insert_rows(source_row + 1)
|
||||
copy_row_style(sheet, source_row, source_row + 1)
|
||||
values = [
|
||||
"CI/CD + gestión de secretos",
|
||||
"1 día",
|
||||
"vie 24/07/26",
|
||||
"vie 24/07/26",
|
||||
None,
|
||||
"Johan y Pedro",
|
||||
0,
|
||||
]
|
||||
for column, value in enumerate(values, start=1):
|
||||
sheet.cell(source_row + 1, column, value)
|
||||
|
||||
# Calendario ajustado con los acuerdos del 13–15 de julio.
|
||||
updates = {
|
||||
"Sesión de validación del prototipo": (
|
||||
"1 día",
|
||||
"jue 23/07/26",
|
||||
"jue 23/07/26",
|
||||
"Johan y Balam",
|
||||
),
|
||||
"Documento de hallazgos + ADRs + plan refinado": (
|
||||
"10 días",
|
||||
"vie 10/07/26",
|
||||
"jue 23/07/26",
|
||||
"Johan",
|
||||
),
|
||||
"Sesión de reglas de negocio con Arturo": (
|
||||
"1 día",
|
||||
"mié 22/07/26",
|
||||
"mié 22/07/26",
|
||||
"Arturo y CEO",
|
||||
),
|
||||
"Backend base: autenticación, roles y bitácora de auditoría": (
|
||||
"5 días",
|
||||
"lun 13/07/26",
|
||||
"vie 17/07/26",
|
||||
"Johan",
|
||||
),
|
||||
"Repositorio + CI/CD + gestión de secretos": (
|
||||
"4 días",
|
||||
"lun 13/07/26",
|
||||
"jue 16/07/26",
|
||||
"Pedro y Noé",
|
||||
),
|
||||
"Arquitectura multi-tenant (tenant_id + RLS)": (
|
||||
"4 días",
|
||||
"mar 14/07/26",
|
||||
"vie 17/07/26",
|
||||
"Johan",
|
||||
),
|
||||
}
|
||||
|
||||
for task, (duration, start, end, resources) in updates.items():
|
||||
row = find_row(sheet, task)
|
||||
sheet.cell(row, 2, duration)
|
||||
sheet.cell(row, 3, start)
|
||||
sheet.cell(row, 4, end)
|
||||
sheet.cell(row, 6, resources)
|
||||
|
||||
rules_row = find_row(sheet, "Sesión de reglas de negocio con Arturo")
|
||||
sheet.cell(rules_row, 1, "Sesión de reglas y dudas de Etapa 1")
|
||||
repo_row = find_row(sheet, "Repositorio + CI/CD + gestión de secretos")
|
||||
sheet.cell(repo_row, 1, "Repositorio privado GitHub")
|
||||
|
||||
headers = {
|
||||
8: "Horas al 15/07",
|
||||
9: "Tipo de registro",
|
||||
10: "Notas de horas",
|
||||
11: "Rango estimado",
|
||||
}
|
||||
for column, value in headers.items():
|
||||
cell = sheet.cell(1, column, value)
|
||||
cell.fill = copy(sheet.cell(1, 1).fill)
|
||||
cell.font = copy(sheet.cell(1, 1).font)
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
|
||||
# Corte: 22 h de Etapa 0 + 8 h del inicio de Etapa 1 = 30 h.
|
||||
hours = {
|
||||
"Sesión de arranque (kickoff) con el Ing. Noé": (
|
||||
2.00,
|
||||
"Incluye preparación y sesión de 1 h",
|
||||
),
|
||||
"Sesión de Discovery: proceso actual de facturación y cobranza": (
|
||||
5.50,
|
||||
"Incluye preparación, sesiones y documentación de facturación/cobranza",
|
||||
),
|
||||
"Entrega y validación de accesos (BIND + manual de marca)": (
|
||||
1.00,
|
||||
"Coordinación, recepción y salvaguardas",
|
||||
),
|
||||
"Validación técnica de la API de BIND con la cuenta real": (
|
||||
4.50,
|
||||
"117 consultas GET, análisis y reporte técnico",
|
||||
),
|
||||
"Prototipo visual navegable": (
|
||||
6.00,
|
||||
"Diseño, construcción y ajustes con datos ficticios",
|
||||
),
|
||||
"Sesión de validación del prototipo": (0.00, "Agendada para el 23-jul"),
|
||||
"Documento de hallazgos + ADRs + plan refinado": (
|
||||
3.00,
|
||||
"PDF/correo de entrega, hallazgos, ADRs y actualización del plan",
|
||||
),
|
||||
"Sesión de reglas y dudas de Etapa 1": (0.00, "Agendada para el 22-jul"),
|
||||
"Definición de alcance Jira → cotización BIND": (
|
||||
0.00,
|
||||
"Se define con Balam el 22–23 jul",
|
||||
),
|
||||
"Backend base: autenticación, roles y bitácora de auditoría": (
|
||||
3.50,
|
||||
"Trabajo local del 13–15 jul",
|
||||
),
|
||||
"Repositorio privado GitHub": (0.50, "Coordinación de creación y acceso"),
|
||||
"CI/CD + gestión de secretos": (0.00, "Programado para el 24-jul"),
|
||||
"Arquitectura multi-tenant (tenant_id + RLS)": (
|
||||
1.50,
|
||||
"Diseño técnico inicial",
|
||||
),
|
||||
"Cliente de la API de BIND (reintentos, OData, errores)": (
|
||||
2.00,
|
||||
"Inicio del cliente productivo",
|
||||
),
|
||||
"Demostración semanal (viernes) + reporte de horas": (
|
||||
0.50,
|
||||
"Seguimiento, ajuste de calendario y corte de horas",
|
||||
),
|
||||
}
|
||||
|
||||
pending = {
|
||||
"Sesión de validación del prototipo",
|
||||
"Sesión de reglas y dudas de Etapa 1",
|
||||
"Definición de alcance Jira → cotización BIND",
|
||||
"CI/CD + gestión de secretos",
|
||||
}
|
||||
|
||||
stage_rows = {0: [], 1: []}
|
||||
current_stage = None
|
||||
for row in range(2, sheet.max_row + 1):
|
||||
task = sheet.cell(row, 1).value
|
||||
if task == "Etapa 0":
|
||||
current_stage = 0
|
||||
sheet.cell(row, 11, "18–22 h")
|
||||
continue
|
||||
if task == "Etapa 1":
|
||||
current_stage = 1
|
||||
sheet.cell(row, 11, "32–39 h")
|
||||
continue
|
||||
if task == "Integracion de plataformas Balam":
|
||||
sheet.cell(row, 11, "112–136 h")
|
||||
continue
|
||||
if task not in hours:
|
||||
continue
|
||||
value, note = hours[task]
|
||||
sheet.cell(row, 8, value)
|
||||
sheet.cell(row, 9, "Pendiente" if task in pending else "Reconstruida")
|
||||
sheet.cell(row, 10, note)
|
||||
if current_stage in stage_rows:
|
||||
stage_rows[current_stage].append(row)
|
||||
|
||||
project_row = find_row(sheet, "Integracion de plataformas Balam")
|
||||
stage0_row = find_row(sheet, "Etapa 0")
|
||||
stage1_row = find_row(sheet, "Etapa 1")
|
||||
sheet.cell(stage0_row, 8, f"=SUM({','.join(f'H{row}' for row in stage_rows[0])})")
|
||||
sheet.cell(stage1_row, 8, f"=SUM({','.join(f'H{row}' for row in stage_rows[1])})")
|
||||
sheet.cell(project_row, 8, f"=H{stage0_row}+H{stage1_row}")
|
||||
|
||||
summaries = {
|
||||
project_row: "Corte reconstruido al 15-jul",
|
||||
stage0_row: "22 h: límite superior estimado de la etapa",
|
||||
stage1_row: "8 h consumidas al 15-jul; etapa aún en curso",
|
||||
}
|
||||
for row, note in summaries.items():
|
||||
sheet.cell(row, 9, "Resumen")
|
||||
sheet.cell(row, 10, note)
|
||||
|
||||
for row in range(2, sheet.max_row + 1):
|
||||
sheet.cell(row, 8).number_format = '0.00 "h"'
|
||||
for column in range(8, 12):
|
||||
sheet.cell(row, column).alignment = Alignment(vertical="top", wrap_text=True)
|
||||
if sheet.cell(row, 1).value in {
|
||||
"Integracion de plataformas Balam",
|
||||
"Etapa 0",
|
||||
"Etapa 1",
|
||||
}:
|
||||
for column in range(8, 12):
|
||||
sheet.cell(row, column).font = Font(bold=True, color="FFFFFF")
|
||||
sheet.cell(row, column).fill = PatternFill("solid", fgColor="331F0E")
|
||||
|
||||
widths = {
|
||||
1: 58,
|
||||
2: 13,
|
||||
3: 16,
|
||||
4: 16,
|
||||
5: 14,
|
||||
6: 22,
|
||||
7: 14,
|
||||
8: 16,
|
||||
9: 18,
|
||||
10: 55,
|
||||
11: 18,
|
||||
}
|
||||
for column, width in widths.items():
|
||||
sheet.column_dimensions[get_column_letter(column)].width = width
|
||||
|
||||
sheet.freeze_panes = "A2"
|
||||
sheet.auto_filter.ref = f"A1:K{sheet.max_row}"
|
||||
sheet.sheet_properties.pageSetUpPr.fitToPage = True
|
||||
sheet.page_setup.fitToWidth = 1
|
||||
sheet.page_setup.fitToHeight = 0
|
||||
sheet.oddFooter.center.text = (
|
||||
"Horas reconstruidas al 15-jul-2026; validar contra Jira antes de facturar"
|
||||
)
|
||||
|
||||
stage0_total = sum(hours[sheet.cell(row, 1).value][0] for row in stage_rows[0])
|
||||
stage1_total = sum(hours[sheet.cell(row, 1).value][0] for row in stage_rows[1])
|
||||
assert stage0_total == 22.0
|
||||
assert stage1_total == 8.0
|
||||
|
||||
temporary = PATH.with_suffix(".tmp.xlsx")
|
||||
workbook.save(temporary)
|
||||
check = openpyxl.load_workbook(temporary, data_only=False)["Plan"]
|
||||
assert check.max_column == 11
|
||||
assert check.cell(find_row(check, "Etapa 0"), 8).value.startswith("=SUM(")
|
||||
assert check.cell(find_row(check, "Etapa 1"), 8).value.startswith("=SUM(")
|
||||
temporary.replace(PATH)
|
||||
|
||||
print(f"Actualizado: {PATH}")
|
||||
print("Etapa 0: 22.00 h | Etapa 1: 8.00 h | Total al 15-jul: 30.00 h")
|
||||
Reference in New Issue
Block a user