--- 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= 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.