--- title: "Instrucciones para el proyecto del cotizador: ligar órdenes de compra al centro de mando" date: 2026-10-27 tags: [project-drogheda, cotizador, compras, inventario, integracion, instrucciones] --- # Instrucciones para el proyecto del cotizador **27-oct-2026 · v1.0.** Para la sesión de Claude que trabaja en `cotizador-helgen` (Apps Script + Sheets + Cloudflare). Diseño completo y decisiones: [Cotizador ↔ centro de mando](integracion-cotizador.md). Aquí está **qué cambiar en el cotizador**, en orden, con sus pruebas. Todo respeta las reglas del `CLAUDE.md` del cotizador (§7): totales en el servidor, datos de compras solo en su libro, `CAMBIOS_API`, manual actualizado en el mismo turno, pruebas en PRUEBAS antes del real. ## Para pegar en el proyecto del cotizador > Lee `CLAUDE.md` completo y después este documento (`2026-10-27_Drogheda_Instrucciones-Cotizador-Integracion_ES_v1.0.md`). > Vamos a ligar las órdenes de compra de los proyectos ligados (hoy DROGHEDA, `PRY-2026-0002`) con el centro de mando > drogheda.helgen.online, siguiendo el contrato de la sección «Contrato». Trabaja por pasos (1 a 7), con respaldos antes > de tocar hojas, pruebas en Node y una ronda en PRUEBAS con `"prueba": true` antes de publicar. No mandes montos al centro > de mando. No pongas el secreto en el código ni en el chat: va en las propiedades del script (`PORTAL_SECRETO`) y me lo > pides a mí cuando haga falta. Al terminar, actualiza `CLAUDE.md` (§4.5.3, §4.5.4, §5, §6.12, §6.13 y una §6.15 nueva) > y el Manual del Cotizador. ## Lo que decidió Leo (27-oct) - El material se **recibe en el centro de mando** (el responsable del sitio da «Recibir»). El cotizador **cierra solo** la requisición cuando el centro de mando dice que llegó todo. «Recibí el material» en el cotizador se queda como respaldo para proyectos no ligados y para cuando el centro de mando no esté disponible. - La autorización, los montos, los pagos y los proveedores **se quedan en el cotizador**. - Cada OC de un proyecto ligado lleva un **sitio de entrega** de 3 letras: `VER` Veracruz (ICAVE) · `ZLO` Manzanillo (TIMSA) · `ESE` Ensenada (EIT) · `LZC` Lázaro Cárdenas (LCT) · `QRO` Querétaro. ## Paso 1 · Propiedades del script | Propiedad | Valor | Nota | |---|---|---| | `PORTAL_URL` | `https://drogheda.helgen.online` | | | `PORTAL_SECRETO` | (lo genera Leo; el mismo que `COTIZADOR_SECRETO` en el servidor) | Nunca en el código, en `CLAUDE.md` ni en el chat | | `PORTAL_PROYECTOS` | `{"PRY-2026-0002": "drogheda"}` | Llave interna del proyecto → proyecto del centro de mando | | `PORTAL_SITIOS` | `VER,ZLO,ESE,LZC,QRO` | Con sus nombres en la interfaz (Veracruz · ICAVE, Manzanillo · TIMSA, …) | Agregar `script.external_request` (ya existe por la IA de claves SAT si está activa) y `script.scriptapp` (para el activador del paso 6) a `appsscript.json`: el dueño (efrain@) tendrá que volver a autorizar. ## Paso 2 · Sitio de entrega e ítem del inventario en la requisición - `Requisiciones`: columna nueva **`sitio_entrega`** (al final, como se agregó `req_folio`; las filas viejas quedan vacías). Obligatoria cuando el proyecto está en `PORTAL_PROYECTOS`; el servidor lo revisa en `guardarRequisicion` (no solo la interfaz). En la interfaz: un selector con los 5 sitios, junto al proyecto. - `RequisicionesRenglones`: columna nueva **`inventario_item`** (código del ítem, opcional). En la interfaz, cada renglón de un proyecto ligado tiene un buscador con los ítems del centro de mando (`GET /api/integracion/inventario/items`, guardado en `CacheService` 6 h). Se puede dejar vacío: el centro de mando lo liga después. - `OrdenesCompra`: columnas nuevas **`sitio_entrega`** (se copia de la requisición al autorizar), **`paqueteria`**, **`guia`**, **`fecha_estimada`**, **`en_camino_en`**, **`en_camino_por`**. Los renglones de la OC guardan el `inventario_item` de su renglón de requisición (si compras divide o junta renglones, conserva el ítem). ## Paso 3 · Estado «En camino» - Nueva función *(UI)* **`marcarEnCamino(folio, {paqueteria, guia, fechaEstimada})`**: la puede usar **compras** o **el solicitante de esa requisición**. Solo desde Autorizada/Emitida o Pagada; guía obligatoria salvo «lo trae el proveedor»; fecha estimada no anterior a hoy. Con candado e idempotencia (`ENC_`), como las demás. - Requisición y OC pasan a **«En camino»** (estado nuevo entre Pagada y Recibida; también puede venir de Autorizada si se paga después). El solicitante lo ve en su historial con la fecha, sin montos. - Una OC «En camino» ya no se cancela desde el cotizador sin motivo explícito (avisa al centro de mando con `oc.cancelada`). - `CAMBIOS_API`: `marcarEnCamino` cambia `requisiciones` y `oc`. ## Paso 4 · Avisos al centro de mando (empujar) Función interna **`avisarPortal_(tipo, folioOC)`**: arma el cuerpo del contrato con los datos de la OC **sin montos** (nada de costo, importe, subtotal, IVA, total, tipo de cambio ni margen), lo firma y lo manda con `UrlFetchApp.fetch` (`muteHttpExceptions: true`). Se llama **después** de que el cambio quedó guardado: | Momento | `tipo` | |---|---| | `autorizarRequisicion` (emite la OC) | `oc.autorizada` | | `registrarPagoProveedor` | `oc.pagada` | | `marcarEnCamino` | `oc.en_camino` | | `cancelarOrdenCompra` | `oc.cancelada` | | `cerrarRequisicionCompras` (compras cierra a mano) | `req.cerrada` | - Solo si el proyecto de la OC está en `PORTAL_PROYECTOS`. - `evento_id` = `::` (n = cuántas veces se mandó ese tipo para esa OC), para que un reintento no duplique. - Si la respuesta no es 200 `ok`, el aviso se guarda en una pestaña nueva **`PortalPendientes`** del libro de compras (`evento_id`, `cuerpo`, `intentos`, `ultimo_error`, `creado`) y lo reintenta el activador del paso 6. La acción del usuario **no** falla por eso: se le muestra «Guardado. El centro de mando se actualizará en unos minutos». - Firma: `X-Hbox-Tiempo` = hora ISO con zona; `X-Hbox-Firma` = `sha256=` + hex de `Utilities.computeHmacSha256Signature(tiempo + "\n" + cuerpo, PORTAL_SECRETO)`. ## Paso 5 · Recepciones (consultar y cerrar solo) Función **`traerRecepcionesPortal()`**: `GET /api/integracion/cotizador/recepciones?desde=` (firmada igual, cuerpo vacío). Por cada recepción: - `completa: true` → cierra la requisición con `cerrarReq_` (estado Cerrada, OC Recibida) y nota «Recibido en ZLO por jesush en el centro de mando, 30-oct-2026». Si la requisición ya estaba Cerrada, no hace nada. - `completa: false` → solo agrega una nota de recepción parcial («Llegaron 3 de 4 soportes») y deja la requisición En camino. - Guarda `PORTAL_ULTIMA_CONSULTA` al final, solo si todo salió bien. - Bitácora en cada cierre automático (quién: «centro de mando»). ## Paso 6 · Activador cada 15 minutos `instalarActivadorPortal()` (se corre una vez desde el editor): `ScriptApp.newTrigger('cadaQuinceMinutos').timeBased() .everyMinutes(15)`. `cadaQuinceMinutos` = reintentar `PortalPendientes` + `traerRecepcionesPortal`. Aprovecha para llenar el caché (la idea de §6.14). ## Paso 7 · Pruebas y publicación 1. **Node** (`tests/portal.test.js`): el cuerpo nunca lleva montos; la firma coincide con un vector de prueba; sitio obligatorio solo en proyectos ligados; `evento_id` estable; un 500 del portal deja el aviso en `PortalPendientes`; recepción completa cierra, parcial solo anota, repetida no hace nada. 2. **PRUEBAS** con `"prueba": true`: requisición → autorizar → en camino → (Leo da «Recibir» en el centro de mando) → la requisición se cierra sola en ≤ 15 min. El centro de mando guarda las de prueba aparte y no toca el inventario. 3. Respaldos de los libros (como siempre), `clasp push`, comparar el editor, `clasp deploy` (misma API_URL), `wrangler deploy`. 4. Primera OC real de DROGHEDA: revisar juntos en el centro de mando (Compras y la pestaña del sitio en Inventario). ## Contrato (lo que espera el centro de mando) ```json POST /api/integracion/cotizador/evento { "evento_id": "OC-2026-0012:oc.en_camino:1", "tipo": "oc.en_camino", "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", "autorizo": "tania@helgen.tech", "renglones": [ { "n": 1, "descripcion": "Soporte para RTG", "unidad": "pza", "cantidad": 4, "item": "SOP-RTG" }, { "n": 2, "descripcion": "Tornillo 3/8 x 2 in", "unidad": "pza", "cantidad": 32, "item": null } ] } } → 200 { "ok": true, "estado_portal": "en-camino", "tarea_id": "t-oc-2026-0012", "por_ligar": [2] } → 200 { "ok": true, "repetido": true } (mismo evento_id) → 400 { "ok": false, "error": "Sitio inválido: MZO (usa VER, ZLO, ESE, LZC o QRO)" } → 401 { "ok": false, "error": "Firma inválida o vencida" } GET /api/integracion/inventario/items → 200 { "ok": true, "items": [ { "codigo": "SOP-RTG", "descripcion": "Soporte para RTG", "unidad": "pza", "categoria": "instalacion" } ] } GET /api/integracion/cotizador/recepciones?desde=2026-10-28T00:00:00-06:00 → 200 { "ok": true, "hasta": "2026-10-30T12:40:00-06:00", "recepciones": [ { "oc_folio": "OC-2026-0012", "req_folio": "REQ-2026-0031", "sitio": "ZLO", "completa": false, "recibio": "jesush", "cuando": "2026-10-30T12:31:00-06:00", "renglones": [ { "n": 1, "pedido": 4, "recibido": 4 }, { "n": 2, "pedido": 32, "recibido": 20 } ] } ] } ``` Hasta que el centro de mando publique estas rutas (fase 1 del lado del sitio), el cotizador puede programar y probar todo contra un servidor simulado en Node con estas mismas respuestas.