Registro #92: Azure + CI/CD (14–15-oct) — plataforma en balam-paf-staging.azurewebsites.net, pipeline de GitHub Actions por OIDC sin secretos (build verde; la publicación espera el rol Website Contributor que asigna Balam), PostgreSQL 17 B1ms con respaldos de 35 días, Log Analytics, Application Insights, Key Vault y Storage creados (≈32 USD/mes), traspaso de la BD ensayado y pendiente (red de la BD nueva, ventana tras la prueba de Pedro); pendientes para cerrar la Etapa 1. Incluye lo que estaba sin subir: #91 (primer CFDI timbrado desde la plataforma, 1–2 oct), corte de horas del 2-oct con su Excel y generador, manual de usuario, cartas de datos bancarios, reglas de permisos de Claude Code (.claude/settings.json) y start-n8n.bat
This commit is contained in:
Binary file not shown.
@@ -51,14 +51,15 @@ fecha,semana,etapa,actividad_id,actividad,horas,estado_hora,fuente_evidencia,fac
|
||||
2026-08-11,2026-W33,1,E1-05,"Bitácora #62–#63 (merge de las definiciones, respuesta de Noé) y sondeo nocturno de FAC (molde inexistente, FAC-98)",0.75,reconstruida,bitacora/REGISTRO.md#63,si,"commits f28fcc5, a2f338f; por validar"
|
||||
2026-08-11,2026-W33,2,E2-02,"Capa de escritura mínima ticket→cotización: Quotes_NewQuote en BindClient, POST /api/emision/cotizacion con dry-run/confirmación, pantalla 'Generar cotización en BIND' y script para Balam (73/73 pruebas)",2.50,reconstruida,bitacora/REGISTRO.md#64,si,"commits e854eda y a99eb8b (cliente, 21:28), 2fb230e (21:30); sesión nocturna; por validar"
|
||||
2026-08-12,2026-W33,2,E2-02,"Madrugada y previo a la sesión: coreografía de la demo (Actualizar sin caché, Cotizar por fila, vista previa automática), sin reintentos en escrituras, tipo de cambio, prefill y selector de cliente",2.00,reconstruida,bitacora/REGISTRO.md#64,si,"commits eafa49b 00:34, 1b062d2 01:33, 2be0432/8228c40 06:55 (cliente); por validar"
|
||||
2026-08-12,2026-W33,2,AJ-01,"Sesión acompañada de escritura Jira→BIND con Noé, Araceli y Pedro: formulario construido en vivo, decisión de que la plataforma cotiza, primer POST con 500",1.00,confirmada,fuentes/2026-08-12 - Transcript - Sesion prueba de escritura Jira-BIND.txt,si,"Transcript de 52 min; REGISTRO #65; relevo de Erika por Pedro"
|
||||
2026-08-12,2026-W33,2,AJ-01,"Excel de campos y catálogos de la cotización BIND (67 servicios, 3 productos, 17 clientes) y correo a Pedro (CC Ara, Noé)",0.75,reconstruida,Cotizacion-BIND-campos-y-catalogos.xlsx,si,"correo 8:09 am; REGISTRO #66; por validar"
|
||||
2026-08-12,2026-W33,2,E2-02,"Sesión acompañada de escritura Jira→BIND con Noé, Araceli y Pedro: formulario construido en vivo, decisión de que la plataforma cotiza, primer POST con 500",1.00,confirmada,fuentes/2026-08-12 - Transcript - Sesion prueba de escritura Jira-BIND.txt,si,"Transcript de 52 min; REGISTRO #65; relevo de Erika por Pedro; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-12,2026-W33,2,E2-02,"Excel de campos y catálogos de la cotización BIND (67 servicios, 3 productos, 17 clientes) y correo a Pedro (CC Ara, Noé)",0.75,reconstruida,Cotizacion-BIND-campos-y-catalogos.xlsx,si,"correo 8:09 am; REGISTRO #66; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-20,2026-W34,1,E1-05,"Coordinación con Pedro por WhatsApp (18–20 ago): 17 categorías, formulario modificado, ticket FAC-106 y convocatoria del 21",0.50,reconstruida,bitacora/REGISTRO.md#67,si,"por validar"
|
||||
2026-08-20,2026-W34,1,E1-05,"Cruce del buzón contra la bitácora (9 comunicaciones, 2 correcciones), registro #65 desde el transcript y guion de la sesión del 21-ago",2.00,reconstruida,bitacora/REGISTRO.md#65,si,"commits 5d846ec 23:02 → 072a3cd 23:52; por validar"
|
||||
2026-08-20,2026-W34,2,AJ-01,"Sondeo de FAC-106 por API, diagnóstico del 500 (VATRate como fracción, VAT, PriceListID) y wizard cableado al formulario 'Cotización Bind', verificado e2e (74/74)",1.50,reconstruida,planeacion/Sondeo-FAC-106-y-payload-2026-08-20.md,si,"commits f839d0e 23:41 y eb04e23 23:47 (cliente); por validar"
|
||||
2026-08-21,2026-W34,2,AJ-01,"Sesión 7:00 am 'Seguimiento - PAF' con Noé, Araceli y Pedro: tres intentos de emisión (500) y pivote a las definiciones",0.25,reconstruida,bitacora/REGISTRO.md#69,si,"Informe Read AI recibido 7:16 am; por validar"
|
||||
2026-08-21,2026-W34,2,AJ-01,"Investigación posterior: schema del portal, intentos 4–5, PRIMERA COTIZACIÓN REAL (folio 1026), VAT corregido como tasa, registro #69, correo 'Seguimiento FAC-106' a Noé y borrador del correo de causa",3.00,reconstruida,bitacora/REGISTRO.md#69,si,"commits 46fad4c 10:15 (cliente) y d31747d 10:16; correo 7:32 am; por validar"
|
||||
2026-08-21,2026-W34,2,AJ-01,"Sesión 1:00 pm 'Seguimiento - PAF (2)': homologación de estados del flujo ticket→cotización→prefactura→factura; pendiente prueba de escritura en Jira",0.50,reconstruida,correo Read AI 21-ago 1:34 pm,si,"SIN registro en bitácora; por validar"
|
||||
2026-08-20,2026-W34,1,E1-05,"Cruce del buzón contra la bitácora (9 comunicaciones incorporadas, 2 correcciones)",1.00,reconstruida,commit 5d846ec 23:02,si,por validar; dividida el 2-oct: la otra hora (registro #65 y guion del 21-ago) pasó a E2-02
|
||||
2026-08-20,2026-W34,2,E2-02,"Registro #65 de la sesión de cotización del 12-ago desde el transcript y guion de la sesión del 21-ago (aviso previo a Pedro, coreografía de escritura, plan B del 500, 8 preguntas, protocolo si falla)",1.00,reconstruida,"commits a03f81f 23:29, cabe0fe 23:50 y 072a3cd 23:52",si,por validar; dividida el 2-oct de la fila de gestión: documenta y prepara trabajo de cotización (regla 7)
|
||||
2026-08-20,2026-W34,2,E2-02,"Sondeo de FAC-106 por API, diagnóstico del 500 (VATRate como fracción, VAT, PriceListID) y wizard cableado al formulario 'Cotización Bind', verificado e2e (74/74)",1.50,reconstruida,planeacion/Sondeo-FAC-106-y-payload-2026-08-20.md,si,"commits f839d0e 23:41 y eb04e23 23:47 (cliente); por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-21,2026-W34,2,E2-02,"Sesión 7:00 am 'Seguimiento - PAF' con Noé, Araceli y Pedro: tres intentos de emisión (500) y pivote a las definiciones",0.25,reconstruida,bitacora/REGISTRO.md#69,si,"Informe Read AI recibido 7:16 am; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-21,2026-W34,2,E2-02,"Investigación posterior: schema del portal, intentos 4–5, PRIMERA COTIZACIÓN REAL (folio 1026), VAT corregido como tasa, registro #69, correo 'Seguimiento FAC-106' a Noé y borrador del correo de causa",3.00,reconstruida,bitacora/REGISTRO.md#69,si,"commits 46fad4c 10:15 (cliente) y d31747d 10:16; correo 7:32 am; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-21,2026-W34,2,E2-02,Sesión 1:00 pm 'Seguimiento - PAF (2)': homologación de estados del flujo ticket→cotización→prefactura→factura; pendiente prueba de escritura en Jira,0.50,reconstruida,correo Read AI 21-ago 1:34 pm,si,"SIN registro en bitácora; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-08-23,2026-W34,2,E2-03,"Camino cotización→prefactura real: Invoices_AddInvoice con candado, POST /api/emision/prefactura, wizard sin simulación (borrador + aprobación real), herencia de CFDIUse/ServiceID/PriceListID desde factura y cliente",3.00,reconstruida,balam-plataforma commits a893fa9..a07fff6,si,"commits 22:15–23:40 del 23-ago (cliente); por validar"
|
||||
2026-08-24,2026-W35,2,E2-03,"Sesión 7:00 am 'Seguimiento - PAF': validación del flujo cotización→prefactura (MXN/USD, IVA 16 %/0 %) y ajuste en vivo: false = remisión, columna BIND en la bandeja",1.00,reconstruida,correo Read AI 24-ago 7:44 am,si,"commit 77d3767 7:38 (cliente); SIN registro en bitácora; por validar"
|
||||
2026-08-25,2026-W35,1,E1-05,"Correo a Noé con los requisitos de Azure (suscripción, grupo de recursos, Contributor) y lectura de su explicación remisión vs prefactura",0.50,reconstruida,correo 25-ago 4:54 pm,si,"por validar"
|
||||
@@ -67,14 +68,35 @@ fecha,semana,etapa,actividad_id,actividad,horas,estado_hora,fuente_evidencia,fac
|
||||
2026-08-26,2026-W35,2,E2-03,"Contención del incidente: confirmación de prefactura bloqueada (409), candado triple IsFiscalInvoice=false, API en ReadOnly, registro #70",0.75,reconstruida,bitacora/REGISTRO.md#70,si,"commits c5143cf 7:25 y fbb31fc 7:59 (cliente), a48fba0 8:01; por validar"
|
||||
2026-08-26,2026-W35,2,E2-03,"Investigación de cierre solo lectura: body de Invoices_AddInvoice vs portal y SDK, PDFs de 1000/1001/2384/2386, censo de 58 no fiscales, diff prefactura 2384 vs remisión — adenda al #70",2.50,reconstruida,bitacora/REGISTRO.md#70,si,"commit 2ae6880 21:28; por validar"
|
||||
2026-08-26,2026-W35,2,E2-03,"Experimentos autorizados en modo Write (folio compartido, 12 sondas de campo, DELETE), dump del APIM (98 ops), script de captura Playwright y adenda 2 al #70",2.50,reconstruida,bitacora/REGISTRO.md#70,si,"commits e7775e0 21:48 (cliente) y db16f62 00:10 del 27-ago; por validar"
|
||||
2026-09-03,2026-W36,2,AJ-01,"Sesión 7:00 am 'Seguimiento PAF': datos para el tenant de Azure y conceptos de Acuntia (headcount); definición: ítem por código y concepto = título del ticket",0.50,reconstruida,correo Read AI 3-sep 7:28 am,si,"SIN registro en bitácora; por validar"
|
||||
2026-09-03,2026-W36,2,E2-02,Sesión 7:00 am 'Seguimiento PAF': datos para el tenant de Azure y conceptos de Acuntia (headcount); definición: ítem por código y concepto = título del ticket,0.50,reconstruida,correo Read AI 3-sep 7:28 am,si,"SIN registro en bitácora; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-09-03,2026-W36,2,E2-02,"Catálogo de servicios de BIND en la emisión (GET /api/Services), mapeo categoría Jira → código configurable, selector de ítem, y caché de la bandeja invalidada al cotizar (visto en producción)",1.50,reconstruida,balam-plataforma commit ed4c819,si,"archivos 07:46–07:58 y 12:55 del 3-sep; por validar"
|
||||
2026-09-03,2026-W36,1,E1-09,"Publicación de la plataforma en https://paputec.mx/balam/ como ambiente provisional: Docker (Postgres + API chiseled read-only), Apache/proxy/SPA, deploy.sh reproducible, seeder de admin, migración al arrancar",2.50,reconstruida,balam-plataforma/deploy/paputec/README.md,si,"archivos 08:36–12:57 del 3-sep; commit 03582af; por validar"
|
||||
2026-09-03,2026-W36,3,E3-01,"Endurecimiento aplicado en la publicación: CSP/HSTS, rate limit por IP, bloqueo de login, contraseñas 12+, secretos solo en .env root, respaldos diarios con retención",1.00,reconstruida,balam-plataforma/deploy/paputec/README.md,si,"commit 03582af; por validar"
|
||||
2026-09-03,2026-W36,3,E3-02,"Runbook de publicación, seguridad aplicada, operación y rollback (deploy/paputec/README.md)",0.50,reconstruida,balam-plataforma/deploy/paputec/README.md,si,"commit 03582af; por validar"
|
||||
2026-09-03,2026-W36,2,AJ-01,"Sesión 1:30 pm 'Seguimiento PAF (2)': tres tickets de Pedro convertidos en cotizaciones reales desde la plataforma publicada; prueba con ticket de Linares",0.25,reconstruida,correo Read AI 3-sep 1:51 pm,si,"SIN registro en bitácora; por validar"
|
||||
2026-09-03,2026-W36,2,E2-02,Sesión 1:30 pm 'Seguimiento PAF (2)': tres tickets de Pedro convertidos en cotizaciones reales desde la plataforma publicada; prueba con ticket de Linares,0.25,reconstruida,correo Read AI 3-sep 1:51 pm,si,"SIN registro en bitácora; por validar; reclasificada de AJ-01 a E2-02 el 2-oct por Johann: es cotización contra el API de BIND, no integración con Jira"
|
||||
2026-09-07,2026-W37,2,E2-02,"Cotización en lote desde la bandeja: selección de tickets, pantalla cotizar-lote con dry-run y confirmación por ticket en serie, reglas de prellenado compartidas (cotizacion-prefill.ts)",2.00,reconstruida,balam-plataforma commit fbdbd18,si,"archivos 07:27–07:30 del 7-sep; por validar"
|
||||
2026-09-09,2026-W37,2,E2-03,"Sesión 1:00 pm 'Seguimiento PAF - Prefactura' con Araceli y Pedro: prefactura creada en la UI de BIND ('Generar venta') mientras se capturan las peticiones (InvoiceDetails)",0.50,reconstruida,correo Read AI 9-sep 1:24 pm,si,"SIN registro en bitácora; por validar"
|
||||
2026-09-09,2026-W37,2,E2-03,"Tras la sesión de la 1:00 pm: análisis de la captura de 'Generar venta' en la UI de BIND, prueba de folio (no se puede sobrescribir) y diseño del puente a la web app de BIND para la prefactura",1.25,reconstruida,transcript de Claude Code 65198403 (17:36–18:53); balam-plataforma/CLAUDE.md §Puente a la UI,si,Corte 2-oct: no entró al corte del 15-sep (el código se subió hasta el 29-sep en 4d6b3d8); por validar
|
||||
2026-09-10,2026-W37,2,E2-03,"Sesión 1:00 pm con Pedro: análisis de la cotización 1078 (Acuntia) y captura de la petición de red de la prefactura para evaluar su réplica",0.25,reconstruida,correo Read AI 10-sep 1:21 pm,si,"SIN registro en bitácora; por validar"
|
||||
2026-09-10,2026-W37,2,E2-03,"Preparación de la segunda sesión de captura: lista de lo que hay que recabar en DevTools (Generar venta sobre la 1078, Preserve log, Fetch/XHR)",0.25,reconstruida,transcript de Claude Code 65198403 (12:21),si,Corte 2-oct; por validar
|
||||
2026-09-10,2026-W37,2,E2-03,"Contrato real de AddInvoiceTransaction (PrevInvoice/FiscalInvoice/IsCFDI + QuoteID + Number), sonda autorizada op #12 (remisión 1004 creada y cancelada), BindUiClient con login de formulario, endpoints prefactura-ui, vista de cotizaciones sin ticket y mensaje a Pedro con las alternativas",1.75,reconstruida,"transcript de Claude Code 65198403 (13:21–13:29, 14:15–15:49, 16:50–16:53); commit 4d6b3d8",si,Corte 2-oct; la sesión con Pedro (1:00–1:21) ya está en su fila; por validar
|
||||
2026-09-15,2026-W38,1,E1-05,"Corte de horas 6-ago→15-sep (reconstrucción desde commits, correos e informes de reunión), Excel de avance, commits y publicación de ambos repositorios",0.75,reconstruida,planeacion/Seguimiento-horas.csv,si,"sesión asistida por IA; por validar"
|
||||
2026-09-16,2026-W38,1,E1-05,"Excel de avance publicado en Google Sheets (Drive/Balam vía rclone, enlace compartible) y corte del 16-sep regenerado desde script",0.50,reconstruida,planeacion/generar_avance_2026_09_16.py,si,"sesión asistida por IA; por validar"
|
||||
2026-09-29,2026-W40,2,E2-03,"Sesión 7:00 am 'Seguimiento PAF - Integración ERP' con Pedro: validación de accesos, Balam entrega login web a BIND con permisos de prefactura y error al cargar la solicitud al cotizar",0.50,reconstruida,correo Read AI 29-sep 7:23 am; REGISTRO #83,si,Corte 2-oct; por validar
|
||||
2026-09-29,2026-W40,2,E2-03,Cierre de los huecos de bitácora #71–#83 desde el buzón (informes de Read AI y correos del 21-ago al 29-sep) e investigación de cierre con el SDK oficial de BIND (modelo de dos endpoints),0.75,reconstruida,bitacora/REGISTRO.md#71-83; commit 3de2739 08:06,si,"Corte 2-oct; otra máquina, inicio estimado al terminar la sesión (7:23); por validar; movida de E1-05 a E2-03 el 2-oct: documenta las sesiones de prefactura e investiga el SDK de BIND (regla 7)"
|
||||
2026-09-29,2026-W40,2,E2-03,"Puente de prefactura con el usuario propio CTO: credenciales y API key nueva en user-secrets, app en modo escritura, PRIMERA PREFACTURA REAL desde la plataforma (folio 2467, 16:08), una sesión por usuario, API interno de la web app y contención de la escritura accidental (FillQuote: cotización 1079 Surtida)",1.50,reconstruida,"transcript de Claude Code 65198403 y 02f58158 (10:54–11:21, 13:24–13:39, 15:28–16:26); balam-plataforma/CLAUDE.md",si,"Corte 2-oct; dos sesiones en paralelo, contadas una sola vez; por validar"
|
||||
2026-09-29,2026-W40,2,AJ-01,"Webhook de Jira FAC: análisis (leer la creación de tickets vs ADR-001), plan de 5 fases, Fase 1 por GET (token de Pedro con ADMINISTER), endpoint POST /api/jira/webhook con HMAC, idempotencia y relectura (38/38), túnel ngrok y piloto e2e: webhook id 1 + FAC-169 detectado en 14.4 s y borrado",1.00,reconstruida,"transcript de Claude Code 02f58158 (15:34–15:41, 17:57–18:14, 18:39–19:08); REGISTRO #84; commit d7238da",si,"Corte 2-oct; incluye plan, bitácora y push de 4d6b3d8 (rebase con 3 conflictos); por validar"
|
||||
2026-09-29,2026-W40,2,AJ-01,"Cierre de la sesión: push de ambos repos, appsettings.Local portátil de los user-secrets y relevo a la máquina con acceso a paputec",0.50,reconstruida,"transcript de Claude Code 02f58158 (23:47–00:11); commits 8479e96, 5145801, e5912bf",si,Corte 2-oct; por validar
|
||||
2026-09-30,2026-W40,2,AJ-01,"Webhook en producción (paputec): fusión de las dos bitácoras del 29-sep, publicación de 5145801 (116 pruebas, respaldo y rollback), secreto nuevo, PUT del webhook id 1 y prueba e2e con FAC-170 detectada en 1.05 s",0.50,reconstruida,"bitacora/REGISTRO.md#85; commits e88b5e1 00:19, 9351aa4 00:30, 7e50c71 00:31",si,Corte 2-oct; ≈12:10–12:35 am; por validar
|
||||
2026-09-30,2026-W40,2,AJ-01,"Vista previa automática de la cotización por cada ticket detectado (cola, trabajador y port del prellenado; 147 pruebas), push y publicación; causa del 500 de BIND: la API key de arturo (la de cto responde 200)",0.50,reconstruida,"bitacora/REGISTRO.md#86; commits 1c37eaa 00:45, 287cb65, 085e903 00:55",si,Corte 2-oct; ≈12:35–1:00 am; por validar
|
||||
2026-09-30,2026-W40,2,AJ-01,"API key de cto en producción (el sync vuelve tras 20 días), radiografía del formulario contra el catálogo, vista previa validada contra BIND real con FAC-171 y modal en vivo con 'Aceptar', campana y modo automático configurable (170 pruebas)",0.75,reconstruida,"bitacora/REGISTRO.md#87; commits aac7447 01:40, bdafb06 01:42",si,Corte 2-oct; ≈1:00–1:45 am; por validar
|
||||
2026-09-30,2026-W40,2,AJ-01,"Push y publicación del modal en vivo, con el modo automático permitido en el servidor (sin activar)",0.25,reconstruida,"bitacora/REGISTRO.md#87 adenda; commits 29a354b, 8799348 06:55",si,Corte 2-oct; respaldo de las 6:53 am; por validar
|
||||
2026-09-30,2026-W40,2,E2-03,"Puente de prefactura encendido en producción, 'Servicio de Enlace' → ítem 106 y relectura completa de cotizaciones (11 con estado atrasado), tras lo que Pedro vio a las 7:26 en el wizard de FAC-173",0.75,reconstruida,bitacora/REGISTRO.md#88,si,Corte 2-oct; 7:50–8:30 am; por validar
|
||||
2026-09-30,2026-W40,2,E2-03,"Cuatro bugs reales del puente y de los importes (Total en cero, USD mostrado en pesos e IVA doble, post-proceso que escondía la prefactura, duplicados posibles) con pruebas que los reproducen (197), panel bajo la fila y sync de cotizaciones por barrido completo",1.25,reconstruida,"bitacora/REGISTRO.md#88; commits 597c8a2 21:16, 63af3d6 21:29",si,Corte 2-oct; 8:30–9:40 pm; por validar
|
||||
2026-09-30,2026-W40,2,E2-03,"Credenciales del usuario CTO en el .env de producción (autorización de Johann), push y publicación de 597c8a2 con la migración QuoteSubtotal y verificación del primer ciclo del sync",0.25,reconstruida,"bitacora/REGISTRO.md#88 adenda; commits 4a219b5, 604cb8d 21:49",si,Corte 2-oct; 9:40–9:50 pm; por validar
|
||||
2026-09-30,2026-W40,2,E2-03,"Timbrado desde la plataforma (POST /api/emision/timbrar-ui: EmitCFDI con dry-run, palabra TIMBRAR y verificación por lectura), estado incierto cerrado en los POST, consecutivo sin la serie B, uso CFDI del mismo cliente, 212 pruebas, recorrido en navegador, publicación y guion de la prueba del 1-oct",0.75,reconstruida,"bitacora/REGISTRO.md#89; commits f5b3743 22:40, 9874547 22:45",si,Corte 2-oct; 10:00–10:45 pm según respaldo (04:40 UTC) y último commit — el título del #89 dice 10:00 pm–12:50 am: Johann no recuerda la hora de cierre (2-oct): se queda la cota de la evidencia
|
||||
2026-09-30,2026-W40,2,E2-03,"Llamada con Pedro (7:26 am): revisión del wizard de FAC-173 en producción — prefactura deshabilitada, totales en cero, campo de servicios y Arturo en la lista de aprobadores",0.25,validada,bitacora/REGISTRO.md#88,si,Corte 2-oct; ~15 min según Johann (2-oct)
|
||||
2026-10-01,2026-W40,2,E2-04,Prueba completa con Balam en producción: ticket → cotización → prefactura; el timbrado (prefactura → CFDI) no se intentó y queda por probar,0.25,validada,planeacion/Guion-prueba-completa-2026-10-01.md,si,Corte 2-oct; ~20 min o menos según Johann (2-oct)
|
||||
2026-10-02,2026-W40,2,E2-03,"Prefacturas en lote (Cotizaciones y bandeja → una prefactura por cotización, en serie, folios consecutivos con el API lento) y timbrado también en el detalle de la factura (213 pruebas, recorrido en navegador); push y publicación",1.00,reconstruida,"bitacora/REGISTRO.md#90; commits 73d1c2a 08:35, 4f3eb99, 4e90543 08:41",si,Corte 2-oct; 7:40–8:40 am; por validar
|
||||
2026-10-02,2026-W40,1,E1-05,"Corte de horas 16-sep → 2-oct: pull de ambos repos, reconstrucción desde los transcripts de Claude Code, los commits y los informes de Read AI, y Excel de avance",0.75,confirmada,transcript de Claude Code a134588d (11:47–12:25); planeacion/generar_avance_2026_10_02.py,si,"Corte 2-oct; sesión asistida por IA; tiempo real. No se cobran la proyección de costos, la reclasificación ni los ajustes posteriores del Excel (~0.75 h, decisión de Johann 2-oct)"
|
||||
2026-10-02,2026-W40,2,E2-04,"Manual de usuario en PDF (14 páginas): acceso y roles, consulta, solicitudes de Jira, ticket → cotización (aviso en vivo, pantalla y lote), prefactura (individual, emisión asistida y lote), timbrado, reglas y mensajes frecuentes, con capturas de la demo",0.50,confirmada,manual/Manual-usuario-Balam.pdf; transcript de Claude Code a134588d (12:25–12:55),si,Corte 2-oct; sesión asistida por IA; tiempo real de esta sesión
|
||||
|
||||
|
@@ -5,7 +5,7 @@ Fuente local de horas efectivamente trabajadas para el proyecto Balam. Se mantie
|
||||
**Tarifa contractual:** $600 MXN/h + IVA
|
||||
**Cadencia:** corte semanal; factura los viernes; pago a 30 días
|
||||
**Datos tabulares:** [`Seguimiento-horas.csv`](Seguimiento-horas.csv)
|
||||
**Última actualización:** 2026-09-16 (corte 6-ago → 16-sep reconstruido, por validar)
|
||||
**Última actualización:** 2026-10-02 (corte 16-sep → 2-oct reconstruido, por validar)
|
||||
|
||||
## Reglas de captura
|
||||
|
||||
@@ -18,6 +18,94 @@ Fuente local de horas efectivamente trabajadas para el proyecto Balam. Se mantie
|
||||
7. El esfuerzo real de una sesión incluye TODO su alrededor, no solo el desarrollo: el análisis/pensamiento previo para decidir qué se hace, la documentación de cada parte (bitácora, pendientes, investigación), y la comunicación asociada (cada correo, cada mensaje de WhatsApp, minutas y reportes). Definido por Johann el 6-ago.
|
||||
8. Para proyectar el costo final del proyecto, las horas no comprimibles (sesiones con Balam, pruebas acompañadas, capacitación, demos, gestión y comunicación) son el piso, y el objetivo de cierre es ese piso y un poco más — no el piso pelón ni el rango completo de la propuesta. Los cortes ya reportados a Erika nunca se recalculan retroactivamente (ajustado por Johann el 6-ago).
|
||||
|
||||
## Corte al 2-oct (reconstruido, por validar)
|
||||
|
||||
Del 16-sep al 2-oct no se capturó nada en el CSV. El 2-oct se agregaron **15.25 h**, en 21 filas con la nota `Corte 2-oct`:
|
||||
- 13.50 h `reconstruida`.
|
||||
- 0.50 h `validada`: Johann dio la duración el 2-oct.
|
||||
- 1.25 h `confirmada`: tiempo real de la sesión del 2-oct (solo lo que se cobra).
|
||||
|
||||
Las fuentes fueron:
|
||||
- los registros de las sesiones de Claude Code de esta máquina (marcas de tiempo de cada mensaje; las sesiones paralelas se unieron para no contar doble);
|
||||
- los commits de ambos repos;
|
||||
- los rangos horarios que anota la bitácora (#83–#90);
|
||||
- el informe de Read AI del 29-sep.
|
||||
|
||||
**Johann debe validar las horas `reconstruida`** antes de reportarlas. El corte del 16-sep (94.58 h) no se tocó.
|
||||
|
||||
| Etapa | Horas al corte | Rango | Avance | Estado |
|
||||
|---|---:|---:|---:|---|
|
||||
| Etapa 0 | **21.83 h** | 18–22 h | 100% | Cerrada y validada |
|
||||
| Etapa 1 | **32.50 h** | 32–39 h | 97% | Capa de escritura completa; Azure entregado, falta desplegar (75%); CI/CD 50% |
|
||||
| Etapa 2 (sin ajuste Jira) | **43.50 h** | 38–46 h | 97% | Cadena completa validada en real: Pedro timbró desde la plataforma el 2-oct ([#91](../bitacora/REGISTRO.md)); quedan ajustes de UI/UX (fila nueva) |
|
||||
| » Ajuste Jira → BIND | **10.50 h** | 7–10 h | 100% | 0.5 h arriba del tope: webhook en producción, vista previa automática y modal "Aceptar" |
|
||||
| Etapa 3 | **1.50 h** | 24–29 h | 13% | Solo cartera por moneda (25%), endurecimiento y runbook |
|
||||
| **Total** | **109.83 h** | | 81% | 55.00 h desde el 6-ago: 52.25 `reconstruida` + 2.25 `confirmada` + 0.50 `validada` |
|
||||
|
||||
Movimientos entre etapas en este corte (el total no cambia): 7.75 h de cotización contra BIND salen del ajuste Jira hacia la Etapa 2, y 1.75 h de documentación técnica salen de la gestión de la Etapa 1 hacia la Etapa 2 (ver abajo).
|
||||
|
||||
**De dónde salen las 15.25 h:**
|
||||
- **9/10-sep (3.25 h) — no entraron al corte del 15-sep.** El puente a la web app de BIND para la prefactura (`BindUiClient`, contrato de `AddInvoiceTransaction`, sonda op #12) se construyó en esta máquina y se quedó sin commit hasta el 29-sep (`4d6b3d8`). Por eso la reconstrucción del 15-sep, hecha en la otra máquina, no lo vio. Las dos sesiones con Pedro de esos días ya estaban en el CSV y no se duplican.
|
||||
- **29-sep (4.25 h):** la sesión de las 7:00 con Pedro (login web a BIND) y el cierre de huecos de la bitácora. Luego, en esta máquina, la primera prefactura real (folio 2467) y el piloto del webhook. Esas dos sesiones corrieron en paralelo y se cuentan una sola vez.
|
||||
- **30-sep (5.25 h), en la otra máquina:** el webhook pasa a producción, con la vista previa automática y el modal en vivo. Hubo una llamada de 15 min con Pedro a las 7:26. Después, el puente se encendió en producción con cuatro bugs corregidos y se construyó el timbrado.
|
||||
- **1-oct (0.25 h):** prueba completa con Balam, de unos 20 min según Johann. Llegó a prefactura; el timbrado no se intentó.
|
||||
- **2-oct (2.25 h):**
|
||||
- Prefacturas en lote (1.00 h).
|
||||
- Este corte en sí (0.75 h). La proyección de costos, la reclasificación y los ajustes posteriores del Excel (~0.75 h) **no se cobran** (decisión de Johann, 2-oct).
|
||||
- El manual de usuario en PDF (0.50 h, `manual/Manual-usuario-Balam.pdf`).
|
||||
- El acompañamiento por WhatsApp a Pedro mientras timbraba (3:28–3:46 pm) **no se cobra** (decisión de Johann, 2-oct).
|
||||
- **Reparto CSV → filas del Excel** (`generar_avance_2026_10_02.py`; el script se detiene si no cuadra):
|
||||
- E1-05 → seguimiento semanal
|
||||
- E2-03 → conversión cotización→factura
|
||||
- E2-04 → pruebas integrales + manual
|
||||
- AJ-01 → disparador → cotización automática
|
||||
- Además se movieron 0.25 h de "Creación de cotizaciones" a "Tipo de cambio" (el TC se resolvió el 12-ago dentro de ese bloque), para que ninguna fila cerrada quede con 0 h.
|
||||
|
||||
**Reclasificación del 2-oct (Johann):** las 7.75 h que el corte del 15-sep cargó al ajuste Jira (`AJ-01`) y que en realidad fueron cotización contra el API de BIND pasan a la Etapa 2 como `E2-02`, a la fila «Creación de cotizaciones». Son las sesiones acompañadas del 12-ago, las dos del 21-ago y las dos del 3-sep, el Excel de catálogos, el diagnóstico del 500 y la investigación hasta el folio 1026. Así quedan:
|
||||
|
||||
| Línea | Antes | Ahora | Rango |
|
||||
|---|---:|---:|---:|
|
||||
| Ajuste Jira → BIND | 18.25 h | **10.50 h** ($6,300) | 7–10 h |
|
||||
| Disparador → cotización automática | 8.00 h | **3.50 h** (webhook, vista previa, modal) | 3–4 h |
|
||||
| Trazabilidad | 4.75 h | **1.50 h** | 2–3 h |
|
||||
| Etapa 2 | 34.00 h | **41.75 h** | 38–46 h |
|
||||
| Creación de cotizaciones | 7.75 h | **15.50 h** | 5–6 h |
|
||||
|
||||
El total del proyecto no cambia. Lo que Balam aprobó y controla es el rango por etapa, y ninguna etapa queda rebasada. A nivel fila, «Creación de cotizaciones» y «Conversión cotización→factura» superan su estimado: el API de BIND no documentaba cómo cotizar ni permitía crear prefacturas.
|
||||
|
||||
**Fila de seguimiento y gestión (2-oct, Johann):** la fila «Demostración semanal (viernes) + reporte de horas» no tenía demostraciones: las sesiones con Pedro se cargan a las filas técnicas. Lo que tenía era gestión. Por eso:
|
||||
- Se renombra a «Seguimiento y gestión: reportes de horas, bitácora y comunicación con Balam».
|
||||
- Lo técnico se va a su fila (regla 7):
|
||||
- 20-ago: 1.00 h del registro #65 y el guion de la sesión del 21-ago pasan a «Creación de cotizaciones». La fila del CSV se dividió según los commits: 5d846ec 23:02 para el cruce del buzón; a03f81f, cabe0fe y 072a3cd para lo de cotización.
|
||||
- 29-sep: 0.75 h (registros #71–#83 e investigación del SDK de BIND para la prefactura) pasan a «Conversión».
|
||||
- La fila queda en **8.00 h**, unos 40 minutos por semana en 12 semanas:
|
||||
|
||||
| Qué | Horas | Fechas y evidencia |
|
||||
|---|---:|---|
|
||||
| Cortes de horas y Excel de avance | 3.75 | 15-jul 0.50 · 16-jul 1.00 (Excel y mensaje a Erika) · 21-jul 0.25 · 15-sep 0.75 · 16-sep 0.50 (Google Sheets) · 2-oct 0.75 |
|
||||
| Bitácora | 2.25 | 1-ago 0.50 (#59–#60, commit 2b862ee) · 11-ago 0.75 (#62–#63, commits f28fcc5 y a2f338f) · 20-ago 1.00 (cruce del buzón, commit 5d846ec) |
|
||||
| Comunicación con Balam | 2.00 | 28-jul 0.25 (Azure a Erika) · 29-jul 0.75 (envío de la v1.3) · 20-ago 0.50 (WhatsApp con Pedro) · 25-ago 0.50 (correo de Azure a Noé) |
|
||||
| **Total** | **8.00** | El estimado de 2–3 h de la propuesta era para demostraciones y reporte, no para la gestión de 12 semanas |
|
||||
|
||||
**Decisiones de Johann sobre el Excel (2-oct):**
|
||||
- **Fechas reales.** Inicio = primer día trabajado. Fin = día en que se terminó el entregable según la bitácora y los commits; en las tareas en curso, el último día trabajado.
|
||||
- **Orden.** Las filas van en orden cronológico dentro de cada etapa; el bloque del ajuste Jira → BIND sigue al frente de la Etapa 2.
|
||||
- **Avance de los encabezados.** Es el promedio de sus filas ponderado por las horas estimadas (regla 4). Antes se calculaba con horas consumidas entre horas estimadas.
|
||||
- **Al 100%.**
|
||||
- Capa de escritura, Disparador → cotización automática, Creación de cotizaciones, Pruebas integrales + manual y Demostración semanal (al día).
|
||||
- Conversión cotización→factura y Trazabilidad: se cerraron al 100% el 2-oct, cuando Pedro timbró desde la plataforma y completó todo el proceso. Los ajustes de UI/UX que quedan (botones «Ver en Jira» / «Ver en BIND» y las pantallas de Solicitudes, Cotizaciones y Facturas) no reabren esas filas: van en una fila nueva de la Etapa 2, «Ajustes de UI/UX de la emisión» (⚪ Próxima, 0%, 1–2 h estimadas), que deja la Etapa 2 en 97%.
|
||||
- Con alcance ajustado: Catálogo de clientes (Balam eliminó la lista blanca el 6-jul), Detalle de factura (el XML se consulta en BIND), Exportación (queda CSV) y Tipo de cambio (sale del catálogo de monedas de BIND; el proceso del DOF sale del alcance).
|
||||
- **Otros avances.**
|
||||
- Azure sube a 75%: Balam ya dio el acceso, aunque falta volver a pedir la contraseña.
|
||||
- CI/CD queda en 50%.
|
||||
- Cobranza queda en 25%: la cartera con vencidos existe desde el 1-ago.
|
||||
- Endurecimiento y documentación técnica se quedan como estaban.
|
||||
|
||||
**Sin registro de horas:**
|
||||
- Timbrado del 30-sep (#89): se dejó en 0.75 h (10:00–10:45 pm según el respaldo y el último commit). Johann no recuerda si siguió hasta las 12:50 am, como dice el título del registro.
|
||||
- Comunicación del 16 al 28-sep (convocatoria del 28-sep, credenciales en TXT, autorización escrita de Pedro para el webhook) y la fecha en que Balam dio el acceso a Azure.
|
||||
- Las duraciones salen de la actividad dentro de las sesiones de trabajo. Una pausa de más de 20 minutos corta el bloque, así que el tiempo de revisar o pensar fuera de la sesión no aparece (regla 7). Johann puede subirlas.
|
||||
|
||||
## Corte al 16-sep (reconstruido, por validar)
|
||||
|
||||
Del 10-ago al 15-sep no se capturó nada en el CSV; el 15-sep se reconstruyeron **39.25 h** (más 0.50 h del 16-sep: Excel publicado en Google Sheets) con evidencia fechada (commits de ambos repos, correos, informes de reunión de Read AI y mtimes de archivos) y quedaron marcadas `reconstruida` (salvo la sesión del 12-ago, `confirmada` por transcript). **Johann debe validarlas** (o ajustarlas) antes de reportarlas; el corte del 6-ago (54.83 h) no se tocó.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Plan-actividades-avance-2026-07-27.xlsx
|
||||
@@ -0,0 +1,622 @@
|
||||
"""Genera Avance-Balam-2026-10-02.xlsx a partir del corte del 16-sep.
|
||||
|
||||
Horas — corte del viernes 2-oct: todo lo nuevo en `Seguimiento-horas.csv`
|
||||
lleva la nota "Corte 2-oct":
|
||||
- 3.25 h del 9/10-sep que no entraron al corte del 15-sep: el puente a la
|
||||
web app de BIND para la prefactura vivía sin commit en esta máquina (se
|
||||
subió el 29-sep en 4d6b3d8); fuente: transcripts de Claude Code.
|
||||
- 29-sep: sesión con Pedro (login web a BIND), primera prefactura real
|
||||
(folio 2467) y webhook de Jira FAC (piloto con FAC-169).
|
||||
- 30-sep: webhook en producción, vista previa automática, modal en vivo,
|
||||
llamada con Pedro (7:26), puente encendido en producción con cuatro bugs
|
||||
corregidos y timbrado.
|
||||
- 1-oct: prueba completa con Balam (llegó a prefactura; timbrado sin probar).
|
||||
- 2-oct: prefacturas en lote, manual de usuario y este corte. Ese día Pedro
|
||||
timbró desde la plataforma (cadena completa con CFDI), con lo que se cierran
|
||||
«Conversión» y «Trazabilidad»; el acompañamiento por WhatsApp no se cobra, y del
|
||||
corte solo se cobran 0.75 h (no la proyección de costos ni la reclasificación).
|
||||
- La fila "Demostración semanal + reporte de horas" pasa a llamarse "Seguimiento y
|
||||
gestión"; lo técnico que traía (20-ago 1.00 h, 29-sep 0.75 h) va a su fila.
|
||||
- Hoja 2 "Desglose de horas": cada registro del CSV con fecha, etapa, actividad,
|
||||
horas, monto, estado y evidencia, más un resumen por etapa que cuadra con la hoja 1.
|
||||
Quedan ajustes de UI/UX de esas pantallas, como fila nueva de la Etapa 2.
|
||||
|
||||
Avance y fechas — decisiones de Johann del 2-oct:
|
||||
- Fin = día en que se terminó el entregable según bitácora y commits (las
|
||||
tareas en curso: último día trabajado). Los encabezados de etapa toman el
|
||||
primer Inicio y el último Fin de sus filas.
|
||||
- Dentro de cada etapa las filas van en orden cronológico (Inicio, luego Fin);
|
||||
las no iniciadas al final. El bloque del ajuste Jira → BIND se queda al
|
||||
frente de la Etapa 2 (decisión del 29-jul), con sus filas también en orden.
|
||||
- El avance de cada encabezado es el promedio de sus filas ponderado por las
|
||||
horas estimadas (regla 4: el avance mide entregable, no horas). La Etapa 2
|
||||
excluye las filas del ajuste, igual que sus horas.
|
||||
- Cierres con alcance ajustado: lista blanca (Balam la eliminó el 6-jul), XML
|
||||
(se consulta en BIND), XLSX (queda CSV) y tipo de cambio del DOF (se toma
|
||||
del catálogo de monedas de BIND).
|
||||
- Estatus simples: una línea de estado y viñetas cortas en cada fila; sin
|
||||
bloque de notas debajo de la tabla (el detalle vive en Seguimiento-horas.md).
|
||||
El corte del 16-sep no se toca.
|
||||
"""
|
||||
import csv
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(BASE, "Avance-Balam-2026-09-16.xlsx")
|
||||
DST = os.path.join(BASE, "Avance-Balam-2026-10-02.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"
|
||||
|
||||
C_NOM, C_INI, C_FIN, C_PCT, C_EST, C_TRAB, C_STAT = 1, 2, 3, 4, 5, 6, 7
|
||||
CORTE_ANTERIOR = 94.58 # corte del 16-sep
|
||||
MARCA = "Corte 2-oct" # nota de las filas nuevas del CSV (incluye fechas del 9/10-sep)
|
||||
SIN = "—"
|
||||
|
||||
# --- CSV: totales por etapa y por actividad -----------------------------------
|
||||
por_act, por_etapa, nuevas_act = defaultdict(float), defaultdict(float), defaultdict(float)
|
||||
nuevas, desde_6ago = 0.0, 0.0
|
||||
with open(CSV_HORAS, encoding="utf-8", newline="") as fh:
|
||||
for r in csv.DictReader(fh):
|
||||
if not (r.get("fecha") or "").strip():
|
||||
continue
|
||||
h = float(r["horas"])
|
||||
por_act[r["actividad_id"]] += h
|
||||
por_etapa[r["etapa"]] += h
|
||||
if MARCA in r["notas"]:
|
||||
nuevas += h
|
||||
nuevas_act[r["actividad_id"]] += h
|
||||
if r["fecha"] > "2026-08-06":
|
||||
desde_6ago += h
|
||||
|
||||
E0, E1, E3 = por_etapa["0"], por_etapa["1"], por_etapa["3"]
|
||||
AJ = por_act["AJ-01"]
|
||||
E2 = por_etapa["2"] - AJ
|
||||
TOTAL = E0 + E1 + E2 + AJ + E3
|
||||
assert abs(TOTAL - CORTE_ANTERIOR - nuevas) < 0.01, "El CSV no arranca del corte del 16-sep"
|
||||
|
||||
# Reparto CSV → filas del Excel (mismo criterio que el corte del 15-sep):
|
||||
FILA_POR_ACT = {
|
||||
"E1-05": "Demostración semanal", # gestión, bitácora, cortes
|
||||
"E2-03": "Conversión cotización→factura", # prefactura, timbrado, lote
|
||||
"E2-04": "Pruebas integrales", # prueba completa del 1-oct + manual de usuario
|
||||
"AJ-01": "Disparador por estatus", # webhook, vista previa, modal
|
||||
}
|
||||
sin_fila = set(nuevas_act) - set(FILA_POR_ACT)
|
||||
assert not sin_fila, f"Actividades nuevas sin fila en el Excel: {sin_fila}"
|
||||
DELTAS_TRAB = defaultdict(float)
|
||||
for a, h in nuevas_act.items():
|
||||
DELTAS_TRAB[FILA_POR_ACT[a]] += h
|
||||
# Reasignación entre filas (suma cero): el tipo de cambio se resolvió el 12-ago dentro del
|
||||
# bloque de cotización (CSV 2026-08-12, E2-02: "tipo de cambio, prefill y selector de cliente").
|
||||
DELTAS_TRAB["Creación de cotizaciones desde la plataforma"] -= 0.25
|
||||
DELTAS_TRAB["Tipo de cambio del DOF"] += 0.25
|
||||
# Reclasificación del 2-oct (decisión de Johann, suma cero): las 7.75 h de cotización contra el API
|
||||
# de BIND que el corte del 15-sep cargó al ajuste Jira pasan a la Etapa 2 (en el CSV, AJ-01 → E2-02).
|
||||
# Disparador −4.50: sondeo de FAC-106 y diagnóstico del 500 (20-ago 1.50), investigación hasta el folio 1026 (21-ago 3.00)
|
||||
# Trazabilidad −3.25: sesión 12-ago 1.00, Excel de catálogos 0.75, sesiones 21-ago 0.75 y 3-sep 0.75
|
||||
DELTAS_TRAB["Disparador por estatus"] -= 4.50
|
||||
DELTAS_TRAB["Trazabilidad ticket"] -= 3.25
|
||||
DELTAS_TRAB["Creación de cotizaciones desde la plataforma"] += 7.75
|
||||
# 20-ago: de las 2.00 h de "bitácora", 1.00 h fue el registro #65 de la sesión de cotización y el guion del 21-ago
|
||||
# (commits a03f81f, cabe0fe, 072a3cd) → se dividió en el CSV (E1-05 1.00 + E2-02 1.00) y aquí sigue a su fila.
|
||||
DELTAS_TRAB["Demostración semanal"] -= 1.00
|
||||
DELTAS_TRAB["Creación de cotizaciones desde la plataforma"] += 1.00
|
||||
assert abs(sum(DELTAS_TRAB.values()) - nuevas) < 0.01, (
|
||||
f"El reparto ({sum(DELTAS_TRAB.values()):.2f}) no suma las {nuevas:.2f} h nuevas del CSV")
|
||||
|
||||
HORAS_ENCABEZADO = {
|
||||
"Integracion de plataformas Balam": TOTAL,
|
||||
"Etapa 0": E0,
|
||||
"Etapa 1": E1,
|
||||
"Etapa 2": E2,
|
||||
"» Ajuste dentro de la Etapa 2": AJ,
|
||||
"Etapa 3": E3,
|
||||
}
|
||||
ETAPAS = ("Etapa 0", "Etapa 1", "Etapa 2", "Etapa 3")
|
||||
AJUSTE = "» Ajuste dentro de la Etapa 2"
|
||||
AJUSTE_HIJOS = ("Cliente de la API de Jira", "Disparador por estatus", "Trazabilidad ticket")
|
||||
|
||||
# --- Fechas reales de las filas que cambian (dd/mm/yyyy) -----------------------
|
||||
FECHAS_REALES = {
|
||||
"Demostración semanal": ("15/07/2026", "02/10/2026"), # corte del 2-oct
|
||||
"Capa de escritura controlada": ("19/07/2026", "30/09/2026"), # diseño 19-jul → timbrado 30-sep
|
||||
"Disparador por estatus": ("29/09/2026", "30/09/2026"), # webhook (29-sep) → vista previa y modal (30-sep)
|
||||
"Trazabilidad ticket": ("10/08/2026", "02/10/2026"), # definiciones 10-ago → Pedro completa la cadena con CFDI 2-oct
|
||||
"Creación de cotizaciones desde la plataforma": ("06/08/2026", "30/09/2026"), # total desde BIND corregido 30-sep
|
||||
"Conversión cotización→factura": ("06/08/2026", "02/10/2026"), # puente 9/10-sep → lote 2-oct
|
||||
"Tipo de cambio del DOF": ("12/08/2026", "12/08/2026"), # TC del catálogo de monedas de BIND
|
||||
"Pruebas integrales": ("30/09/2026", "02/10/2026"), # recorrido e2e 30-sep, Balam 1-oct, manual 2-oct
|
||||
"Módulo de cobranza": ("21/07/2026", "01/08/2026"), # cartera por moneda (B4) → vista de vencidas
|
||||
}
|
||||
|
||||
# --- Avance de las filas que cambian -----------------------------------------
|
||||
AVANCE = {
|
||||
"Demostración semanal": 1.0,
|
||||
"Capa de escritura controlada": 1.0,
|
||||
"Configuración de infraestructura en Azure": 0.75,
|
||||
"CI/CD": 0.5,
|
||||
"Definición de alcance Jira": 1.0, # decía "falta el estatus disparador", viejo desde el 7-ago (REGISTRO #62)
|
||||
"Disparador por estatus": 1.0,
|
||||
"Trazabilidad ticket": 1.0, # 2-oct: Pedro timbró desde la plataforma, proceso completo
|
||||
"Detalle de factura": 1.0,
|
||||
"Catálogo de clientes": 1.0,
|
||||
"Tipo de cambio del DOF": 1.0,
|
||||
"Creación de cotizaciones desde la plataforma": 1.0,
|
||||
"Conversión cotización→factura": 1.0, # 2-oct: primer CFDI timbrado desde la plataforma
|
||||
"Exportación de facturas": 1.0,
|
||||
"Pruebas integrales": 1.0,
|
||||
"Ajustes de UI/UX de la emisión": 0.0,
|
||||
"Módulo de cobranza": 0.25,
|
||||
}
|
||||
|
||||
# --- Estatus: una línea de estado y viñetas cortas (todas las filas de detalle) --
|
||||
OK, CURSO, PROX = "✅ Completada", "🟡 En curso", "⚪ Próxima"
|
||||
ESTATUS = {
|
||||
# Etapa 0
|
||||
"Sesión de arranque (kickoff)": [OK],
|
||||
"Entrega y validación de accesos": [OK],
|
||||
"Validación técnica de la API de BIND": [OK],
|
||||
"Sesión de Discovery": [OK],
|
||||
"Prototipo visual navegable": [OK],
|
||||
"Documento de hallazgos": [OK],
|
||||
"Sesión de validación del prototipo": ["✅ Aprobado por Balam (24-jul)"],
|
||||
# Etapa 1
|
||||
"Repositorio privado GitHub": [OK],
|
||||
"Backend base": [OK],
|
||||
"Arquitectura multi-tenant": [OK],
|
||||
"Cliente de la API de BIND": [OK],
|
||||
"Demostración semanal": ["✅ Al día",
|
||||
"Cortes de horas y Excel de avance (último: 2-oct)",
|
||||
"Bitácora y comunicación con Balam",
|
||||
"Las sesiones con Pedro van en las filas técnicas"],
|
||||
"Sincronización de clientes": [OK],
|
||||
"Capa de escritura controlada": [OK,
|
||||
"Vista previa + confirmación humana + auditoría",
|
||||
"Cotización (21-ago), prefactura (29-sep) y timbrado (30-sep)",
|
||||
"Timbrado validado en real por Balam (2-oct)"],
|
||||
"Sesión de reglas y dudas": [OK],
|
||||
"Sincronización de facturas": [OK],
|
||||
"Modelo de facturas": [OK],
|
||||
"Definición de alcance Jira": [OK,
|
||||
"v1.3 aprobada por Noé (30-jul)",
|
||||
"Disparador y campos definidos por Balam (7-ago)"],
|
||||
"Configuración de infraestructura en Azure": [CURSO,
|
||||
"Balam ya dio el acceso",
|
||||
"Falta: volver a pedir la contraseña y desplegar",
|
||||
"Mientras tanto corre en paputec.mx (staging)"],
|
||||
"CI/CD": [CURSO,
|
||||
"Despliegue reproducible con respaldo y rollback",
|
||||
"Secretos fuera del repositorio",
|
||||
"Falta: el pipeline en Azure"],
|
||||
# Etapa 2 · ajuste Jira → BIND
|
||||
"Cliente de la API de Jira": [OK,
|
||||
"Bandeja y detalle de tickets FAC en vivo",
|
||||
"Adjuntos descargables"],
|
||||
"Trazabilidad ticket": [OK,
|
||||
"Cadena ticket → cotización → prefactura → CFDI visible",
|
||||
"Pedro completó el proceso con timbrado (2-oct)",
|
||||
"El tablero se alimenta en la Etapa 3"],
|
||||
"Disparador por estatus": [OK,
|
||||
"Webhook de Jira en producción (30-sep)",
|
||||
"Ticket detectado en ~1 s, cotización lista para aceptar",
|
||||
"De Balam: 7 categorías sin ítem en BIND"],
|
||||
# Etapa 2
|
||||
"Andamiaje del frontend": [OK],
|
||||
"Listado de facturas": [OK],
|
||||
"Detalle de factura": [OK,
|
||||
"Descarga del PDF",
|
||||
"El XML se consulta en BIND"],
|
||||
"Catálogo de clientes": [OK,
|
||||
"Catálogo y búsqueda",
|
||||
"Lista blanca eliminada por Balam (6-jul)"],
|
||||
"Exportación de facturas": [OK,
|
||||
"Facturas y clientes a CSV (abre en Excel)"],
|
||||
"Layout base": [OK],
|
||||
"Creación de cotizaciones desde la plataforma": [OK,
|
||||
"Individual y en lote",
|
||||
"Vista previa + confirmación humana"],
|
||||
"Conversión cotización→factura": [OK,
|
||||
"Prefactura validada con Balam (1-oct)",
|
||||
"Prefacturas en lote (2-oct)",
|
||||
"Pedro timbró desde la plataforma (2-oct)"],
|
||||
"Tipo de cambio del DOF": [OK,
|
||||
"Se toma del catálogo de monedas de BIND"],
|
||||
"Pruebas integrales": [OK,
|
||||
"Prueba completa con Balam (1-oct)",
|
||||
"Timbrado real desde la plataforma (2-oct)",
|
||||
"213 pruebas automáticas",
|
||||
"Manual de usuario en PDF (2-oct)"],
|
||||
# Etapa 3
|
||||
"Módulo de cobranza": [CURSO,
|
||||
"Cartera por moneda con vencidos",
|
||||
"Falta: rangos 30/60/90 y vista por vencer"],
|
||||
"Endurecimiento de seguridad": [CURSO,
|
||||
"Rate limit y bloqueo de login",
|
||||
"CSP/HSTS y respaldos diarios"],
|
||||
"Documentación técnica": [CURSO,
|
||||
"Runbook de publicación, operación y rollback"],
|
||||
"Alertas internas": [PROX],
|
||||
"Tablero directivo": [PROX],
|
||||
"Reporte operativo": [PROX],
|
||||
"Sesión de capacitación": [PROX],
|
||||
"Soporte posterior": ["⚪ Incluido en el cierre, sin costo"],
|
||||
"Ajustes de UI/UX de la emisión": [PROX,
|
||||
"Botones «Ver en Jira» / «Ver en BIND»",
|
||||
"Pantallas de Solicitudes, Cotizaciones y Facturas"],
|
||||
}
|
||||
ANCHO_ESTATUS = 46 # ancho de la columna G (caracteres)
|
||||
|
||||
|
||||
def texto_estatus(lineas):
|
||||
return "\n".join([lineas[0]] + [f"• {l}" for l in lineas[1:]])
|
||||
|
||||
|
||||
def alto_fila(*textos_y_anchos):
|
||||
"""Alto en puntos para que quepa el texto envuelto (fuente 9-10 pt)."""
|
||||
renglones = 1
|
||||
for texto, ancho in textos_y_anchos:
|
||||
n = sum(max(1, -(-len(l) // max(1, int(ancho * 1.05)))) for l in str(texto or "").split("\n"))
|
||||
renglones = max(renglones, n)
|
||||
return max(15, 12 * renglones + 4)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def fecha(v):
|
||||
return datetime.strptime(v, "%d/%m/%Y") if isinstance(v, str) and v.count("/") == 2 else None
|
||||
|
||||
|
||||
def estimadas(v):
|
||||
"""Punto medio de 'Horas estimadas' ('2', '4–5', '1.5–2'); 0 si no aplica ('—')."""
|
||||
if isinstance(v, (int, float)):
|
||||
return float(v)
|
||||
partes = [p for p in str(v or "").replace(" ", "").split("–") if p]
|
||||
try:
|
||||
nums = [float(p) for p in partes]
|
||||
except ValueError:
|
||||
return 0.0
|
||||
return sum(nums) / len(nums) if nums else 0.0
|
||||
|
||||
|
||||
wb = openpyxl.load_workbook(SRC)
|
||||
ws = wb.active
|
||||
|
||||
ws["A2"] = ("Plan y avance al 2-oct-2026 · webhook de Jira FAC en producción con vista previa y modal 'Aceptar', "
|
||||
"prefactura por la web app de BIND, timbrado desde la plataforma y prefacturas en lote · horas del "
|
||||
"10-ago al 2-oct reconstruidas, pendientes de validar")
|
||||
ws["A2"].font = Font(size=10, italic=True, color=GRIS)
|
||||
|
||||
# Notas del corte anterior: se limpian y se reescriben abajo.
|
||||
primera_nota = None
|
||||
for row in range(5, ws.max_row + 1):
|
||||
v = str(ws.cell(row=row, column=C_NOM).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=C_NOM).value = None
|
||||
tope = primera_nota if primera_nota else ws.max_row + 1
|
||||
|
||||
# Fila nueva (Johann, 2-oct): ajustes de UI/UX pendientes de la Etapa 2, al final de la etapa. Toma el
|
||||
# formato de «Pruebas integrales», que en el corte del 16-sep era una fila ⚪ sin iniciar.
|
||||
UIUX = "Ajustes de UI/UX de la emisión: «Ver en Jira» / «Ver en BIND» y pantallas de Solicitudes, Cotizaciones y Facturas"
|
||||
assert not any(str(ws.cell(row=r, column=C_NOM).value or "").startswith("Ajustes de UI/UX") for r in range(5, tope))
|
||||
modelo = next(r for r in range(5, tope) if str(ws.cell(row=r, column=C_NOM).value or "").startswith("Pruebas integrales"))
|
||||
nueva = next(r for r in range(modelo + 1, tope) if str(ws.cell(row=r, column=C_NOM).value or "").startswith("Etapa 3"))
|
||||
ws.insert_rows(nueva)
|
||||
for c, valor in enumerate((UIUX, SIN, SIN, 0, "1–2", SIN, None), start=1):
|
||||
cel = ws.cell(row=nueva, column=c, value=valor)
|
||||
cel._style = copy(ws.cell(row=modelo, column=c)._style)
|
||||
tope += 1
|
||||
filas_tabla = [r for r in range(5, tope) if str(ws.cell(row=r, column=C_NOM).value or "").strip()]
|
||||
assert not any(rg.min_row in filas_tabla for rg in ws.merged_cells.ranges), "Hay celdas combinadas en la tabla"
|
||||
|
||||
|
||||
def nombre_de(row):
|
||||
return str(ws.cell(row=row, column=C_NOM).value or "").strip()
|
||||
|
||||
|
||||
def encabezado_de(nombre):
|
||||
return next((p for p in HORAS_ENCABEZADO if nombre.startswith(p)), None)
|
||||
|
||||
|
||||
# --- 1. Valores de las filas de detalle ----------------------------------------
|
||||
aplicado = defaultdict(set)
|
||||
for row in filas_tabla:
|
||||
nombre = nombre_de(row)
|
||||
if nombre.startswith(AJUSTE) and "(en aprobación, v1.3)" in nombre:
|
||||
ws.cell(row=row, column=C_NOM, value=nombre.replace("(en aprobación, v1.3)", "(aprobado en la v1.3, 30-jul)"))
|
||||
if encabezado_de(nombre):
|
||||
continue
|
||||
for prefijo, (ini, fin) in FECHAS_REALES.items():
|
||||
if nombre.startswith(prefijo):
|
||||
ws.cell(row=row, column=C_INI, value=ini)
|
||||
ws.cell(row=row, column=C_FIN, value=fin)
|
||||
aplicado["fechas"].add(prefijo)
|
||||
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["horas"].add(prefijo)
|
||||
for prefijo, pct in AVANCE.items():
|
||||
if nombre.startswith(prefijo):
|
||||
p = ws.cell(row=row, column=C_PCT, value=pct)
|
||||
p.number_format = "0%"
|
||||
pinta(p, V_FILL if pct >= 1 else A_FILL, V_FONT if pct >= 1 else A_FONT, bold=True, size=10,
|
||||
align="center")
|
||||
aplicado["avance"].add(prefijo)
|
||||
for prefijo, lineas in ESTATUS.items():
|
||||
if nombre.startswith(prefijo):
|
||||
cel = ws.cell(row=row, column=C_STAT, value=texto_estatus(lineas))
|
||||
if lineas[0].startswith("✅"):
|
||||
pinta(cel, V_FILL, V_FONT)
|
||||
elif lineas[0].startswith("🟡"):
|
||||
pinta(cel, A_FILL, A_FONT)
|
||||
else: # ⚪ conserva su formato; solo envuelve el texto
|
||||
cel.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
|
||||
ws.row_dimensions[row].height = alto_fila((nombre, 56), (cel.value, ANCHO_ESTATUS))
|
||||
aplicado["estatus"].add(prefijo)
|
||||
break
|
||||
for col in (C_INI, C_FIN):
|
||||
cel = ws.cell(row=row, column=col)
|
||||
cel.alignment = Alignment(horizontal="center", vertical="center")
|
||||
cel.font = Font(size=9, color=GRIS) if cel.value == SIN else Font(size=9)
|
||||
|
||||
sin_estatus = [nombre_de(r) for r in filas_tabla if not encabezado_de(nombre_de(r))
|
||||
and not any(nombre_de(r).startswith(p) for p in ESTATUS)]
|
||||
assert not sin_estatus, f"Filas sin estatus: {sin_estatus}"
|
||||
for clave, esperado in (("horas", DELTAS_TRAB), ("fechas", FECHAS_REALES), ("avance", AVANCE), ("estatus", ESTATUS)):
|
||||
faltan = set(esperado) - aplicado[clave]
|
||||
assert not faltan, f"Filas no encontradas en el Excel ({clave}): {faltan}"
|
||||
|
||||
# La fila "Demostración semanal (viernes) + reporte de horas" no tiene demostraciones: las sesiones con Pedro
|
||||
# están en las filas técnicas. Se renombra para que diga lo que contiene (Johann, 2-oct).
|
||||
GESTION = "Seguimiento y gestión: reportes de horas, bitácora y comunicación con Balam"
|
||||
renombradas = 0
|
||||
for row in filas_tabla:
|
||||
if nombre_de(row).startswith("Demostración semanal"):
|
||||
ws.cell(row=row, column=C_NOM, value=GESTION)
|
||||
renombradas += 1
|
||||
assert renombradas == 1
|
||||
|
||||
# --- 2. Grupos: etapa → (bloque del ajuste) + filas ----------------------------
|
||||
grupos = [] # [(fila_encabezado, [filas]), ...]; el ajuste es su propio grupo dentro de la Etapa 2
|
||||
for row in filas_tabla:
|
||||
enc = encabezado_de(nombre_de(row))
|
||||
if enc:
|
||||
grupos.append((row, []))
|
||||
else:
|
||||
grupos[-1][1].append(row)
|
||||
# Las filas del ajuste aparecen justo después de su encabezado; las que siguen son de la Etapa 2.
|
||||
for i, (enc_row, hijos) in enumerate(grupos):
|
||||
if nombre_de(enc_row).startswith(AJUSTE):
|
||||
propios = [r for r in hijos if nombre_de(r).startswith(AJUSTE_HIJOS)]
|
||||
resto = [r for r in hijos if r not in propios]
|
||||
assert len(propios) == len(AJUSTE_HIJOS), "No se encontraron las filas del ajuste"
|
||||
grupos[i] = (enc_row, propios)
|
||||
grupos.insert(i + 1, ("etapa2-resto", resto))
|
||||
break
|
||||
|
||||
|
||||
def clave_cronologica(row):
|
||||
ini, fin = fecha(ws.cell(row=row, column=C_INI).value), fecha(ws.cell(row=row, column=C_FIN).value)
|
||||
return (ini is None, ini or datetime.max, fin or datetime.max, row)
|
||||
|
||||
|
||||
def promedio(filas):
|
||||
pesos = [(estimadas(ws.cell(row=r, column=C_EST).value), ws.cell(row=r, column=C_PCT).value or 0) for r in filas]
|
||||
total = sum(p for p, _ in pesos)
|
||||
return round(sum(p * float(v) for p, v in pesos) / total, 2) if total else 0.0
|
||||
|
||||
|
||||
def rango_fechas(filas):
|
||||
inis = [fecha(ws.cell(row=r, column=C_INI).value) for r in filas]
|
||||
fins = [fecha(ws.cell(row=r, column=C_FIN).value) for r in filas]
|
||||
inis, fins = [d for d in inis if d], [d for d in fins if d]
|
||||
return (min(inis).strftime("%d/%m/%Y") if inis else SIN, max(fins).strftime("%d/%m/%Y") if fins else SIN)
|
||||
|
||||
|
||||
# --- 3. Encabezados: horas del CSV, avance ponderado y fechas de sus filas ------
|
||||
hojas = {} # encabezado → filas de detalle que lo componen (para avance y fechas)
|
||||
etapa_actual = None
|
||||
for enc_row, hijos in grupos:
|
||||
if enc_row == "etapa2-resto":
|
||||
hojas[etapa_actual]["avance"] += hijos
|
||||
hojas[etapa_actual]["fechas"] += hijos
|
||||
continue
|
||||
enc = encabezado_de(nombre_de(enc_row))
|
||||
hojas[enc] = {"row": enc_row, "avance": list(hijos), "fechas": list(hijos)}
|
||||
if enc in ETAPAS:
|
||||
etapa_actual = enc
|
||||
elif enc == AJUSTE:
|
||||
hojas[etapa_actual]["fechas"] += hijos # el ajuste cuenta para las fechas de la Etapa 2, no para su avance
|
||||
todas = [r for g in hojas.values() for r in g["avance"]]
|
||||
hojas["Integracion de plataformas Balam"]["avance"] = todas
|
||||
hojas["Integracion de plataformas Balam"]["fechas"] = todas
|
||||
|
||||
for enc, g in hojas.items():
|
||||
row, pct = g["row"], promedio(g["avance"])
|
||||
ini, fin = rango_fechas(g["fechas"])
|
||||
ws.cell(row=row, column=C_INI, value=ini)
|
||||
ws.cell(row=row, column=C_FIN, value=fin)
|
||||
c = ws.cell(row=row, column=C_TRAB, value=round(HORAS_ENCABEZADO[enc], 2))
|
||||
c.number_format = "0.00"
|
||||
p = ws.cell(row=row, column=C_PCT, value=pct)
|
||||
p.number_format = "0%"
|
||||
pinta(p, V_FILL if pct >= 1 else A_FILL, V_FONT if pct >= 1 else A_FONT, bold=True, size=10, align="center")
|
||||
g["pct"] = pct
|
||||
|
||||
# --- 4. Orden cronológico dentro de cada grupo ----------------------------------
|
||||
orden = []
|
||||
for enc_row, hijos in grupos:
|
||||
if enc_row != "etapa2-resto":
|
||||
orden.append(enc_row)
|
||||
orden += sorted(hijos, key=clave_cronologica)
|
||||
assert sorted(orden) == filas_tabla
|
||||
|
||||
foto = {r: ([(ws.cell(row=r, column=c).value, copy(ws.cell(row=r, column=c)._style)) for c in range(1, 8)],
|
||||
ws.row_dimensions[r].height) for r in filas_tabla}
|
||||
for destino, origen in zip(filas_tabla, orden):
|
||||
celdas, alto = foto[origen]
|
||||
for c, (valor, estilo) in enumerate(celdas, start=1):
|
||||
cel = ws.cell(row=destino, column=c)
|
||||
cel.value = valor
|
||||
cel._style = copy(estilo)
|
||||
ws.row_dimensions[destino].height = alto
|
||||
|
||||
# --- 5. Cuadre: las filas de detalle suman el total del CSV ----------------------
|
||||
detalle = 0.0
|
||||
for row in filas_tabla:
|
||||
v = ws.cell(row=row, column=C_TRAB).value
|
||||
if not encabezado_de(nombre_de(row)) and isinstance(v, (int, float)):
|
||||
detalle += float(v)
|
||||
assert abs(detalle - TOTAL) < 0.01, f"Las filas suman {detalle:.2f} h y el CSV {TOTAL:.2f} h"
|
||||
|
||||
PCT = {enc: g["pct"] for enc, g in hojas.items()}
|
||||
# Sin notas debajo de la tabla (Johann, 2-oct): el estatus de cada fila lo dice todo.
|
||||
for r in range(tope, ws.max_row + 1):
|
||||
ws.row_dimensions[r].height = None
|
||||
ws.column_dimensions["G"].width = ANCHO_ESTATUS
|
||||
r = filas_tabla[-1]
|
||||
|
||||
ws.print_area = f"A1:G{r}"
|
||||
|
||||
# --- Hoja 2: desglose de horas, registro por registro (Johann, 2-oct: "quiero poder justificar todo") ----
|
||||
TARIFA = 600
|
||||
AMARILLO, CREMA = "F7BD0C", "FDF3D8"
|
||||
ACTIVIDADES = {
|
||||
"E0-01": "Kickoff y preparación", "E0-02": "Discovery de facturación y cobranza",
|
||||
"E0-03": "Accesos y salvaguardas", "E0-04": "Validación técnica del API de BIND",
|
||||
"E0-05": "Prototipo navegable", "E0-06": "Entregable de cierre de la Etapa 0",
|
||||
"E0-07": "Validación del prototipo con Balam",
|
||||
"E1-01": "Backend base, autenticación, roles y sincronización", "E1-02": "Repositorio",
|
||||
"E1-03": "Arquitectura multi-tenant y RLS", "E1-04": "Cliente del API de BIND",
|
||||
"E1-05": "Seguimiento y gestión", "E1-06": "Entorno local y andamiaje",
|
||||
"E1-07": "Sesión del API de Jira", "E1-08": "Diseño y planeación",
|
||||
"E1-09": "Publicación provisional (paputec.mx)",
|
||||
"E2-01": "Pantallas de consulta", "E2-02": "Creación de cotizaciones",
|
||||
"E2-03": "Prefactura y timbrado", "E2-04": "Pruebas integrales y manual",
|
||||
"AJ-01": "Integración Jira → BIND",
|
||||
"E3-01": "Endurecimiento de seguridad", "E3-02": "Documentación técnica",
|
||||
}
|
||||
ESTADOS = {"confirmada": "Documentada", "validada": "Validada", "reconstruida": "Reconstruida"}
|
||||
ETAPA_NOMBRE = {"0": "Etapa 0", "1": "Etapa 1", "2": "Etapa 2", "3": "Etapa 3"}
|
||||
|
||||
with open(CSV_HORAS, encoding="utf-8", newline="") as fh:
|
||||
registros = [x for x in csv.DictReader(fh) if (x.get("fecha") or "").strip()]
|
||||
sin_nombre = {x["actividad_id"] for x in registros} - set(ACTIVIDADES)
|
||||
assert not sin_nombre, f"Actividades sin nombre en el desglose: {sin_nombre}"
|
||||
|
||||
|
||||
def etapa_de(x):
|
||||
return "Ajuste Jira → BIND" if x["actividad_id"] == "AJ-01" else ETAPA_NOMBRE[x["etapa"]]
|
||||
|
||||
|
||||
if "Desglose de horas" in wb.sheetnames:
|
||||
del wb["Desglose de horas"]
|
||||
wd = wb.create_sheet("Desglose de horas")
|
||||
wd.sheet_view.showGridLines = False
|
||||
for col, ancho in zip("ABCDEFGH", (11, 18, 30, 72, 8, 12, 14, 46)):
|
||||
wd.column_dimensions[col].width = ancho
|
||||
|
||||
wd["A1"] = "Proyecto Balam · Desglose de horas trabajadas"
|
||||
wd["A1"].font = Font(size=14, bold=True, color=CAFE)
|
||||
wd["A2"] = (f"Cada registro con su fecha, actividad y evidencia · tarifa ${TARIFA}/h + IVA · corte al 2-oct-2026 · "
|
||||
"Documentada = duración comprobada (transcript o sesión) · Validada = confirmada por Johann · "
|
||||
"Reconstruida = armada con evidencia fechada, pendiente de validar")
|
||||
wd["A2"].font = Font(size=9, italic=True, color=GRIS)
|
||||
wd["A2"].alignment = Alignment(wrap_text=True, vertical="top")
|
||||
wd.merge_cells("A2:H2")
|
||||
wd.row_dimensions[2].height = 28
|
||||
|
||||
borde = Border(bottom=Side(style="thin", color="E4E2DC"))
|
||||
|
||||
|
||||
def encabezado(fila, textos):
|
||||
for c, t in enumerate(textos, start=1):
|
||||
cel = wd.cell(row=fila, column=c, value=t)
|
||||
cel.font = Font(size=10, bold=True, color=CAFE)
|
||||
cel.fill = PatternFill("solid", fgColor=AMARILLO)
|
||||
cel.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
wd.row_dimensions[fila].height = 20
|
||||
|
||||
|
||||
# Resumen por etapa (cuadra con la hoja principal)
|
||||
encabezado(4, ["Etapa", "", "Registros", "", "Horas", "Monto (MXN)", "", ""])
|
||||
orden_etapas = ["Etapa 0", "Etapa 1", "Etapa 2", "Ajuste Jira → BIND", "Etapa 3"]
|
||||
esperado = {"Etapa 0": E0, "Etapa 1": E1, "Etapa 2": E2, "Ajuste Jira → BIND": AJ, "Etapa 3": E3}
|
||||
fila = 5
|
||||
for et in orden_etapas:
|
||||
regs = [x for x in registros if etapa_de(x) == et]
|
||||
horas = sum(float(x["horas"]) for x in regs)
|
||||
assert abs(horas - esperado[et]) < 0.01, f"{et}: desglose {horas:.2f} h vs hoja principal {esperado[et]:.2f} h"
|
||||
for c, v in ((1, et), (3, len(regs)), (5, round(horas, 2)), (6, round(horas * TARIFA, 2))):
|
||||
cel = wd.cell(row=fila, column=c, value=v)
|
||||
cel.font = Font(size=10, color=CAFE)
|
||||
cel.border = borde
|
||||
wd.cell(row=fila, column=5).number_format = "0.00"
|
||||
wd.cell(row=fila, column=6).number_format = '"$"#,##0.00'
|
||||
fila += 1
|
||||
for c, v in ((1, "Total"), (3, len(registros)), (5, round(TOTAL, 2)), (6, round(TOTAL * TARIFA, 2))):
|
||||
cel = wd.cell(row=fila, column=c, value=v)
|
||||
cel.font = Font(size=10, bold=True, color=CAFE)
|
||||
cel.fill = PatternFill("solid", fgColor=CREMA)
|
||||
wd.cell(row=fila, column=5).number_format = "0.00"
|
||||
wd.cell(row=fila, column=6).number_format = '"$"#,##0.00'
|
||||
|
||||
# Detalle, en orden cronológico
|
||||
cab = fila + 3
|
||||
encabezado(cab, ["Fecha", "Etapa", "Actividad", "Qué se hizo", "Horas", "Monto (MXN)", "Estado", "Evidencia"])
|
||||
fila = cab + 1
|
||||
for x in registros:
|
||||
h = float(x["horas"])
|
||||
valores = (datetime.strptime(x["fecha"], "%Y-%m-%d"), etapa_de(x), ACTIVIDADES[x["actividad_id"]],
|
||||
x["actividad"], h, round(h * TARIFA, 2), ESTADOS.get(x["estado_hora"], x["estado_hora"]),
|
||||
x["fuente_evidencia"])
|
||||
for c, v in enumerate(valores, start=1):
|
||||
cel = wd.cell(row=fila, column=c, value=v)
|
||||
cel.font = Font(size=9, color=CAFE)
|
||||
cel.alignment = Alignment(vertical="top", wrap_text=True,
|
||||
horizontal="center" if c in (1, 7) else ("right" if c in (5, 6) else "left"))
|
||||
cel.border = borde
|
||||
wd.cell(row=fila, column=1).number_format = "dd/mm/yyyy"
|
||||
wd.cell(row=fila, column=5).number_format = "0.00"
|
||||
wd.cell(row=fila, column=6).number_format = '"$"#,##0.00'
|
||||
if x["estado_hora"] == "reconstruida":
|
||||
wd.cell(row=fila, column=7).font = Font(size=9, color=A_FONT)
|
||||
renglones = max(-(-len(x["actividad"]) // 80), -(-len(x["fuente_evidencia"]) // 50), -(-len(ACTIVIDADES[x["actividad_id"]]) // 32), 1)
|
||||
wd.row_dimensions[fila].height = 12 * renglones + 4
|
||||
fila += 1
|
||||
assert abs(sum(float(x["horas"]) for x in registros) - TOTAL) < 0.01
|
||||
|
||||
wd.freeze_panes = f"A{cab + 1}"
|
||||
wd.auto_filter.ref = f"A{cab}:H{fila - 1}"
|
||||
wd.page_setup.orientation = "landscape"
|
||||
wd.page_setup.fitToWidth = 1
|
||||
wd.page_setup.fitToHeight = 0
|
||||
wd.sheet_properties.pageSetUpPr.fitToPage = True
|
||||
wd.print_title_rows = f"{cab}:{cab}"
|
||||
wb.active = 0
|
||||
wb.save(DST)
|
||||
print("OK ->", DST)
|
||||
print(f" E0 {E0:.2f} · E1 {E1:.2f} · E2 {E2:.2f} · Jira {AJ:.2f} · E3 {E3:.2f} · TOTAL {TOTAL:.2f} h "
|
||||
f"({nuevas:.2f} nuevas)")
|
||||
print(" avance:", " · ".join(f"{k} {v:.0%}" for k, v in PCT.items()))
|
||||
print(" reparto:", ", ".join(f"{k} {v:+.2f}" for k, v in DELTAS_TRAB.items()))
|
||||
Reference in New Issue
Block a user