Sistema centralizado para la gestión y aprobación de material IT asignado a usuarios. Aprobaciones auditables via Microsoft Approvals + Purview, portal web con SSO y sincronización desde listas SharePoint.
Las asignaciones de material IT viven en múltiples listas de SharePoint. No hay proceso automatizado que confirme con el usuario lo que tiene asignado, ni registro auditable de esa confirmación.
Power Automate + conector de Approvals. Aprobaciones gestionadas en la infraestructura Microsoft, visibles desde Purview y el centro de aprobaciones M365. No manipulables por la aplicación local.
Interfaz web con Entra ID App Proxy donde cada usuario consulta en cualquier momento el material asignado, agrupado por categoría, con el estado de su última aprobación.
Cuando se detecta que un activo ha sido desasignado, el usuario recibe un aviso consolidado con todo el material que ha dejado de tener asignado ese día. No requiere aprobación, solo notificación. Mecanismo a decidir (ver sección dedicada).
El scheduler sincroniza las listas de activos y la lista de log de aprobaciones desde SharePoint a SQLite local. Páginas instantáneas, sin latencia Graph API por carga.
Admin UI para asignar material desde la web, ciclo de vida de activos (devolución, baja), reporting y write-back a SharePoint. La base de datos local facilita todas estas extensiones.
La clave del diseño: el sync acumula novedades durante el día en un buffer local. Un job independiente dispara PA una única vez por usuario con todo lo acumulado — aunque los activos lleguen de distintas listas en distintos ciclos de sync.
Lee listas SP, detecta cambios. Acumula asignaciones en pending_approvals y desasignaciones en pending_unassignments. Nunca llama a PA directamente.
Merge si ya existe fila para hoy. Sin historial SP — Python tiene su propia copia en SQLite.
Lee pending_approvals con status=pending. Por cada usuario: POST al flujo PA de aprobaciones con payload consolidado.
Marca como triggered. Una única aprobación PA por usuario por día.
Lee pending_unassignments con status=pending. Por cada usuario: envía notificación consolidada.
Mecanismo a decidir PA ligero (auditable) o notificación directa Python (más simple).
sharepoint_sync.sync_all_lists() lee todas las listas de activos SP vía Graph API. UPSERT en assets.assets.assigned_to_upn en SQLite (Python tiene el historial propio, sin necesidad de consultar el historial de versiones de SP):
assigned_to_upn vacío → existía en DB → desasignación del usuario que estaba guardadoassigned_to_upn nuevo o cambiado → asignación al nuevo usuariopending_approvals (user_upn, date, assets_json). Merge si ya existe fila para hoy.pending_unassignments (user_upn, date, assets_json). Merge si ya existe.pending_approvals con status=pending. Para cada usuario construye el payload completo: assets_new (del buffer) + assets_prev (todos los activos activos del usuario no incluidos como nuevos). Una única aprobación por usuario, con todo lo acumulado en el día.202 Accepted y ejecuta el flujo en la nube. Python guarda el trigger en pa_triggers y marca pending_approvals como triggered.SP: Approval Log. Registro inmutable en SharePoint, auditable desde Purview.SP: Approval Log y hace UPSERT en approval_log en SQLite. El portal web muestra el estado de aprobación leyendo esta copia local sin latencia.Un único flujo, disparado por HTTP desde Python. PA gestiona toda la lógica de aprobación, recordatorios, caducidad y escalación de forma nativa.
// POST {pa_flow_url}
// API Key: x-functions-key o header custom
{
"user_upn": "[email protected]",
"user_name": "Juan García",
"portal_url": "https://portal.idealista.com/asset-manager",
"assets_new": [
{ "name": "Dell Latitude 5540", "tag": "SN123456", "category": "Portátil" }
],
"assets_prev": [
{ "name": "iPhone 14", "tag": "IMEI-999", "category": "Móvil" }
]
}| Trigger | When an HTTP request is received |
| Approval type | Approve/Reject – First to respond |
| Assigned to | @{triggerBody()?['user_upn']} |
| Due date | addDays(utcNow(), 7) |
| Timeout | 7 días (caducidad nativa PA) |
| Item link | URL del portal web |
| Manager lookup | Conector M365 Users: Get manager |
| Escalation notification | Post message in a chat or channel (Teams) |
UserUPN, Outcome (Approved/Rejected/Expired), RespondedBy, RespondedAt, Comments, ApprovalId (GUID de PA), AssetSnapshot (JSON). Python sincroniza esta lista igual que el resto, generando el mirror local en approval_log.
La eliminación del motor de aprobaciones custom simplifica significativamente el código Python. En morado, la nueva capa de integración con Power Automate.
Eliminados respecto al diseño inicial: approval_engine.py (motor custom), ruta /approval/<token> (página sin SSO), tablas approval_batches y approval_items. Simplificación significativa del código Python.
SQLite con WAL mode, conexiones thread-local. La tabla approval_log es un mirror de la lista SP que PA actualiza — no se escribe desde Python, solo se lee desde SharePoint.
| Tabla | Descripción | Campos clave |
|---|---|---|
settings |
Key-value persistente de la app | key, value |
sp_lists |
Listas SP configuradas (activos + approval log), con estado de último sync | list_id, category, list_type, last_sync_at, last_sync_error |
assets |
Un registro por item de cualquier lista de activos SP. UPSERT por (sp_list_id, sp_item_id) |
assigned_to_upn, asset_name, asset_tag, asset_details (JSON), first_seen_at, removed_at |
pending_approvals |
Buffer acumulador diario de asignaciones nuevas. El sync job acumula activos nuevos de cada usuario. El trigger job los consume una vez al día. Garantiza una sola aprobación PA por usuario por día aunque el sync corra varias veces. | user_upn, date, assets_json (JSON acumulado), status (pending/triggered), created_at, triggered_at |
pending_unassignments |
Buffer acumulador diario de desasignaciones. Mismo patrón que pending_approvals pero para activos retirados. Consumido por el trigger job de notificación (mecanismo a decidir: PA o directo desde Python). |
user_upn, date, assets_json (JSON acumulado), status (pending/triggered), created_at, triggered_at |
approval_log |
Mirror de SP "Approval Log". PA escribe en SP; Python solo sincroniza. Registro no modificable desde la app. | sp_item_id, user_upn, outcome, responded_by, responded_at, comments, approval_id, asset_snapshot (JSON) |
pa_triggers |
Log de cada llamada HTTP a PA: usuario, timestamp, assets incluidos, respuesta HTTP. | user_upn, triggered_at, asset_ids (JSON), pa_response_status |
sync_log |
Log de cada ejecución del sync: estado, items procesados, errores | started_at, status, items_new, items_removed, error_message |
audit_log |
Eventos internos de la app: sync, pa_trigger enviado. Las aprobaciones en sí están en SP / Purview. | timestamp, actor_upn, action, details (JSON) |
first_seen_at = timestamp del sync actual. Reasignación: cambió assigned_to_upn. Antes de disparar PA, se comprueba pa_triggers: si ya hay un trigger de las últimas 24h para ese usuario, se añaden los nuevos activos al snapshot pero no se crea una nueva aprobación.
// config/app-config.json
{
"port": 8087,
"db_path": "../db/asset-manager.db",
"sharepoint": {
"site_id": "idealista.sharepoint.com,<SITE-GUID>,<WEB-GUID>",
"sync_interval_minutes": 30,
"approval_log_list_id": "GUID-APPROVAL-LOG", // lista que PA escribe
"lists": [
{
"list_id": "GUID-PORTATILES",
"display_name": "Portatiles",
"category": "Portatiles",
"column_mapping": {
"assigned_to_upn": "AsignadoA_Email",
"asset_name": "Modelo",
"asset_tag": "NumeroSerie",
"assignment_date": "FechaAsignacion"
},
"extra_columns": ["RAM", "Disco", "SO"]
}
]
},
"power_automate": {
"flow_url": "https://prod-XX.westeurope.logic.azure.com/workflows/.../triggers/...",
"api_key_header": "x-functions-key", // o header custom configurado en el flujo
"api_key": "KEY_HERE",
"trigger_window_hours": 24, // no repetir trigger si ya se lanzó en 24h
"portal_url": "https://portal.idealista.com/asset-manager"
}
}Sin páginas de aprobación custom — toda la aprobación ocurre en Teams / M365 Approvals. El portal web muestra el resultado sincronizado desde SharePoint.
| Ruta | Auth | Descripción |
|---|---|---|
GET / |
SSO | Dashboard del usuario: material asignado agrupado por categoría. Badge de estado de aprobación (aprobado / pendiente / rechazado) junto a cada grupo. |
GET /admin/overview |
Admin | Aprobaciones de todos los usuarios: filtros por outcome, usuario y fecha. Link al centro de aprobaciones M365 para ver el detalle completo. |
GET /admin/assets |
Admin | Todos los activos de todas las listas. Búsqueda por usuario, tag de inventario o categoría. |
GET /admin/sync |
Admin | Estado de sincronización (listas de activos + approval log), historial sync_log y trigger manual. |
POST /admin/sync/trigger |
Admin | Lanza un sync inmediato fuera del intervalo programado. |
GET /api/my-assets |
SSO | JSON con activos del usuario actual y último estado de aprobación. |
Nueva App Registration con certificado (Windows cert store). Permisos mínimos necesarios — el manager lookup y las notificaciones las gestiona el flujo de PA con sus propias credenciales.
| Permiso | Tipo | Para qué |
|---|---|---|
Sites.Read.All | Application | Leer listas SP (activos + approval log) |
User.Read.All | Application | Resolver nombres y avatares SSO |
Quitados respecto al plan inicial: Mail.Send (lo gestiona PA). Futuro write-back: Sites.ReadWrite.All.
# Leer items de lista de activos SP
GET /sites/{site_id}/lists/{list_id}/items
?$expand=fields&$top=200
# seguir @odata.nextLink
# Descubrir columnas internas (una vez)
GET /sites/{site_id}/lists/{list_id}/columns
# Leer lista Approval Log (sync)
GET /sites/{site_id}/lists/{approval_log_id}/items
?$expand=fields&$top=200
# Avatares SSO
GET /users/{upn}/photo/$valueEl flujo PA usa su propia conexión (cuenta de servicio o App Registration separada) para el conector de Approvals, el conector de M365 Users (manager lookup), el conector de Teams y la escritura en la lista SP Approval Log. No comparte credenciales con la app Python.
| Conector PA | Para qué |
|---|---|
| Microsoft Approvals | Crear y gestionar la aprobación |
| Office 365 Users | Get manager (para escalación al caducar) |
| Microsoft Teams | Notificación de escalación al manager |
| SharePoint | Create item en lista Approval Log |
Significativamente más simple que el diseño anterior. Recordatorios, caducidad y escalación los gestiona PA. Python solo sincroniza datos y dispara el trigger.
| Job | Frecuencia | Acción |
|---|---|---|
| Sync assets SP → SQLite | Cada 30 min configurable | Leer listas de activos, UPSERT en assets. Si hay nuevos activos → UPSERT en pending_approvals (merge si ya existe fila para hoy). No llama a PA. |
| Sync approval log SP → SQLite | Cada 30 min junto al anterior | Leer lista SP: Approval Log, UPSERT en approval_log. Permite mostrar el estado en el portal sin latencia Graph API. |
| Trigger PA — asignaciones | 1× al día a hora configurable | Lee pending_approvals con status=pending. Por cada usuario: POST al flujo PA con payload consolidado (activos nuevos + previos). Marca triggered. Garantiza una única aprobación por usuario por día. |
| Trigger — desasignaciones a decidir | 1× al día (misma hora) | Lee pending_unassignments con status=pending. Por cada usuario: envía notificación con los activos desasignados ese día. Mecanismo pendiente de decisión (ver sección dedicada). |
| Purga | Diario | Limpiar audit_log, pa_triggers, pending_approvals y pending_unassignments con más de 6 meses. |
Las desasignaciones no requieren aprobación, solo notificación informativa al usuario. Python detecta el evento comparando SP con su copia local en SQLite — sin necesidad de consultar el historial de versiones de SP. Hay dos opciones para enviar el aviso.
assets guarda siempre el último assigned_to_upn conocido. Cuando el sync lee SP y encuentra el campo vacío (o el item ha desaparecido), Python ya sabe quién era el usuario anterior porque lo tiene en su propia DB. No hace falta ninguna llamada extra a Graph.
Python dispara un segundo flujo PA ligero, sin conector de Approvals. El flujo solo envía email + Teams al usuario con la lista de material desasignado y escribe una entrada en SP Approval Log.
| ✅ | Registro auditable en SP / Purview (misma lista Approval Log) |
| ✅ | Coherente con el canal de asignaciones (todo via PA) |
| ✅ | Flujo muy simple: trigger → send email/Teams → create SP item |
| ⚠ | Requiere crear y mantener un segundo flujo PA |
// Payload Python → PA (flujo notificación)
{
"user_upn": "[email protected]",
"user_name": "Juan García",
"assets_removed": [
{ "name": "Dell Latitude 5540", "tag": "SN123" }
]
}Python envía directamente: email via Graph API sendMail y/o Teams via SisAlerts ingest. Sin PA de por medio. El evento se registra únicamente en audit_log de SQLite.
| ✅ | Sin dependencias externas adicionales, todo en Python |
| ✅ | Más rápido de implementar, sin flujo PA que configurar |
| ✅ | Control total del formato y contenido del mensaje |
| ⚠ | Registro solo en SQLite local, no en SP/Purview |
| ⚠ | Si hay requisito de trazabilidad, la Opción A es preferible |
# Python directo via SisAlerts
POST {sisalerts_url}/api/ingest
X-Api-Key: {key}
{
"subject": "Material IT desasignado",
"priority": "low",
"body": "Se ha desasignado: ..."
}pending_unassignments y la misma lógica de trigger diario, por lo que cambiar de una a otra en el futuro no implica refactorizar el scheduler.
db/schema.sql, db/database.py, config/app-config.json, run_webapp.py, deploy.ps1, WinSW XML.webapp/app.py con init_shell() de sisapps-ui, base.html, auth.py, dashboard con estado vacío. Verificar SSO con dev bypass.services/sharepoint_sync.py: cliente Graph API con paginación y column_mapping. Sync de listas de activos + lista Approval Log. Scheduler con ambos jobs.dashboard.html: activos agrupados por categoría + badge con estado de aprobación (leyendo approval_log).services/pa_trigger.py: construye el payload consolidado (nuevos + previos) y hace POST al HTTP trigger del flujo. Guarda el trigger en pa_triggers. Probar end-to-end con usuario de prueba.sisapps_ui/registry.py.| Riesgo | Probabilidad | Mitigación |
|---|---|---|
| Nombres internos SP ≠ display names | Alta | Llamar a GET /lists/{id}/columns para descubrir los nombres OData internos antes de configurar el column_mapping. |
| Seguridad del HTTP trigger PA | Alta | El endpoint HTTP trigger debe protegerse. Opciones: (1) SAS key nativa de Logic Apps en la URL, (2) IP restriction en el flujo (solo la IP del servidor Windows), (3) header de API key verificado en la primera acción del flujo. |
| Identidad de usuario inconsistente en SP | Media | Las listas pueden tener email, UPN o solo nombre. Normalizar a lowercase. Fallback a Graph API /users?$filter=mail eq '...' si hace falta. |
| Límites de PA y throttling | Media | Power Automate tiene límites de ejecuciones por día según licencia. Con volumen bajo (decenas de aprobaciones/día) no debería ser problema. Monitorizar desde el centro de administración PA. |
| Paginación en listas SP grandes | Media | Seguir @odata.nextLink en el sync client para listas con más de 200 items. |
| Syncs concurrentes | Baja | Lock de sync con threading.Lock(). Siguiente tick salta si ya hay sync en curso. |
| Approval Log desincronizado | Baja | El portal muestra el estado del último sync. Máximo 30 min de retraso. Aceptable para este caso de uso. |
Crear y editar asignaciones desde el portal web. Se escribe en SQLite local y un job sincroniza a SharePoint via PATCH /items/{id}/fields. Requiere Sites.ReadWrite.All. Al asignar, Python dispara automáticamente el flujo PA.
Estados: assigned → returned → retired → in_repair. Transiciones registradas en audit_log. Al devolver un activo, se actualiza SP y se cancela cualquier aprobación pendiente en PA.
Activos por departamento, tasa de respuesta a aprobaciones, tiempo medio hasta aprobación, activos sin confirmar. Los datos ya están en SQLite local desde el mirror de SP Approval Log.