diff --git a/CLAUDE.md b/CLAUDE.md index 0eed3b0..2d9b2a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,8 +13,9 @@ Desde el **19/10/2026** (§11) la interfaz se sirve desde **Cloudflare** y habla 2. El servidor **recalcula** subtotal, IVA y total, asigna un **folio único** `HLG-AAAA-0001` y genera el **PDF**. 3. El PDF se guarda en Drive (carpeta por año) y la cotización queda como una fila en Google Sheets, con el vendedor del token. -Además: **proyectos** (opcionales: folio `PRY-AAAA-0001` y carpeta propia en Drive; §6.9), **cobranza por proyecto** +Además: **proyectos** (opcionales: folio `PRY-AAAA-0001-XXX` con clave de 3 letras y carpeta propia en Drive; §6.9), **cobranza por proyecto** (cotización aceptada, contrato firmado, esquema de pagos y pagos `PAG-AAAA-0001` con comprobante; solo rol de cobranza; §6.11), +**carga rápida y sincronización sin recargar** (una llamada `bootstrap` al entrar, caché por versiones y consulta cada 20 s; §6.14), **órdenes de compra por proyecto** (`OC-AAAA-0001` con PDF, proveedores y margen del proyecto; solo rol de compras; el margen solo lo ve cobranza; §6.12), **requisiciones de materiales** (`REQ-AAAA-0001`: un trabajador pide sin precios y adjunta la cotización del proveedor; compras la autoriza y genera la OC, la paga `PGP-AAAA-0001` y el trabajador confirma que recibió; §6.13), cotizaciones en **MXN o USD**, historial con búsqueda, catálogo de productos con precios (Google Sheets aparte), clientes sugeridos con @@ -28,7 +29,7 @@ autorrelleno, y claves SAT internas por producto para contabilidad (con sugerenc |---|---| | **Cotizador (lo usa el equipo)** | `https://cotizador-helgen.cotizador-helgen.workers.dev/cotizador/` (portada en `/`) | | Proyecto Apps Script de la API (`scriptId` en `.clasp.json`) | `1GsH8DjaFEZ9dltqBUTUEFE_frblSYimRRCvPkqKdRB5UhmBwwYHLSwyS` (nombre anterior "Cotizador Helgen — PRUEBAS interfaz web"; ver §9) | -| Deployment de la API (siempre actualizar este; `API_URL` en `web/cotizador/index.html`) | `AKfycbwBjF7ra_4O8INsxBq0eh-4r-BlSsJ4LLxcJgVCPRpx1U1a-Pz1CUSdYtvj_Mh0S6y2` (@10 al 23/10/2026 = requisiciones de materiales y pago a proveedores; @9 = resumen de cobranza; @8 = Compras: órdenes de compra, proveedores y margen; @7 = ocultar aceptar/firmar en proyectos cancelados; @6 = Cobranza y USD; @5 = nombre del vendedor en Historial; @4 = Proyectos; @3 = 19/10/2026; `access: ANYONE_ANONYMOUS` + token) | +| Deployment de la API (siempre actualizar este; `API_URL` en `web/cotizador/index.html`) | `AKfycbwBjF7ra_4O8INsxBq0eh-4r-BlSsJ4LLxcJgVCPRpx1U1a-Pz1CUSdYtvj_Mh0S6y2` (@11 al 23/10/2026 = carga rápida, sincronización sin recargar, requisiciones en cualquier proyecto y clave de proyecto; @10 = requisiciones de materiales y pago a proveedores; @9 = resumen de cobranza; @8 = Compras: órdenes de compra, proveedores y margen; @7 = ocultar aceptar/firmar en proyectos cancelados; @6 = Cobranza y USD; @5 = nombre del vendedor en Historial; @4 = Proyectos; @3 = 19/10/2026; `access: ANYONE_ANONYMOUS` + token) | | Client ID de OAuth (`CLIENT_ID_GOOGLE` en `web/cotizador/index.html` y `Code.js`) | `884526916417-cn5fh45io0nnpvlad2439thkpius4063.apps.googleusercontent.com` (no es secreto) | | Carpeta maestra en Drive "Cotizador Helgen" | `1lf3Z4d3e_oqFbYJse4QbyYkWowYT9HCo` | | Base de datos "Cotizador Helgen — Base de datos" (`SPREADSHEET_ID`) | `1ozFsyt1OjSnSv-PVpRWZjgqeWt6voQREbjvOJAXPEaI` | @@ -41,11 +42,12 @@ autorrelleno, y claves SAT internas por producto para contabilidad (con sugerenc | Respaldos antes de Proyectos | Carpeta "Respaldos 2026-10-20" `1rFGY_k_YRRo243p3xL2RpibQ9jkEo634`: Base de datos `1PBQvmwI412IMyJb74qBWN5ZcyhSuxK3xGU9cV5KPxTE`, Catálogo `1GxBpAvQxVNJyFJSLnaY9umA5L2qf4dqX36S4uehHUNc` (antes de la pestaña Proyectos y la columna T) | | Respaldos antes de Cobranza | Carpeta "Respaldos 2026-10-20 (antes de Cobranza)" `1C_yGbX0Gk_cZBwegdswPFvuzCVDdyTLF` (en la carpeta maestra): Base de datos `14C3d7w2ouaygtCGi0BP1QocGiqG4gf5Umh9O4KHgdow`, Catálogo `140_1sfwXF9u_aL1egDPYSbsE0rMwELGPZ5rkGVdzQnc` (antes de las columnas U:V, la columna I del catálogo y la pestaña "Tipo de cambio"). Verificadas contra los originales | | Respaldos antes de Compras (20/10/2026) | Carpeta "Respaldos 2026-10-20 (antes de Compras)" `1QWHfc7K5w5lk5vU_jt1lCSrn4PY5YyKA` (en la carpeta maestra): Base de datos `1dCTVyMH50_55WzIfN-waUvoXaKYOliet_scZSCyKHYo`, Catálogo `1Y-Ho8kKfZsLd0lris8IIHOR5X_LExRcxThbje0Z5G9s`. Libro de cobranza: carpeta "Respaldo 2026-10-20 (antes de Compras)" `1H_BjW-VEdKbO-4LpF5aeKvFWwPiprKu2` **dentro de la carpeta de cobranza** (nunca en la maestra), copia `1KIhAfkEZhMWzzp81Y8jMFjSs6a2Ki6L7LcO13cvbFgU` (solo encabezados). Las tres verificadas pestaña por pestaña contra los originales. Copias hechas con el conector de Drive: Workspace les puso "helgen.tech: lector" (§6.11); el usuario dejó en Restringido la carpeta de respaldo de cobranza el 21/10/2026 (comprobado: carpeta y copia solo con efrain@, tania@ y leo@) | +| Respaldos antes de carga rápida y claves (23/10/2026) | Base de datos y Catálogo: carpeta "Respaldos 2026-10-23 (antes de carga y claves)" `18i8S6NsEdQqs1zd83ehP8g9bxL6e8Adc` (en la carpeta maestra), copias `19sW5zKbSSl_-avfJ131kM8UgnM6aOFIlXQKwKuGw07g` y `1ycjtw7b6TLYYYeMiRhfVakL0BsMXdrekSiaPw8TCEDI`. Libro de cobranza: carpeta "Respaldo 2026-10-23 (antes de carga y claves)" `1YvtmlKR_bDgYAvsTv2I7txD85zBM8SMO` **dentro de la carpeta de cobranza**, copia `1a7FpjNTYkOh7IiqnGbLIiLET1n3Nd5gDQcglafEAYN4`. Libro de compras: carpeta con el mismo nombre `1DRos9fjHh5SeZle49AjqTYXwStfbQemW` **dentro de la carpeta de compras**, copia `1P3thpiq6jhnWESE8PnfYLlem5cQ3NRW6OfO6lUcEMpY` (cobranza y compras solo encabezados). Las 4 verificadas pestaña por pestaña. Las privadas nacieron con "helgen.tech: lector" aunque se crearon con herencia desactivada; el usuario las dejó en Restringido el 23/10/2026 (comprobado: carpetas con efrain@, tania@ y leo@; copias solo efrain@) | | Respaldos antes de Requisiciones (23/10/2026) | Base de datos: carpeta "Respaldos 2026-10-23 (antes de Requisiciones)" `1rUAEWtUV_z4uXJCS406NOPQLc4BK7YtK` (en la carpeta maestra), copia `1N3NZVURGWyHDQ6fXzUG7AtHbr6YhK7xHwi5YFNaZpro`. Libro de compras: carpeta "Respaldo 2026-10-23 (antes de Requisiciones)" `17Yn700vHeCNebrDj-4mXzeP5YxgXfW2G` **dentro de la carpeta de compras**, copia `1CiMuP7Z2oBh_Lrp4ma1LG7vSKFc69d1a_P46edfFDDE` (solo encabezados). Verificadas pestaña por pestaña. Nacieron con "helgen.tech: lector" (§6.11); el usuario dejó en Restringido la carpeta de compras el 23/10/2026 (comprobado: carpeta con efrain@, tania@ y leo@; copia solo efrain@) | | **Manual del Cotizador Helgen** (para personas) | Documento de Claude Docs `https://claude.ai/artifact/AN8w8T2SKmDXSTa1zpFt2C` (id `AN8w8T2SKmDXSTa1zpFt2C`; pestaña `1cdc8401-1062`, cuerpo `86e49b65-80a1`). Se actualiza **junto con este archivo** (regla 14 y §8) | | Worker de Cloudflare | `cotizador-helgen`, cuenta de efrain@helgen.tech `badd8bca60b008822a73ca24e983ff43` | | Dueño de todo / cuenta de clasp | `efrain@helgen.tech` (la API se ejecuta como esta cuenta) | -| Proyecto de prueba "PRUEBAS — Cotizador Helgen (no usar)" | `1wp8_SQofXXUuMkUjmDyyUxYLxx-LdFv-20JaXE7q9entLfEvDKgYtnt0`; deployment `AKfycbwVu2q5…` retirado el 20/10/2026 (404). Para Cobranza (20/10/2026): deployment `AKfycbz9naWOw4Jj0qlta0RjCNifgZLvvJqXpqRnrrsALewSnmn7ydnEIw_zYriIHZjbQ5ugQw`, **retirado** al publicar (404); su `Pruebas.js` crea "PRUEBAS — Cobranza" y tiene `pruebasRondaSinRol()` / `pruebasRondaConRol()`. Para Compras (20/10/2026): deployment `AKfycbxK3ldf…`, **retirado** al terminar (404); `Pruebas.js` con `configurarCompras()` y rondas C1–C3 (§10.7). Para Requisiciones (23/10/2026): deployment `AKfycbybSSE0…`, **retirado** al publicar (404); `Pruebas.js` con `configurarRequisiciones()` y rondas R1–R3 (§10.8). Está en la carpeta `1w-T05ql…` (lo movió el usuario). Solo datos inventados (§10.5). **No es la API.** | +| Proyecto de prueba "PRUEBAS — Cotizador Helgen (no usar)" | `1wp8_SQofXXUuMkUjmDyyUxYLxx-LdFv-20JaXE7q9entLfEvDKgYtnt0`; deployment `AKfycbwVu2q5…` retirado el 20/10/2026 (404). Para Cobranza (20/10/2026): deployment `AKfycbz9naWOw4Jj0qlta0RjCNifgZLvvJqXpqRnrrsALewSnmn7ydnEIw_zYriIHZjbQ5ugQw`, **retirado** al publicar (404); su `Pruebas.js` crea "PRUEBAS — Cobranza" y tiene `pruebasRondaSinRol()` / `pruebasRondaConRol()`. Para Compras (20/10/2026): deployment `AKfycbxK3ldf…`, **retirado** al terminar (404); `Pruebas.js` con `configurarCompras()` y rondas C1–C3 (§10.7). Para Requisiciones (23/10/2026): deployment `AKfycbybSSE0…`, **retirado** al publicar (404); `Pruebas.js` con `configurarRequisiciones()` y rondas R1–R3 (§10.8). Para carga rápida y claves (23/10/2026): deployment `AKfycbxnFS1f…`, **retirado** al publicar (404); quedó con el código de la API @11 y en la ronda R2 (§10.9). Está en la carpeta `1w-T05ql…` (lo movió el usuario). Solo datos inventados (§10.5). **No es la API.** | | **Proyecto anterior (EN DESUSO)** | `1X-qSKgvqKwuDj4iPZBhXlakR5udau9ibVMevhwvlLXgtSkjV4IoQxqwU`; deployment `AKfycbxahS3…` en @10 = aviso "El cotizador se mudó". Código en `anterior/` (§11) | Los IDs que usa el código están en **Script Properties** del proyecto `1GsH8Dja…` (ver §5). Esta tabla es solo referencia. @@ -102,6 +104,30 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e - `migrarConfig_(hoja)` — actualiza hojas `Config` viejas: `TERMINOS` → `TERMINOS_HELGEN` + `TERMINOS_ENERGY`, agrega claves nuevas de `CONFIG_INICIAL`, cambia valores de ejemplo (`CONFIG_EJEMPLO_ANTERIOR`) por los reales y repara celdas `#ERROR!`. +### 4.1.1 Carga inicial, caché y sincronización (sección 1.1 del archivo; ver §6.14) +- `bootstrap()` *(UI, al cargar)* — única llamada al entrar: `versiones`, `permisos` (`permisosUsuario_`), `soloRequisicion`, IVA, + vigencia, catálogo (sin precios con `SOLO_REQUISICION`), clientes y proyectos (cotizar) y, según el rol, `req: {proyectos, mias}`, + `com: {proyectos, requisiciones}`, `cob: {proyectos, resumen}`. Las listas pesadas (mis requisiciones, requisiciones de compras, + resumen de cobranza) solo van si ya están en caché (si no, `null` y la interfaz las pide al abrir su pestaña). Cada pieza va en + `intentar_`: si falla, `null` y su texto en `errores`. `getDatosIniciales()` = alias (interfaces viejas en caché del navegador). +- `sincronizar(versiones, ctx)` *(UI, cada 20 s)* — revisa las hojas que se editan a mano (`proyectosCache_(true)`…), compara versiones + y devuelve `{versiones, permisos, soloRequisicion, cambios}` más **solo** las listas o vistas que cambiaron y que el rol puede ver: + `catalogo`, `clientes`, `proyectos`, `reqProyectos`, `misReq`, `comProyectos`, `comRequisiciones` (con el filtro `ctx.rcBuscar/rcEstado`), + `requisicion` (`ctx.req`), `comprasProyecto` (`ctx.comProyecto`), `cobProyectos`, `cobranzaProyecto` (`ctx.cobProyecto`), `resumen` + (`ctx.cobResumen`). Con `SOLO_REQUISICION` nunca manda lo demás aunque el contexto lo pida. +- Versiones: `COLECCIONES` (proyectos, contratos, cotizaciones, catalogo, requisiciones, oc, pagosCobranza, pagosProveedores) en + Script Properties `VER_`; `CAMBIOS_API` dice qué sube cada función; `doPost` llama `subirVersiones_` (con el candado de + script) **después** de que la escritura terminó bien (no si devolvió `{confirmar}`). Durante una escritura `SIN_CACHE_` = lo que + devuelve sale de la hoja. **Toda función nueva que escriba debe agregarse a `CAMBIOS_API`.** +- Caché: `enCache_(base, colecciones, ttl, fn)` (clave = base + versiones; sin `fn` solo lee), `leerCache_`/`guardarCache_` (JSON en trozos + de 30 000 caracteres), `TTL_CACHE` = 6 h. `fuenteConHuella_(nombre, colección, cadaSeg, leer, revisar)` para hojas editables a mano: + dato 6 h; con `revisar` (solo `sincronizar`) relee la hoja si pasaron `cadaSeg` (marca `R_`) y si la huella MD5 cambió con la + misma versión, sube la versión. Fuentes: `proyectosCache_` (5 min), `contratosCache_`, `catalogoCache_`, `clientesCache_` (10 min); + `configLigera_`. `REVISAR_A_MANO_` asegura esas fuentes antes de armar una clave. `MEMO_` = lo leído en la petición (propiedades, + fuentes); `doPost` lo limpia. `enRol_` lee de `propiedades_()`. +- En caché por versión: `listarProyectos`, `listarProyectosCobranza/Compras/Requisicion`, `misRequisiciones` (por correo), + `listarRequisiciones` (por filtro), `resumenCobranzaGeneral`, historial (folio con clave). Las vistas de detalle no. + ### 4.2 Catálogo de productos - `hojaCatalogo_()` — abre "Catálogo Helgen"; si no existe, `crearCatalogo_()` (bajo candado). Llama `asegurarColumnasSat_()`. - `crearCatalogo_()` — crea el archivo con formato, validaciones (clave única con `COUNTIF`, precio ≥ 0), migra la pestaña @@ -132,8 +158,8 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e - `existeEnSat_(pestana, clave)` — valida una clave con `TextFinder` (celda completa). ### 4.4 Datos para la interfaz -- `getDatosIniciales()` *(UI, al cargar)* — IVA, vigencia, marcas, catálogo, clientes, `usuario` (correo), `vendedor` (`{email, nombre, apellido}`) - y `permisos` (`{cobranza, desbloqueo, compras, requisicion}`; solo para mostrar pestañas: cada función lo vuelve a revisar). +- `getDatosIniciales()` — desde el 23/10/2026 es alias de `bootstrap()` (§4.1.1): IVA, vigencia, marcas, catálogo, clientes, `usuario`, + `vendedor` y `permisos` (`{cobranza, desbloqueo, compras, requisicion}`; solo para mostrar pestañas: cada función lo vuelve a revisar). Con `SOLO_REQUISICION` devuelve `soloRequisicion: true`, catálogo sin precios y sin clientes ni proyectos (§4.5.4). - `getClientes()` *(UI)* — clientes que ya cotizaron, calculados desde la hoja `Cotizaciones` (`clientesDesdeFilas`). - `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa/proyecto; `vendedor` = @@ -157,14 +183,20 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e ### 4.5.1 Proyectos (sección 4.1 del archivo) - `hojaProyectos_()` — pestaña `Proyectos` de la base de datos; la crea sola (lista desplegable de Estado en G) y reescribe solo la fila 1 si faltan encabezados (`ENCABEZADOS_PROYECTOS`). -- `listarProyectos(busqueda, incluirInactivos)` *(UI)* — busca por folio, cliente o nombre; por omisión solo `Activo` y **sin - contrato firmado** (el buscador de cotizaciones; `foliosConContratoFirmado_`); máx. 50, recientes primero. No devuelve `carpeta_id`. -- `crearProyecto(p)` *(UI)* — exige vendedor; `normalizarProyecto`; candado + idempotencia (`PRY_`, 6 h); - folio `PRY-AAAA-NNNN`; **primero** crea la carpeta y **después** la fila (estado `Activo`). Devuelve `{folio, …, url}`. +- `listarProyectos(busqueda, incluirInactivos)` *(UI)* — busca por folio (con o sin clave), clave, cliente o nombre; por omisión solo + `Activo` y **sin contrato firmado** (el buscador de cotizaciones; `contratosCache_`); máx. 50, recientes primero; `proyectoParaLista_` + (con `clave` y `folioVisible`; sin `carpeta_id`). +- `crearProyecto(p)` *(UI)* — exige vendedor; `normalizarProyecto` (incluye `clave`: la capturada o `claveSugerida`); candado + + idempotencia (`PRY_`, 6 h); folio `PRY-AAAA-NNNN` + clave en la columna L; **primero** crea la carpeta + ("PRY-AAAA-NNNN-XXX — Cliente — Nombre") y **después** la fila (estado `Activo`). Devuelve `{folio, clave, folioVisible, …, url}`. +- `buscarProyecto_` acepta el folio con o sin clave (`folioInterno`). `guardarCotizacion` guarda siempre la llave interna en la columna T. +- `asignarClavesProyectos()` (editor; vista previa en el registro) y `asignarClavesProyectosAplicar()` — clave sugerida a los proyectos + sin clave y su carpeta principal (por `carpeta_id`) pasa a "PRY-AAAA-NNNN-XXX — …"; una carpeta que no empieza con su folio + (renombrada a mano) no se toca; los que ya tienen clave no cambian; sube la versión de proyectos. - `buscarProyecto_(hoja, folio)`, `carpetaProyectos_()` (`PROYECTOS_FOLDER_ID`; si falta o está en la Papelera la crea en la carpeta maestra = padre de la base de datos), `subcarpeta_(padre, nombre)` (por nombre, ignora la Papelera, crea si falta), `crearCarpetaProyecto_`, `subcarpetaProyecto_(proyecto, nombreSub)` (ver §6.9). -- `getDatosIniciales()` incluye `proyectos` (activos; si falla, `[]` y la app carga igual). +- `bootstrap()` incluye `proyectos` (activos sin contrato firmado; si falla, `[]` y la app carga igual). ### 4.5.2 Cobranza (sección 4.2 del archivo; ver §6.11) - Todas empiezan con `exigirCobranza_()` (token + correo en `ROL_COBRANZA`; si no, `errorPermiso_` → `codigo: "PERMISO"`). @@ -194,13 +226,14 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e ### 4.5.3 Compras (sección 4.3 del archivo; ver §6.12) - Todas empiezan con `exigirCompras_()` (token + correo en `ROL_COMPRAS`; si no, `codigo: "PERMISO"`). -- *(UI)* `listarProyectosCompras(busqueda)` (solo `Activo` con contrato firmado; con `moneda` y `marca`, **sin montos**), +- *(UI)* `listarProyectosCompras(busqueda)` (`Activo` y `Cerrado`, con o sin contrato; `monedaDeProyecto_`: `moneda`, `marca`, `sinContrato`, `contrato`; **sin montos**), `getComprasProyecto(folio)` (`vistaCompras_`: proyecto, `moneda`, `marca`, órdenes con su `reqFolio`; sin contrato ni margen; solo consulta), `cancelarOrdenCompra(folio, motivo)` (solo Emitida: una Pagada o Recibida no se cancela; motivo ≥ 5; si la orden viene de una requisición Autorizada, la requisición regresa a Solicitada sin `oc_folio`). - **Desde el 23/10/2026 no hay emisión directa ni "Recibida" desde compras**: `guardarOrdenCompra` y `marcarOcRecibida` ya no existen. - La orden la emite `emitirOrdenCompra_(libro, datos, proyecto, reqFolio)` (dentro del candado de `autorizarRequisicion`: proyecto - Activo y firmado; folio `siguienteFolio_('OC', hoja, 'CONSECUTIVO_OC_')`; proveedor; PDF con la línea "Requisición: REQ-…"; + La orden la emite `emitirOrdenCompra_(libro, datos, proyecto, reqFolio)` (dentro del candado de `autorizarRequisicion`: proyecto no + `Cancelado`, con o sin contrato; moneda y línea de `monedaDeProyecto_(contratosDelLibro_()…)`: la aceptada o MXN y sin línea; el PDF + lleva el folio con clave; folio `siguienteFolio_('OC', hoja, 'CONSECUTIVO_OC_')`; proveedor; PDF con la línea "Requisición: REQ-…"; fila con `req_folio`; Bitacora) y queda Recibida al cerrar su requisición (§4.5.4). - Internas: `carpetaCompras_`, `libroCompras_`, `libroComprasSiExiste_`, `hojaCompras_`, `ordenDesdeFila_`, `ordenesDeProyecto_`, `proveedoresDelLibro_`, `guardarProveedor_` (alta o actualización por id; si llega con RFC y existía sin RFC, esa fila toma el RFC), @@ -215,8 +248,8 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e `doPost`, toda función fuera de `FUNCIONES_SOLO_REQUISICION` se niega con `PERMISO` ("Tu cuenta solo tiene acceso a Requisiciones."), antes de ejecutarla; `getDatosIniciales` les manda `soloRequisicion: true`, catálogo **sin precios** y nada de clientes, proyectos ni tipo de cambio. -- *(UI, solicitante)* `listarProyectosRequisicion(busqueda)` (Activos con contrato firmado; solo `{folio, nombre}`; busca por folio y - nombre, no por cliente), `misRequisiciones()` (**filtra por el correo del token**; `reqParaSolicitante_`: estado, motivo, renglones, +- *(UI, solicitante)* `listarProyectosRequisicion(busqueda)` (`Activo` y `Cerrado`, con o sin contrato; solo `{folio, clave, folioVisible, + nombre, estado}`; busca por folio, clave y nombre, no por cliente), `misRequisiciones()` (**filtra por el correo del token**; `reqParaSolicitante_`: estado, motivo, renglones, eventos, `puedeEditar`/`puedeCancelar`/`puedeCerrar`; nunca orden, montos, cliente ni pagos), `guardarRequisicion(p)` (nueva o reenvío de una Devuelta con `p.folio`; idempotencia `REQ_`; cotización obligatoria al crear, opcional al reenviar), `cancelarRequisicion(folio, motivo)` (Solicitada o Devuelta), `cerrarRequisicion(folio, notas)` ("Recibí el material": solo el @@ -250,6 +283,9 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e normalizados. `normalizarProyecto(p)`, `nombreCarpetaProyecto(folio, cliente, nombre)` (quita `/ \ : * ? " < > |`), `proyectosDesdeFilas`, `filtrarProyectos`. - `normalizarProducto(p)`, `indiceDeClave(claves, clave)`. +- Clave de proyecto: `PALABRAS_SIN_CLAVE`, `claveSugerida(nombre)` (regla en §6.9), `validarClaveProyecto(clave, nombre)` (exactamente + 3 letras A–Z; vacía = sugerida), `folioVisible(folio, clave)`, `folioInterno(texto)` ("PRY-2026-0002-DRO" → "PRY-2026-0002"). + `proyectosDesdeFilas` agrega `clave` (columna L) y `folioVisible`; `filtrarProyectos` busca también por ellos. - `nombresDeVendedores(filas)` (correo en minúsculas → "Nombre Apellido"), `vendedorDeFila(r, nombres)`. - `terminosParaPdf(cfg, marca)` — renglones de T&C de la marca; agrega la línea de vigencia si falta. - `textoCelda(s)` — antepone `'` a texto que empiece con `= + - @` para que Sheets no lo lea como fórmula. @@ -294,8 +330,17 @@ si el servidor lo pide), `guardarEsquemaCob`, `firmarCob`, `desbloquearCob`, `re `recalcular` (vista previa; el servidor recalcula), `guardar`, `limpiar`, `cargarHistorial`, `recibirClientes`, `autorrellenarCliente`, `pintarDatalists`, `cargarCatalogo`, `pintarCatalogo`, `editarProd`, `guardarProd`, `sugerirSat`, `verTab`. Proyectos (módulo "1 · Cliente y proyecto"): `recibirProyectos`, `proyectoElegido`, `alElegirProyecto` (si "Nombre" está vacío, -lo llena buscando el cliente por `cliente_id`), `abrirNuevoProyecto`, `crearProyecto` (usa el cliente de los campos de arriba). -Autocompletado con ``: clientes (en "Nombre"), proyectos activos (`PRY-… — cliente — nombre`), productos por +lo llena buscando el cliente por `cliente_id`), `abrirNuevoProyecto`, `crearProyecto` (usa el cliente de los campos de arriba; manda +`clave`). Clave: `#npClave` (se llena con `claveSugerida`, copia de la de `Code.js`, mientras no se edite a mano: `CLAVE_A_MANO`), +`#npFolio`, `pintarFolioNuevo`. `folioVis(p)` muestra `PRY-AAAA-NNNN-XXX` en todas las listas; `p.folio` (interno) es lo que se manda. +Arranque y sincronización (§6.14): `aplicarPermisos`, `leerRolesGuardados`/`guardarRoles` (localStorage `cotizadorHelgenRoles:`), +`rolesDeUltimaSesion` (evento `sesion-lista` de la capa de sesión), `recibirBootstrap`, `CARGADO` (qué listas llegaron), `VERS`, +`sincronizarAhora`, `contextoSync`, `aplicarSync`, `iniciarSincronizacion` (`setInterval` 20 s si `document.visibilityState` es +visible, `visibilitychange`, foco en los buscadores `proyecto`/`resProyecto`/`cobProyecto`/`comProyecto`/`reqProyecto`), +`sinInterrumpir`/`listoVista` (si hay captura en `cobVistaProyecto`, `comVistaReq` o `comModOrdenes`, aviso "Otra persona cambió… +Actualizar" en vez de repintar), `recibirProyectosReq/Com/Cob`, `COM_FILTRO`. `verTab(t, recienCargado)` ya no vuelve a pedir lo +que trajo el bootstrap. La capa de sesión expone `window.sesionVigente()` y `window.correoSesion()`. +Autocompletado con ``: clientes (en "Nombre"), proyectos activos (`PRY-…-XXX — cliente — nombre`), productos por descripción y por clave, unidades SAT. Capa de sesión y `fetch` en `