Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions operaciones/inventario-insumos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Automatización · Inventario de Insumos ↔ Rutas Completadas

Grupo Portátil · Operaciones (MTY / QRO)

Descuento automático de insumos (desinfectantes, papel, gel/jabón, equipo de
limpieza) al cierre de cada jornada, sincronizado con el registro de rutas
completadas. Stack: **Supabase** (datos + lógica) · **n8n** (orquestación) ·
**AppSheet** (captura del operador) · **WhatsApp/Troncalnet** (alertas).

---

## OBJETIVO OPERATIVO
Que el inventario refleje el consumo real de cada jornada sin captura manual, para
eliminar faltantes en campo y compras reactivas. Indicador que mejora: **% de
jornadas con inventario actualizado el mismo día** (meta 100%) y **cero compras de
emergencia**.

## PROCESO / FLUJO
```
Operador cierra ruta en AppSheet ─► Webhook n8n ─► procesar_cierre_jornada() en Supabase
│ │
│ ├─ descuenta insumos (salida en ledger)
│ ├─ marca órdenes como descontadas
│ └─ cierra la jornada con totales
▼ ▼
(respaldo 20:30 si no cerró) alerta de reorden a Eduardo + resumen al operador
```
- **Consumo real declarado** por el operador manda sobre el **estimado por BOM**.
- Si no declara nada, se usa el estimado (cero fricción).

## IMPLEMENTACIÓN EN EL STACK GP
| Componente | Herramienta | Archivo |
|---|---|---|
| Esquema de datos | Supabase | `sql/01_schema.sql` |
| Lógica de descuento (idempotente) | Supabase (PL/pgSQL) | `sql/02_funciones_triggers.sql` |
| Vistas, alertas y KPIs | Supabase | `sql/03_vistas_kpi.sql` |
| Catálogo + recetas de consumo (BOM) | Supabase | `sql/04_datos_semilla.sql` |
| Orquestación de cierre | n8n | `n8n/workflow-cierre-jornada.md` |
| Captura del operador | AppSheet | `appsheet/cierre-jornada.md` |
| Procedimiento del equipo | — | `docs/SOP-cierre-jornada-insumos.md` |

### Modelo de datos (resumen)
- `movimientos_insumo` — **libro mayor** (fuente de verdad). Nada edita el saldo directo.
- `insumo_existencias` — saldo vivo por plaza, lo mantiene un trigger.
- `consumo_estandar` — receta (BOM): insumo por unidad servida y tipo de servicio.
- `jornadas` — bisagra rutas↔inventario: `abierta → cerrada → procesada`.
- `consumo_declarado` — cantidades reales del operador (override del BOM).

### Orden de despliegue en Supabase
```
01_schema.sql → 02_funciones_triggers.sql → 03_vistas_kpi.sql → 04_datos_semilla.sql
```

## RIESGOS OPERATIVOS
- **Doble descuento** → blindado con función idempotente + índice único por
jornada/insumo + bandera `insumos_descontados` en las órdenes.
- **Operador no cierra jornada** → respaldo programado 20:30 en n8n.
- **BOM desviado de la realidad** → override declarado + inventario físico mensual
(`ajuste`, `origen='inventario_fisico'`) para recalibrar.
- **Stock negativo** → semáforo `AGOTADO` y alerta; señala desfase a corregir.

## MÉTRICAS DE ÉXITO
- `v_ordenes_sin_descontar` vacía al cierre de cada día.
- 100% de jornadas procesadas el mismo día.
- Reducción de faltantes en campo y de compras de emergencia.
- Costo de insumos por servicio visible en `jornadas.costo_insumos`.

## SIGUIENTE ACCIÓN (Eduardo)
1. Revisar `sql/04_datos_semilla.sql`: ajustar **recetas de consumo (BOM)**, costos
y stock inicial a la realidad de GP (los valores son de arranque).
2. Confirmar el **nombre real** de la tabla de rutas completadas (aquí `ordenes_trabajo`
con columnas `tipo_servicio`, `unidades`, `estado`, `operador`, `plaza`).
3. Dar OK para **desplegar en vivo**: aplicar las migraciones en Supabase y crear el
workflow en n8n (inactivo para tu revisión) usando los tools MCP ya conectados.
35 changes: 35 additions & 0 deletions operaciones/inventario-insumos/appsheet/cierre-jornada.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# AppSheet · Cierre de jornada y captura de consumo real

Cómo debe verse la experiencia del operador (Alberto, Emmanuel, Meñito, Juan Pablo)
para que el inventario se actualice al terminar la ruta.

## Flujo del operador
1. Durante la ruta, cada parada se marca **completado** (ya existe: foto + firma).
2. Al terminar, el operador abre **"Cerrar mi jornada"**.
3. AppSheet muestra el **consumo estimado** de la jornada (calculado por BOM sobre
sus órdenes completadas) — es el valor por defecto.
4. Opcional: el operador **ajusta a consumo real** si gastó más/menos (ej. derramó
desinfectante, repuso papel extra). Esto llena `consumo_declarado`.
5. Toca **Confirmar cierre** → se dispara el webhook a n8n con el `jornada_id`.

