feat(ti/licenciamiento): tabla propia con CRUD; la hoja deja de ser la fuente
Licenciamiento leia la pestaña "Licencias" EN VIVO en cada request
(services/licencias_sheet.py, cache en memoria de 300s). No habia donde capturar:
toda alta, correccion o baja tenia que hacerse en el Google Sheet. Ahora la
fuente de verdad es ti_licencias y la captura ocurre en el portal.
Por que NO se registro en sheets_sync
La via "natural" habria sido agregar un parser a services/sheets_sync.py. Es la
peor opcion posible aqui: ese camino entra por _safe_replace, que hace DELETE +
INSERT de la tabla COMPLETA en cada corrida. Cualquier fila capturada o corregida
en el portal moriria en el siguiente sync, sin aviso. Esta tabla no se sincroniza
y el modelo lo deja escrito para que nadie lo "arregle" despues.
Esquema — UNA tabla, sin catalogos
Se sigue el patron de ti_doc_api: baja logica con motivo/quien/cuando,
auditoria, origen ('sheet'|'portal'), catalogos cerrados como constantes con
CHECK en BD, y los abiertos (software, tipo de licencia, servicio) por SELECT
DISTINCT de lo ya capturado. Cero tablas de catalogo: el esquema CRUD de
Proyectos TI llego a tener seis y se elimino entero en mayo de 2026; las ocho
tablas siguen residuales sin que nadie las consulte.
-
idES el Id de la hoja (1..1252). El area agrego esa columna justo para poder rastrear las filas, asi que se conserva en vez de generar ids nuevos. La carga reposiciona la secuencia; la primera alta del portal fue la 1253. - Sin clave natural: se permiten repetidos a proposito. Un usuario puede tener dos licencias del mismo software y tipo, y la hoja ya trae 21 combinaciones repetidas de software+correo.
-
estado(activa/inactiva) es del NEGOCIO y no es la baja logica. Una licencia inactiva sigue siendo un registro valido. Las 9 INACTIVA de la hoja entraron como activas en registro. - La empresa es el UNICO punto de cruce con el catalogo de /admin; portafolio y
estatus se derivan de ahi. La columna PORTAFOLIO de la hoja se IGNORA: traia la
taxonomia CCC (7 valores), que no es la misma que
empresas_grupo_caabsa.portafolio(agrupada por holding). Sostener las dos es como se contradicen dos pantallas del mismo portal.
Calidad del dato, visible en vez de silenciosa
campos_faltantes por fila y faltantes_por_campo en el resumen. empresa
encabeza la lista porque sin ella no hay cruce y la fila queda fuera de los
filtros mandatorios. La carga reporta lo mismo, agrupado, antes de escribir.
Carga inicial (local): 1252 filas, ids 1..1252, gasto MXN 2,480,465.41 / USD 128,933.00 -- coincide al centavo con lo medido leyendo la hoja directo, que es la comprobacion de que el parseo no deformo nada. Cruzan 1128; 124 quedan como texto y se corrigen con alias en /admin o editando en el portal.
scripts/import_licencias_sheet.py es de UN SOLO USO y se borra cuando los tres
entornos esten cargados, igual que el de Documentacion API.
Verificado: ci_validate_models.py OK (19 tablas en main) y ciclo CRUD completo
contra el backend -- alta con id 1253, edicion, baja logica con motivo, papelera
para gerente, restaurar. Un error corregido antes de cargar: resolve() del
matcher devuelve la clave como empresa_id, no id; con la primera version el
cruce habria quedado nulo en las 1252 filas sin que nada lo señalara.
NO se retira todavia la lectura en vivo del Sheet: la vista actual la sigue
consumiendo y quitarla ahora dejaria /ti/sistemas/licenciamiento rota. Se retira
en el commit que migre el frontend, junto con el codigo muerto de la hoja vieja
(models/licenciamiento.py, _parse_licenciamiento, sync_licenciamiento_only) y la
tabla licenciamiento de 110 filas.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com