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

9.9 KiB

title date tags
Instrucciones para el proyecto del cotizador: ligar órdenes de compra al centro de mando 2026-10-27
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. 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_<idSolicitud>), 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 = <folio OC>:<tipo>:<n> (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=<PORTAL_ULTIMA_CONSULTA> (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)

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.