## Tablas/vistas que consume AppSheet
- **Vista resumen de jornada** (solo lectura), consulta sugerida:
```sql
SELECT ce.tipo_servicio, i.nombre, i.unidad_medida,
SUM(ce.cantidad_por_unidad * ot.unidades) AS estimado
FROM ordenes_trabajo ot
JOIN consumo_estandar ce ON ce.tipo_servicio = ot.tipo_servicio
JOIN insumos i ON i.id = ce.insumo_id
WHERE ot.jornada_id = :jornada_id AND ot.estado='completado'
GROUP BY ce.tipo_servicio, i.nombre, i.unidad_medida;
```
- **Formulario de ajuste** → escribe en `consumo_declarado (jornada_id, insumo_id, cantidad)`.

## Acción "Confirmar cierre" (AppSheet Action → Webhook)
- Marca `jornadas.estado = 'cerrada'`, `cerrada_en = NOW()`.
- Llama al webhook n8n `POST /gp/cierre-jornada` con body `{ "jornada_id": "<id>" }`.

## Regla de negocio
- Si el operador **no** ajusta nada, el sistema usa el estimado por BOM (sin fricción).
- Si ajusta aunque sea un insumo, se usa **todo** lo declarado (el operador tomó control
de esa jornada). Documentar esto para que el equipo lo entienda.
49 changes: 49 additions & 0 deletions operaciones/inventario-insumos/docs/SOP-cierre-jornada-insumos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SOP · Cierre de jornada y actualización de inventario de insumos

**Área:** Operaciones · **Plazas:** MTY / QRO · **Responsables:** operadores + Eduardo
**Frecuencia:** diaria, al finalizar cada jornada de servicio.

## Objetivo
Que el inventario de insumos (desinfectantes, papel, gel/jabón, equipo de limpieza)
refleje el consumo real de cada jornada de forma automática, ligado a las rutas
completadas, para evitar faltantes en campo y compras reactivas.

## Procedimiento diario

### Operador (Alberto / Emmanuel / Meñito / Juan Pablo)
1. Marca cada parada como **completado** en AppSheet (foto + firma) — sin cambios.
2. Al terminar la ruta, entra a **"Cerrar mi jornada"**.
3. Revisa el consumo estimado; ajústalo solo si gastaste algo distinto.
4. Toca **Confirmar cierre**. Listo — el inventario se descuenta solo.

### Sistema (n8n + Supabase) — automático
5. Recibe el cierre, calcula consumo (real declarado o estimado por BOM).
6. Descuenta insumos de la plaza correspondiente (movimiento de salida).
7. Marca las órdenes de esa jornada como `insumos_descontados`.
8. Si algún insumo quedó en o bajo el mínimo, avisa a Eduardo por WhatsApp con la
sugerencia de reorden.
9. Envía al operador un resumen: órdenes, unidades y costo de insumos del día.

### Respaldo (automático, 20:30)
10. n8n barre jornadas no cerradas manualmente y las procesa igual. Nada se queda
sin descontar.

## Verificación (Eduardo, semanal)
- La vista `v_ordenes_sin_descontar` debe estar **vacía**. Si tiene filas, hubo
rutas completadas sin cerrar jornada → revisar con el operador.
- Revisar `v_consumo_semanal_insumo` para planear compras.
- Hacer **inventario físico** mensual y capturar diferencias como `ajuste`
(`origen='inventario_fisico'`) para recalibrar el BOM si el estimado se desvía.

## Riesgos y controles
| Riesgo | Control |
|---|---|
| Doble descuento por reintento | Función idempotente + índice único por jornada/insumo |
| Operador olvida cerrar jornada | Respaldo programado 20:30 |
| BOM desviado de la realidad | Ajuste declarado por operador + inventario físico mensual |
| Stock en negativo (desfase) | Semáforo AGOTADO en `v_inventario_actual` + alerta |

## Métricas de éxito
- 100% de jornadas con inventario descontado el mismo día.
- Reducción de faltantes en campo (unidades servidas sin insumo).
- Cero compras de emergencia por sorpresa de stock.
113 changes: 113 additions & 0 deletions operaciones/inventario-insumos/n8n/workflow-cierre-jornada.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# n8n · Workflow "Cierre de Jornada → Inventario de Insumos"

Automatiza el descuento de insumos al finalizar cada jornada y sincroniza el
resultado con el registro de rutas completadas. Idempotente por diseño: apoyarse
en `procesar_cierre_jornada()` (ver `sql/02_funciones_triggers.sql`) evita doble
descuento aunque el flujo se dispare dos veces.

---

## Arquitectura del disparo (dos entradas, misma lógica)

