"""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")