hbox-docs/docs/inventario/integracion-cotizador.md

155 lines
13 KiB
Markdown

---
title: "Cotizador ↔ centro de mando: órdenes de compra, inventario por sitio y tareas"
date: 2026-10-27
tags: [project-drogheda, hbox, compras, inventario, cotizador, integracion, sql]
estado: propuesta con 3 decisiones tomadas (27-oct); falta construir
---
# Cotizador ↔ centro de mando: órdenes de compra, inventario por sitio y tareas
**27-oct-2026 · v1.0 · Claude (CTO/CSO) con Leo.** Responde al pendiente `t-cotizador-integracion`: que la
requisición, la orden de compra y la llegada del material sean un solo hilo que termine en el inventario de cada sitio,
con su pendiente en el tablero. Las instrucciones para el proyecto del cotizador están en
[Instrucciones para el cotizador](integracion-cotizador-instrucciones.md).
![Flujo de una orden de compra del cotizador al inventario del sitio](img/2026-10-27_Drogheda_Flujo-OC-Inventario_v1.0.png)
## Resumen
1. **El cotizador se queda con el dinero y la autorización.** Ya tiene requisición → autorización de compras → orden de
compra (OC) → pago → «Recibí el material», con roles y montos protegidos. No se duplica en el sitio.
2. **El centro de mando recibe lo operativo de cada OC, sin montos:** folio, proyecto, **sitio de entrega** (código de 3
letras), proveedor, renglones con cantidad y su ítem del inventario, fechas, paquetería y guía.
3. **«En camino» es nuevo:** cuando el proveedor manda el material, compras o quien pidió lo confirma en el cotizador con
la guía. En ese momento el material aparece **en camino** en la pestaña del sitio del inventario.
4. **Se recibe en el sitio, en el centro de mando** (decisión de Leo, 27-oct): el responsable del sitio da **Recibir** con
lo que llegó de verdad; el cotizador se entera solo y cierra la requisición.
5. **Cada OC abre su pendiente** (pista Compras, su sitio, el responsable del sitio, vence en la fecha de entrega) y se
cierra solo cuando se recibe todo.
6. **Base de datos: SQLite** en el servidor del sitio (decisión de Leo, 27-oct), empezando por las órdenes de compra; el
inventario pasa después, con una prueba de que los números no cambian.
## Decisiones
| # | Decisión | Estado |
|---|---|---|
| D-27-1 | El material de una OC se **recibe en el centro de mando** (pestaña del sitio, botón Recibir). El cotizador cierra la requisición solo. | **Tomada** (Leo, 27-oct) |
| D-27-2 | Base de datos **SQLite** en el servidor del sitio (`/srv/projects/drogheda/drogheda.db`). PostgreSQL solo si otra aplicación tiene que escribir directo en ella. | **Tomada** (Leo, 27-oct) |
| D-27-3 | Código de 3 letras por sitio = el de su aeropuerto y su puerto (UN/LOCODE): **VER** Veracruz (ICAVE) · **ZLO** Manzanillo (TIMSA) · **ESE** Ensenada (EIT) · **LZC** Lázaro Cárdenas (LCT) · **QRO** Querétaro. Son los que vienen en guías y documentos de embarque; el sitio ya los acepta. | Propuesta de Claude (Leo pidió códigos de 3 letras) |
| D-27-4 | La **autorización** de la OC se queda en el cotizador (rol de compras). El centro de mando **nunca** muestra montos: todo el equipo lo ve. | Propuesta |
| D-27-5 | Solo las OC de proyectos ligados van al centro de mando. Hoy: DROGHEDA = `PRY-2026-0002-DRO` → proyecto `drogheda`. | Propuesta |
## Lo que hay hoy
| | Cotizador | Centro de mando (drogheda.helgen.online) |
|---|---|---|
| Dónde vive | Google Apps Script + Sheets + Drive; interfaz en Cloudflare; entra con cuenta @helgen.tech | Servidor de Helgen (Flask + nginx), página de entrada propia desde el 27-oct |
| Proyecto | `PRY-2026-0002-DRO` «Helgen — DROGHEDA», MXN, sin contrato | `drogheda` |
| Requisición | `REQ-AAAA-NNNN`: Solicitada → Autorizada → Pagada → Cerrada (también Devuelta, Rechazada, Cancelada). Piden sin precios: adriana@, miguel@, paola@, jesus@ (solo requisiciones) y efrain@, tania@, leo@ | — |
| Orden de compra | `OC-AAAA-NNNN`: Emitida → Pagada → Recibida, o Cancelada. Sale siempre de una requisición autorizada. **Sin recepciones parciales** | — |
| Sitio de entrega | No existe (solo «lugar de entrega» en texto) | `icave` · `timsa` · `eit` · `lzc` · `qro` en cada pendiente, movimiento y miembro |
| Material | Renglones de texto libre (descripción, unidad, cantidad) | Ítems con código, existencia por sitio, envíos «en camino» con Recibir (también parcial), kit por instalación |
| Tareas | — | Pendientes con pista, sitio, responsable y vencimiento |
**Lo que falta para unirlos:** el sitio de entrega en la requisición, el ítem del inventario en cada renglón, el estado
«En camino», un canal entre los dos y una forma de cerrar la requisición desde la recepción.
**Una restricción que define el diseño:** el cotizador puede llamar al centro de mando (Apps Script tiene `UrlFetchApp`),
pero el centro de mando **no** puede llamar al cotizador, porque cada llamada al cotizador exige una sesión de Google de
una persona (su regla 10, que no se quita). Por eso: el cotizador **avisa** (empuja) cada cambio de una OC, y **consulta**
cada 15 minutos lo que ya se recibió.
## El flujo, paso por paso
| Paso | Dónde | Quién | Qué pasa |
|---|---|---|---|
| 1. Pedir | Cotizador | Quien necesita el material | Requisición en DROGHEDA con **sitio de entrega** (obligatorio en proyectos ligados) y renglones; cada renglón puede llevar su **ítem del inventario** (lista que el cotizador baja del centro de mando). |
| 2. Autorizar | Cotizador | Compras (Efraín, Tania, Leo) | Emite la OC como hoy. Aviso `oc.autorizada` → el centro de mando crea la OC (sin montos) y el pendiente «Recibir OC-… en ZLO» para el responsable del sitio, que vence en la fecha de entrega. |
| 3. Pagar | Cotizador | Compras | Como hoy. Aviso `oc.pagada` (solo el estado). |
| 4. En camino | Cotizador | Compras o quien pidió | «El proveedor ya lo mandó»: paquetería, guía y fecha estimada. Aviso `oc.en_camino` → en la pestaña del sitio aparece cada renglón **en camino** (un envío desde «Proveedor o fuera del inventario», con el folio de la OC como referencia). Los renglones sin ítem quedan en **Compras → por ligar** del centro de mando. |
| 5. Recibir | Centro de mando | Responsable del sitio (Alán VER, Jesús Honorato ZLO, Miguel ESE, Antonio LZC, Jesús Romero QRO) | **Recibir** con lo que llegó de verdad (puede ser parcial). La OC queda «recibida» o «recibida en parte»; el pendiente se cierra solo cuando llega todo. |
| 6. Cerrar | Cotizador (solo) | — | Cada 15 min el cotizador consulta las recepciones: si llegó todo, cierra la requisición («Recibido en ZLO por jesush en el centro de mando, 30-oct») y la OC queda Recibida. Si llegó una parte, lo anota en las notas de recepción. |
| Cancelar | Cotizador | Compras | Aviso `oc.cancelada` → el centro de mando cancela los envíos abiertos y el pendiente. |
## Datos (SQLite)
Un archivo, `/srv/projects/drogheda/drogheda.db`, en modo WAL (aguanta a los dos procesos del servicio). Se respalda
con el resto de `/srv/projects/drogheda`.
```sql
CREATE TABLE sitios (codigo TEXT PRIMARY KEY, -- VER ZLO ESE LZC QRO
id_portal TEXT UNIQUE NOT NULL, -- icave timsa eit lzc qro
nombre TEXT NOT NULL, responsable TEXT);
CREATE TABLE proyectos (id TEXT PRIMARY KEY, -- drogheda
pry_folio TEXT UNIQUE NOT NULL, -- PRY-2026-0002
clave TEXT, nombre TEXT);
CREATE TABLE ordenes (folio TEXT PRIMARY KEY, -- OC-2026-0012
req_folio TEXT, proyecto_id TEXT REFERENCES proyectos(id),
sitio TEXT REFERENCES sitios(codigo), proveedor TEXT,
estado TEXT CHECK (estado IN ('autorizada','pagada','en-camino','recibida-parcial','recibida','cancelada')),
fecha_entrega TEXT, paqueteria TEXT, guia TEXT, en_camino_en TEXT,
solicitante TEXT, autorizo TEXT, tarea_id TEXT, prueba INTEGER DEFAULT 0,
creado_en TEXT, actualizado_en TEXT);
CREATE TABLE renglones (oc_folio TEXT REFERENCES ordenes(folio), n INTEGER,
descripcion TEXT, unidad TEXT, cantidad REAL,
item_codigo TEXT, -- ítem del inventario (NULL = por ligar)
envio_id TEXT, -- el envío «en camino» que se creó en el inventario
recibido REAL DEFAULT 0, PRIMARY KEY (oc_folio, n));
CREATE TABLE eventos (evento_id TEXT PRIMARY KEY, -- idempotencia: el mismo aviso dos veces no duplica
oc_folio TEXT, tipo TEXT, cuando TEXT, quien TEXT,
origen TEXT CHECK (origen IN ('cotizador','portal')), datos TEXT);
```
**Fases.** 1) Órdenes de compra en SQLite; el inventario sigue en sus JSON y la OC crea sus envíos con la API de hoy.
2) El inventario (ítems y movimientos) pasa a SQLite con una comparación de existencias por sitio antes y después.
3) **Activos**: una fila por unidad y por pieza con serie (HBOX, tarjetas, módulos) con su sitio, estado, vehículo y actas
— lo que hoy se arma leyendo las actas.
## Cómo se hablan (API del centro de mando para el cotizador)
**Seguridad.** Las rutas `/api/integracion/…` no usan la sesión de una persona: cada llamada va firmada con un secreto
compartido (HMAC-SHA256 del cuerpo y la hora). El secreto vive en las propiedades del script del cotizador
(`PORTAL_SECRETO`) y en el servicio del servidor (`COTIZADOR_SECRETO`); **nunca en el código ni en un chat**. Se rechaza
una firma de hace más de 5 minutos.
```
POST /api/integracion/cotizador/evento
Encabezados: X-Hbox-Tiempo: 2026-10-28T10:15:00-06:00
X-Hbox-Firma: sha256=<hex de HMAC(secreto, tiempo + "\n" + cuerpo)>
Cuerpo:
{ "evento_id": "OC-2026-0012:oc.en_camino:1",
"tipo": "oc.en_camino", // oc.autorizada · oc.pagada · oc.en_camino · oc.cancelada · req.cerrada
"cuando": "2026-10-28T10:15:00-06:00", "quien": "tania@helgen.tech", "prueba": false,
"oc": { "folio": "OC-2026-0012", "req_folio": "REQ-2026-0031",
"proyecto_folio": "PRY-2026-0002", "proyecto_clave": "DRO", "sitio": "ZLO",
"proveedor": "Ferretería del Pacífico", "fecha_entrega": "2026-10-30",
"paqueteria": "Estafeta", "guia": "123456789", "solicitante": "jesus@helgen.tech",
"renglones": [ { "n": 1, "descripcion": "Soporte para RTG", "unidad": "pza", "cantidad": 4, "item": "SOP-RTG" } ] } }
Respuesta: { "ok": true, "estado_portal": "en-camino", "tarea_id": "t-oc-2026-0012", "por_ligar": [] }
(el mismo evento_id otra vez → { "ok": true, "repetido": true })
GET /api/integracion/inventario/items → [{codigo, descripcion, unidad, categoria}] (para el selector)
GET /api/integracion/cotizador/recepciones?desde=… → [{oc_folio, req_folio, sitio, completa, recibio, cuando,
renglones: [{n, pedido, recibido}]}]
```
`"prueba": true` (rondas del proyecto de PRUEBAS del cotizador) se guarda aparte y **no** toca el inventario ni el tablero.
## Qué se construye y quién
| Lado | Qué | Quién |
|---|---|---|
| Centro de mando | `backend/compras.py` (SQLite, las 3 rutas firmadas, envíos en camino, pendiente por OC, cierre al recibir); página **Compras** (OC por sitio y estado, renglones por ligar, liga desde el envío en camino del inventario); ruta en nginx sin sesión para `/api/integracion/` (la firma la revisa el backend); pruebas | Claude (fase 1), Leo despliega |
| Cotizador | Sitio de entrega y su regla; ítem por renglón; estado «En camino»; avisos firmados con reintento; consulta de recepciones cada 15 min y cierre automático; pruebas en Node y en PRUEBAS | El proyecto del cotizador (Claude con Efraín) — ver [instrucciones](integracion-cotizador-instrucciones.md) |
| Los dos | El secreto compartido (Leo lo genera y lo pone en los dos lados) y una prueba de punta a punta con una OC de PRUEBA | Leo |
## Riesgos y cómo se cubren
- **El centro de mando no contesta** cuando el cotizador avisa → el cotizador guarda el aviso en una pestaña de
pendientes y lo reintenta; el `evento_id` evita duplicados.
- **Una OC llega antes que su proyecto o su sitio** → se rechaza con un error claro (sitio inválido, proyecto no ligado)
y el cotizador lo muestra a compras.
- **Doble captura durante la transición** → las OC de antes no se migran; solo las nuevas de proyectos ligados.
- **Fuga del secreto** → se cambia en los dos lados; las firmas viejas dejan de valer al momento.
- **Montos** → el centro de mando no los recibe; si un aviso los trae, se descartan antes de guardar.