```
(1) AppSheet "Cerrar jornada" ──► Webhook n8n ─┐
├─► [Procesar cierre] ─► [Alertas] ─► [Notificar]
(2) Respaldo programado 20:30 ──► Schedule ────┘
```

- **Entrada 1 — Webhook (principal):** cuando el operador toca *Cerrar jornada*
en AppSheet, se envía `jornada_id`. Descuento inmediato al terminar la ruta.
- **Entrada 2 — Schedule (red de seguridad):** a las 20:30 (MTY y QRO) barre
jornadas con `estado='cerrada'` o con órdenes completadas sin procesar, por si
un operador olvidó cerrar. Como la función es idempotente, no hay riesgo de
doble descuento si ambas entradas coinciden.

---

## Nodos

### A) Rama Webhook

1. **Webhook** (`POST /gp/cierre-jornada`)
- Body esperado: `{ "jornada_id": "<uuid>" }`
- Autenticación: Header Auth (token compartido con AppSheet).

2. **Supabase → RPC `procesar_cierre_jornada`** (nodo Postgres o Supabase, "Execute Query")
```sql
SELECT * FROM procesar_cierre_jornada('{{ $json.jornada_id }}'::uuid);
```
Devuelve una fila por insumo descontado (`sku`, `cantidad`, `costo`). Cero
filas = jornada ya procesada o sin consumo (no es error).

### B) Rama Schedule (respaldo)

1. **Schedule Trigger** — Cron `30 20 * * *`.
2. **Postgres — jornadas pendientes**
```sql
-- Cierra automáticamente jornadas con órdenes completadas sin procesar
INSERT INTO jornadas (operador, plaza, fecha, estado, cerrada_en)
SELECT ot.operador, ot.plaza, CURRENT_DATE, 'cerrada', NOW()
FROM ordenes_trabajo ot
WHERE ot.estado='completado' AND ot.jornada_id IS NULL
GROUP BY ot.operador, ot.plaza
ON CONFLICT (operador, plaza, fecha) DO NOTHING;

SELECT id AS jornada_id FROM jornadas
WHERE fecha = CURRENT_DATE AND estado IN ('abierta','cerrada');
```
> Nota: si tus órdenes aún no traen `jornada_id`, asigna la jornada del día
> antes de procesar. Ajusta este bloque a cómo tu app agrupa las rutas.
3. **Loop / Split In Batches** → por cada `jornada_id`, llamar la misma RPC del paso A2.

### C) Rama común — Alertas de stock bajo

4. **Postgres — revisar mínimos**
```sql
SELECT plaza, sku, nombre, stock_actual, stock_minimo, sugerido_pedir,
unidad_medida, costo_reorden_estimado
FROM v_insumos_bajo_minimo;
```
5. **IF** — ¿hay filas?
- **True** → **WhatsApp / Troncalnet (HTTP Request)** a Eduardo:
```
🧴 Insumos en punto de reorden ({{plaza}}):
{{#each}}• {{nombre}}: {{stock_actual}} {{unidad_medida}} (mín {{stock_minimo}}) → pedir {{sugerido_pedir}}
{{/each}}
```

### D) Rama común — Confirmación al operador

6. **WhatsApp / Troncalnet** al operador con el resumen de su jornada:
```
✅ Jornada cerrada — {{operador}} {{fecha}}
Órdenes: {{ordenes_completadas}} · Unidades: {{unidades_servidas}}
Insumos descontados: {{n_insumos}} · Costo insumos: ${{costo_insumos}}
```
Consultar totales con:
```sql
SELECT ordenes_completadas, unidades_servidas, costo_insumos, operador, fecha
FROM jornadas WHERE id = '{{ $json.jornada_id }}'::uuid;
```

### E) Manejo de errores
- Conectar un **Error Trigger** que notifique a Eduardo por WhatsApp si la RPC
falla, incluyendo `jornada_id`. Nunca se pierde una jornada en silencio.

---

## Reglas de idempotencia (crítico)
1. `procesar_cierre_jornada()` sale sin efectos si `jornada.estado='procesada'`.
2. Índice único `uniq_salida_jornada_insumo` impide doble salida por jornada/insumo.
3. `ordenes_trabajo.insumos_descontados=TRUE` evita reprocesar la misma ruta.

Por eso Webhook + Schedule pueden coexistir sin duplicar descuentos.

---

## Construcción vía MCP de n8n (cuando se apruebe el despliegue)
El asistente puede crear este workflow con el SDK de n8n:
1. `get_sdk_reference` + `get_workflow_best_practices` (techniques: `scheduling`, `webhook`).
2. `search_nodes` → `webhook`, `schedule trigger`, `postgres`, `if`, `http request`, `split in batches`.
3. `get_node_types` de todos los nodos.
4. `create_workflow_from_code` con la lógica de arriba.
5. `validate_workflow` y dejar **inactivo** para revisión de Eduardo antes de publicar.
Loading