41 KiB
Cotizador Helgen — contexto para Claude Code
Léelo completo antes de cambiar algo. Resume el sistema, dónde viven los datos y las decisiones ya tomadas (con su porqué) para no repetir errores. Textos de la interfaz y comentarios del código: español de México.
1. Qué es
App web interna de Helgen Servicios Especializados para hacer cotizaciones, solo para cuentas @helgen.tech.
Desde el 19/10/2026 (§11) la interfaz se sirve desde Cloudflare y habla por doPost con una API en
Google Apps Script (sin servidor propio), con sesión de Google verificada en cada llamada. Flujo:
- El usuario inicia sesión con Google y llena el formulario (cliente, conceptos, notas, línea de negocio Helgen / Helgen Energy).
- El servidor recalcula subtotal, IVA y total, asigna un folio único
HLG-AAAA-0001y genera el PDF. - 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), historial con búsqueda, catálogo de productos con precios (Google Sheets aparte), clientes sugeridos con
autorrelleno, y claves SAT internas por producto para contabilidad (con sugerencias por palabras o con IA).
Helgen Energy es la submarca de paneles solares: mismo folio y diseño, pero sus propios Términos y Condiciones.
2. Datos clave (IDs y URLs)
| Qué | Valor |
|---|---|
| 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 (@5 al 20/10/2026 = 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 |
Catálogo "Catálogo Helgen" (CATALOGO_ID) |
11Rr0G7ZHE8PqXWsw9OpPPfGLQx8Yz7g8h1wkjJY2ahQ |
Carpeta PDF "Cotizaciones Helgen (PDF)" (FOLDER_ID) |
1CDN1LyOlA1L6z5dvBp49tlqu0fDoxdX8 (subcarpeta 2026 = 1X6m0nexpc3TzJd9XWBmGOAeZuiJE3Pig) |
"Catálogos SAT (CFDI 4.0)" (SAT_ID) |
1B2t9tXuEj1i9JO9ybqsuH4PN1K47CqMZgoEBzd-Dmc8 (subcarpeta "Catálogos SAT" 15OYcUp41onVJoW7--b3gnLZY8ymvYbUZ) |
| Respaldos de la migración | Carpeta "Respaldos 2026-10-19" 1hxx0tgh4cLZMqoyf-eT8l14UAde7Y-L1 (copias de base de datos y catálogo antes del cambio) |
| 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) |
| 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)" (ARCHIVADO) | 1wp8_SQofXXUuMkUjmDyyUxYLxx-LdFv-20JaXE7q9entLfEvDKgYtnt0; deployment AKfycbwVu2q5… retirado el 20/10/2026 (la URL da 404). 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.
3. Archivos del proyecto (carpeta local oficial ~/Proyectos/cotizador-helgen)
Carpeta oficial desde el 19/10/2026: ~/Proyectos/cotizador-helgen (abrir aquí las sesiones de Claude Code).
Antes estaba en ~/Downloads/cotizador-helgen (se mandó a la Papelera tras copiarla y verificarla).
~/Proyectos/VIEJA-NO-USAR-cotizador-helgen-web es una copia vieja (mañana del 19/10, antes de la sesión con token):
no trabajar ahí. Su .clasp.json (que apunta a 1GsH8Dja…) se renombró a .clasp.json.desactivado el 19/10/2026
para que un clasp push desde ella no pise la API con código viejo; no volver a renombrarlo.
| Archivo | Para qué |
|---|---|
Code.js |
Todo el backend: API doPost con sesión + lógica (ver §4). Se sube a Apps Script. |
Logo.js |
Logos en base64: LOGO_HELGEN_AZUL (PDF, fondo blanco, 700×200) y LOGO_HELGEN_BLANCO. Se sube. |
Index.html |
Interfaz vieja de HtmlService. Se sube pero no se usa (doGet ya no la sirve; su API_URL es un marcador). La interfaz real es web/cotizador/index.html. |
appsscript.json |
Manifiesto: zona America/Mexico_City, V8, web app executeAs: USER_DEPLOYING, access: ANYONE_ANONYMOUS. Se sube. |
.clasp.json |
Liga la carpeta al proyecto 1GsH8Dja…. No cambiar el scriptId. |
.claspignore |
Lista blanca: solo se suben Code.js, Logo.js, Index.html, appsscript.json (excluye anterior/). |
web/ |
Portada (index.html, logo-helgen-blanco.png) y la interfaz real web/cotizador/index.html (§10). No se sube a Apps Script. .assetsignore excluye Icon/.DS_Store. |
wrangler.jsonc |
Publica web/ en Cloudflare como Worker solo de archivos estáticos. |
tests/ |
Pruebas en Node con servicios de Google simulados (ver §8). No se suben. cotizacion.test.js acepta Code.js (actual) o Code.gs (anterior). |
anterior/ |
Código del proyecto viejo (@9), su .clasp.json (1X-qSKgv…), LEEME.md con cómo regresar y el CLAUDE.md previo a la migración. No tocar salvo para regresar. |
herramientas/extraer-catalogos-sat.js |
Convierte el Excel oficial del SAT en los CSV que importa la app (ver §6.5). |
GUIA.md |
Guía para personas (instalación inicial). Desactualizada: describe el proyecto anterior. |
package.json |
Solo @google/clasp como dependencia de desarrollo. |
Icon |
Archivo vacío de macOS; ignorarlo. |
Git inicializado el 19/10/2026 (rama main; nombre y correo configurados solo en este repositorio). El .gitignore
excluye node_modules/, .wrangler/, Icon, .DS_Store y archivos de credenciales (.env, .clasprc.json, *.pem…).
Respaldo en el repositorio privado de GitHub https://github.com/efrainsosa27/cotizador-helgen.git (remoto origin).
Primer push hecho el 19/10/2026 con un token personal (llavero osxkeychain). Desde el 19/10/2026 git usa GitHub CLI:
gh v2.102.0 en ~/.local/bin/gh (binario oficial de cli/cli, sin Homebrew porque pide administrador; ~/.zprofile
agrega ~/.local/bin al PATH), sesión efrainsosa27 con gh auth login --web y gh auth setup-git (en ~/.gitconfig,
solo para github.com). git push ya no pide token. Sin llave SSH. Si la sesión expira: gh auth login --hostname github.com --git-protocol https --web.
Si git fetch falla con bad object refs/Icon, macOS dejó archivos Icon vacíos dentro de .git; borrarlos con
find .git -name $'Icon\r' -size 0 -delete.
4. Funciones de Code.js
Convención: las que terminan en _ son internas; las demás las llama la interfaz (por doPost, ver API_FUNCIONES)
o son lógica pura (sección 5 del archivo, sin servicios de Google, probables en Node).
4.1 Entrada, sesión e instalación
doGet()— solo responde un texto con la dirección del cotizador (no sirve la app; ver §10.3).doPost(e)— verifica el token (verificarTokenGoogle_) antes de buscar la función; solo ejecuta las deAPI_FUNCIONES. Pone el vendedor enVENDEDOR_durante la petición y lo limpia enfinally. Detalle en §10.3.verificarTokenGoogle_(token),validarClaimsToken(...)(pura),usuarioActual_()(reemplazaSession.getActiveUser()).setup()— crea la base de datos (pestañasCotizacionesyConfig) y la carpeta de PDF, y guarda sus IDs.db_()la ejecuta sola si faltanSPREADSHEET_ID/FOLDER_ID(bajo candado).db_()— abre la base de datos; si faltanSPREADSHEET_ID/FOLDER_ID, corresetup().getConfig_()— leeConfigcomo objeto; antes llamamigrarConfig_().migrarConfig_(hoja)— actualiza hojasConfigviejas:TERMINOS→TERMINOS_HELGEN+TERMINOS_ENERGY, agrega claves nuevas deCONFIG_INICIAL, cambia valores de ejemplo (CONFIG_EJEMPLO_ANTERIOR) por los reales y repara celdas#ERROR!.
4.2 Catálogo de productos
hojaCatalogo_()— abre "Catálogo Helgen"; si no existe,crearCatalogo_()(bajo candado). LlamaasegurarColumnasSat_().crearCatalogo_()— crea el archivo con formato, validaciones (clave única conCOUNTIF, precio ≥ 0), migra la pestañaCatalogovieja de la base de datos (sin el ejemploEJ-001, luego la borra) y lo mueve junto a la base de datos.asegurarColumnasSat_(hoja)— agrega encabezadosClave SAT/Unidad SAT(G:H, formato texto) a catálogos viejos.getCatalogo_(hoja?)— lista{clave, descripcion, unidad, precio, claveSat, unidadSat}.getCatalogo()(UI) — productos + URL del archivo + unidades SAT para la lista de selección.guardarProducto(p)(UI) — alta o edición (p.claveOriginal= edición, permite cambiar la clave). Valida connormalizarProducto, rechaza claves duplicadas (sin distinguir mayúsculas) y claves/unidades SAT que no existan. "Actualizó" =usuarioActual_().
4.3 Catálogos del SAT
libroSat_()— abre "Catálogos SAT (CFDI 4.0)"; si no existeSAT_ID,importarCatalogosSat_().importarCatalogosSat_()— busca en DriveSAT_c_ClaveProdServ.csvySAT_c_ClaveUnidad.csv, crea el libro con pestañasClaveProdServ,ClaveUnidadyFuente, y lo deja en la carpeta de los CSV.getUnidadesSat_()—[[clave, nombre], …]de unidades.sugerirClavesSat(texto)(UI) — si el texto es una clave de 7–8 dígitos la busca exacta; si no, toma ~40 candidatos (candidatosSat) y, si existeANTHROPIC_API_KEY, la IA elige las 5 mejores con razón (ordenarConIaSat_); si no hay key o falla, devuelve los 8 mejores por palabras.ordenarConIaSat_(texto, candidatos)— llama a la API de Claude (UrlFetchApp), guarda el resultado 6 h en caché;nullsi falla o no hay key.probarIaSat()— correr desde el editor: muestra en el registro sugerencias de ejemplo.existeEnSat_(pestana, clave)— valida una clave conTextFinder(celda completa).
4.4 Datos para la interfaz
getDatosIniciales()(UI, al cargar) — IVA, vigencia, marcas, catálogo, clientes,usuario(correo) yvendedor({email, nombre, apellido}).getClientes()(UI) — clientes que ya cotizaron, calculados desde la hojaCotizaciones(clientesDesdeFilas).listarCotizaciones(busqueda)(UI) — últimas 100 cotizaciones, filtro por folio/cliente/empresa/proyecto;vendedor=vendedorDeFila: nombre + apellido de la fila; en filas viejas (sin Q:S), el nombre que ese correo tiene en otras filas (nombresDeVendedores, gana la más reciente); si no se conoce, el correo (vendedor_emailo "Elaboró"). No modifica la hoja.
4.5 Guardar cotización
guardarCotizacion(payload)(UI) — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT a cada concepto que venga del catálogo (si falla, guarda igual), toma el candado, revisa idempotencia (payload.idSolicitudenCacheService, 6 h), asigna folio, genera PDF, lo guarda en la carpeta del año, reescribe encabezados si no coinciden conENCABEZADOS_COTIZACIONESy agrega la fila. Devuelve{folio, url, total, proyectoFolio}. Si vienepayload.proyectoFolio, dentro del candado valida que el proyecto exista y estéActivo(si no, error y no se gasta folio) y guarda el PDF en<carpeta del proyecto>/Cotizacionesen lugar de la carpeta del año.siguienteFolio_(prefijo, hoja, baseContador)—max(<baseContador>AAAA, mayor folio del año en la hoja) + 1.baseContador=CONSECUTIVO_(cotizaciones, por omisión) oCONSECUTIVO_PROY_(proyectos).carpetaDelAnio_(anio)— subcarpeta2026,2027… dentro de la carpeta de PDF (la busca por nombre).logoDataUrl_(fileId)— logo de Drive siLOGO_FILE_IDtiene valor; si no,LOGO_HELGEN_AZUL.
4.5.1 Proyectos (sección 4.1 del archivo)
hojaProyectos_()— pestañaProyectosde 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 soloActivo; máx. 50, recientes primero. No devuelvecarpeta_id.crearProyecto(p)(UI) — exige vendedor;normalizarProyecto; candado + idempotencia (PRY_<idSolicitud>, 6 h); folioPRY-AAAA-NNNN; primero crea la carpeta y después la fila (estadoActivo). Devuelve{folio, …, url}.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()incluyeproyectos(activos; si falla,[]y la app carga igual).
4.6 Lógica pura (sin Google; se prueba en Node)
redondear(n),formatoMoneda(n),escaparHtml(s),importeConLetra(n)("MIL CIENTO SESENTA PESOS 02/100 M.N.").normalizarCotizacion(p, ivaTasa)— limpia cliente/conceptos, exige nombre y ≥1 concepto, calcula importes y totales,marca.formatearFolio,maxConsecutivo(folios, prefijo, anio).clientesDesdeFilas(filas)— agrupa por nombre+empresa normalizados (textoNormal), la fila más reciente manda, un dato vacío no borra uno anterior;etiqueta= nombre, o "nombre — empresa" si el nombre se repite;id=idCliente.idCliente(c)— RFC válido y no genérico (RFC_GENERICOS: XAXX010101000, XEXX010101000); si no,SIN-RFC:nombre|empresanormalizados.normalizarProyecto(p),nombreCarpetaProyecto(folio, cliente, nombre)(quita/ \ : * ? " < > |),proyectosDesdeFilas,filtrarProyectos.normalizarProducto(p),indiceDeClave(claves, clave).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.validarClaimsToken(claims, clientId, dominio, ahora)— ver §10.3.- SAT:
palabrasSat,SINONIMOS_SAT,PALABRAS_SERVICIO_SAT,buscarClavesSat(consulta, filas, n),candidatosSat,solicitudIaSat,leerRespuestaIaSat(solo acepta claves que estaban entre los candidatos). Modelo:MODELO_IA_SAT. construirHtmlPdf(d, cfg, logoUrl)— HTML del PDF (ver reglas 3–4).
4.7 Interfaz (web/cotizador/index.html)
Tres pestañas: Nueva cotización, Historial, Catálogo. Funciones principales: agregarFila, leerItems,
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 <datalist>: clientes (en "Nombre"), proyectos activos (PRY-… — cliente — nombre), productos por
descripción y por clave, unidades SAT.
Capa de sesión y fetch en <script id="api-web"> (§10.2).
5. Dónde se guardan los datos
Todo en la carpeta maestra "Cotizador Helgen" de Mi unidad:
📁 Cotizador Helgen
├─ 📊 Cotizador Helgen — Base de datos (Cotizaciones, Config)
├─ 📊 Catálogo Helgen (Catalogo)
├─ 📁 Cotizaciones Helgen (PDF) / 📁 2026 / HLG-2026-0001 - Cliente.pdf (cotizaciones "Sin proyecto")
├─ 📁 Proyectos / 📁 2026 / 📁 PRY-2026-0001 — Cliente — Nombre / Cotizaciones, Órdenes de compra, Pagos
├─ 📁 Catálogos SAT / SAT_c_ClaveProdServ.csv, SAT_c_ClaveUnidad.csv, 📊 Catálogos SAT (CFDI 4.0)
├─ 📁 Respaldos 2026-10-19 (copias antes de la migración)
└─ 📜 Cotizador Helgen (código de la app) ← proyecto ANTERIOR, en desuso
El proyecto de la API (1GsH8Dja…) está en la carpeta 1w-T05ql62SXxsm3z4u-xCpMjcdXc0b0p (antes "PRUEBAS — Cotizador web"),
junto con los archivos de prueba que ya no se usan (§9).
Pestaña Cotizaciones (20 columnas, 1 fila por cotización): Folio, Fecha, Cliente, Empresa, RFC, Correo, Teléfono,
Subtotal, IVA, Total, Elaboró, PDF (link), Conceptos (JSON: clave, descripcion, unidad, cantidad, precio, importe y, si vienen
del catálogo, claveSat/unidadSat), Notas, Marca (Helgen / Helgen Energy), Dirección, vendedor_email, vendedor_nombre,
vendedor_apellido, proyecto_folio (T; vacío = "Sin proyecto"). Las columnas P:S se agregaron el 19/10/2026 (§11); las
filas 0001–0004 no las tienen llenas. La T se agregó con Proyectos (10/2026): las filas anteriores quedan vacías y no se mueven.
guardarCotizacion reescribe la fila de encabezados si no coincide con ENCABEZADOS_COTIZACIONES.
Registros de prueba en la hoja real (excluir de reportes y totales; identificarlos por folio, no por texto):
HLG-2026-0005— cliente "PRUEBA - CANCELADA" (prueba de la migración, 19/10/2026).HLG-2026-0006— prueba del primer proyecto (20/10/2026). Cliente original "Angel Sosa"; cambiado a "PRUEBA - CANCELADA" (guion corto, igual que la 0005) enCotizaciones!C7el 20/10/2026.PRY-2026-0001— "Proyecto de Prueba oficial" (20/10/2026); marcadoCanceladoenProyectos!G2el 20/10/2026. Su carpeta enProyectos/2026es de prueba.- Las filas HLG-2026-0001 a 0004 son anteriores a la sesión con Google y también parecen pruebas, pero no se marcaron.
Pestaña Config (Clave | Valor, columna B en formato texto): EMPRESA_NOMBRE (Helgen Servicios Especializados),
EMPRESA_RFC (HSE240304PC1), EMPRESA_DIRECCION, EMPRESA_TELEFONO (+52 442 644 5391), EMPRESA_EMAIL, EMPRESA_WEB,
PREFIJO_FOLIO (HLG), IVA_TASA (0.16), VIGENCIA_DIAS (15), MONEDA, LOGO_FILE_ID (vacío = logo de Logo.js),
TERMINOS_HELGEN, TERMINOS_ENERGY (un término por renglón dentro de la celda).
Pestaña Proyectos (1 fila por proyecto): Folio (PRY-AAAA-NNNN), Fecha, Nombre, Cliente ("nombre — empresa"),
cliente_id (ver idCliente), Ubicación, Estado (Activo/Cerrado/Cancelado; se cambia con la lista desplegable en la hoja,
no hay pantalla), carpeta_id, vendedor_email, vendedor_nombre, vendedor_apellido.
"Catálogo Helgen" / pestaña Catalogo: Clave (única, mayúsculas), Descripción, Unidad, Precio unitario (sin IVA),
Actualizado, Actualizó, Clave SAT, Unidad SAT. A:C y G:H en formato texto.
Script Properties del proyecto 1GsH8Dja… (Configuración del proyecto en el editor):
SPREADSHEET_ID, FOLDER_ID, CATALOGO_ID, SAT_ID, CONSECUTIVO_AAAA (p. ej. CONSECUTIVO_2026),
PROYECTOS_FOLDER_ID y CONSECUTIVO_PROY_AAAA (se crean solos con el primer proyecto),
ANTHROPIC_API_KEY (opcional; activa la IA para claves SAT; el proyecto anterior no la tenía). Nunca poner la API key en el código.
6. Decisiones tomadas (y por qué)
6.1 Folios sin duplicados
- Todo el guardado va dentro de
LockService.getScriptLock()contryLock(30000)y mensaje en español si se agota. - El folio sale del mayor entre el contador y el último folio de la hoja: si alguien borra o edita
CONSECUTIVO_AAAA, no se repite. - Idempotencia: el formulario manda
idSolicitud(UUID por cotización, se renueva al limpiar); un doble clic o reenvío devuelve el mismo folio. - Borrar una fila no hace retroceder el contador (intencional). El consecutivo reinicia cada año.
- El candado es por proyecto: nunca dejar dos proyectos de Apps Script guardando en la misma hoja (por eso el anterior lanza error al guardar; §11).
6.2 Instalación y migraciones automáticas
- La app se instala sola (
db_()→setup(),hojaCatalogo_()→crearCatalogo_(),libroSat_()→importarCatalogosSat_()), porque corrersetup()a mano falló la primera vez. - Toda estructura nueva se migra sola (Config, columnas del catálogo, encabezados de Cotizaciones). Al agregar columnas o claves, seguir este patrón: nunca pedir al usuario que edite la hoja a mano.
6.3 PDF (lecciones del conversor HTML→PDF de Google)
- No imprime colores de fondo (
background): con la banda azul, el logo blanco y el texto blanco desaparecieron. Por eso el PDF usa texto azul, líneas y bordes;bgcolorsolo como extra opcional. - No soporta flex/grid: solo tablas e inline CSS. Fuente del PDF: Arial (las web fonts no son confiables ahí).
- Logo azul en el PDF; el blanco solo sobre fondo azul.
- No hay forma local de ver el PDF real: para revisar, renderizar el HTML quitando los fondos (simula al conversor).
6.4 Interfaz
- Fuentes de Google (League Spartan, Roboto Mono) sí funcionan.
- Máximo 3 módulos por vista. Mensajes de error en español.
- (Solo aplicaba a
Index.htmlcon HtmlService: imágenesdata:con<?!= ?>, porque<?= ?>las bloquea.)
6.5 Catálogo y SAT
- El catálogo es un archivo aparte para poder compartirlo con quien mantiene precios sin exponer el historial.
- La clave SAT no reemplaza la clave interna: muchas claves internas comparten una clave SAT. Es dato interno de contabilidad: no sale en el PDF; se valida contra el catálogo vigente y viaja en el JSON de conceptos.
- Catálogo SAT:
catCFDI_V_4_20261015.xlsdel Anexo 20 (http://omawww.sat.gob.mx/tramitesyservicios/Paginas/anexo_20.htm; el sitio rechaza HTTPS desde aquí, usar HTTP concurl). 52,513 claves de producto/servicio y 2,418 unidades vigentes. - Actualizar el catálogo SAT: descargar el
.xlsnuevo, corrernode herramientas/extraer-catalogos-sat.js <archivo.xls> <carpeta-salida>(requierenpm i xlsx@0.18.5en una carpeta temporal), reemplazar los CSV en la subcarpeta "Catálogos SAT", borrar la propiedadSAT_IDy abrir la pestaña Catálogo. - La búsqueda por palabras funciona bien para productos (panel solar → 26111607 / 32111701 / 60104702; inversor → 32121705;
cargador VE → 25175002) y mal para servicios genéricos; para eso existe la IA opcional. Ampliar
SINONIMOS_SATsi una búsqueda falla. - No inventar claves SAT de memoria: una vez se afirmó que 26111604 era "Paneles solares" y es "Generadores a gas". Verificar siempre contra el CSV/hoja. Contabilidad debe validar toda clave sugerida.
6.6 Clientes sugeridos
- No hay lista de clientes aparte: se calcula cada vez desde
Cotizaciones. Si se borran sus filas, el cliente deja de sugerirse (lo pidió el usuario).
6.7 Drive
- La app usa IDs, no rutas: se pueden mover o renombrar archivos. Rompe: copiar-y-borrar (cambia el ID), borrar, renombrar
las pestañas
Cotizaciones/Config, sacar o renombrar las subcarpetas de año, perder acceso de edición de efrain@helgen.tech. - La carpeta local
~/Downloadsse sincroniza a Drive en una carpeta "Downloads" (1BBB4vyr9WW1TowC6Uemtd3K6V7_NitAh). Por eso el proyecto se sacó de ahí a~/Proyectos/cotizador-helgen(§3). No volver a guardar el proyecto en~/Downloads. - Google Drive para escritorio está montado en
~/Library/CloudStorage/GoogleDrive-efrain@helgen.tech/Mi unidad/: sirve para dejar archivos en Drive (así se subieron los CSV del SAT).
6.8 clasp
- clasp está instalado local en el proyecto (instalar global pedía permisos de administrador): usar siempre
npx clasp. - No volver a correr
clasp create: crea otro proyecto y sobrescribeappsscript.json(pone zona de Nueva York y quita la configuración de web app). Ya pasó y hubo que borrar un proyecto duplicado. - Publicar siempre sobre el mismo deployment (misma
API_URL). Nuncaclasp deploysin--deploymentId. - Si se agrega un servicio nuevo de Google, el dueño debe autorizar el permiso una vez desde el editor (
UrlFetchAppya está autorizado).
6.9 Proyectos
- Opcionales al cotizar ("Sin proyecto" por omisión). Solo se pueden elegir proyectos
Activo; el servidor lo vuelve a revisar. - No hay hoja de clientes: el cliente del proyecto es el de "Datos del cliente" (mismo datalist y autorrelleno; uno nuevo se
escribe ahí, como siempre). Se guarda como texto +
cliente_id(RFC oSIN-RFC:nombre|empresa, veridCliente). Si después se le agrega RFC al cliente, suidcambia a ese RFC y los proyectos viejos conservan elSIN-RFC:…. - Folio propio con contador
CONSECUTIVO_PROY_AAAAbajo el mismo candado de script que las cotizaciones. - Subcarpetas que se reparan solas: las subcarpetas de un proyecto se buscan por nombre. Si alguien borra (Papelera) o
renombra
Cotizaciones,Órdenes de compraoPagos, la próxima vez que se necesite el sistema crea una nueva con el nombre correcto (la renombrada se queda como está, con sus archivos). Si se borra la carpeta completa del proyecto, se vuelve a crear (con sus 3 subcarpetas) y se actualizacarpeta_iden la hoja. Igual conProyectosy su carpeta de año. Renombrar la carpeta del proyecto sí se puede (se usa por ID). - Las cotizaciones viejas no se mueven ni se ligan a proyectos.
6.10 Modelo futuro (fase 2; NO construido)
- Pestañas
OrdenesCompra,PagosyGastosen la base de datos, cada una con su folio (OC-,PAG-,GAS-AAAA-NNNN), contadorCONSECUTIVO_<TIPO>_AAAA(usarsiguienteFolio_(prefijo, hoja, 'CONSECUTIVO_OC_')), columnaproyecto_folioyvendedor_*del token. Crear las pestañas con el patrón dehojaProyectos_()(encabezados que se migran solos). - Archivos en
subcarpetaProyecto_(proyecto, 'Órdenes de compra' | 'Pagos'). Gastos aún no tiene subcarpeta: agregarla aSUBCARPETAS_PROYECTOcuando se construya (los proyectos viejos la obtienen sola por §6.9). - Monto contratado: la fase 2 necesitará marcar una cotización como "Aceptada" (columna de estatus en
Cotizaciones) para definir el monto contratado del proyecto, contra el que se comparan órdenes de compra, pagos y gastos.
7. Reglas del proyecto (no romper)
- Los totales SIEMPRE se recalculan en el servidor (
normalizarCotizacion). El navegador solo muestra una vista previa. - Redondeo a 2 decimales por renglón (
redondear). IVA = subtotal × tasa. - PDF con tablas e inline CSS, sin flex/grid y sin depender de colores de fondo (§6.3).
- Todo texto del usuario que va al PDF pasa por
escaparHtml. - Colores de marca (Brand Guidelines de Helgen en Canva): azul
#2C3C5By aqua#51F3DA(primarios), teal#0AC5A8(secundario), negro/blanco; fondo neutro#F2F4F7. Tipografías: League Spartan Bold (títulos), Roboto Mono (texto). No usar los colores viejos#0B1C3F/#4CAF50. - Toda cotización imprime Términos y Condiciones según la línea de negocio:
- Helgen (
TERMINOS_HELGEN): solo "Precios vigentes por 15 días o hasta agotar existencias, sujetos a variaciones en costos de suministro de componentes." - Helgen Energy (
TERMINOS_ENERGY): metraje adicional, centro de carga, obra civil + vigencia. No agregar términos por cuenta propia (se quitó uno de "fuera de alcance" que el usuario no quería).
- Helgen (
- Texto del usuario que va a Sheets pasa por
textoCelda(un "+52…" se leería como fórmula →#ERROR!). - Textos de la interfaz en español de México.
- Las claves SAT son internas: nunca en el PDF.
- Toda llamada a la API exige token válido de @helgen.tech (
hd,email_verified,aud); el vendedor sale siempre del token, nunca de lo que mande el navegador. No quitar esas revisiones. - Responder siempre en español (de México) al usuario, en el chat y en los mensajes de commit.
8. Cómo trabajar
# Pruebas (Node, servicios de Google simulados). Correr las seis antes de subir.
mkdir -p /tmp/salida-pdf && node tests/cotizacion.test.js Code.js /tmp/salida-pdf # folios, idempotencia, marcas, Config, clientes; deja helgen.html / energy.html
node tests/catalogo.test.js . # catálogo: migración, alta, edición, duplicados
node tests/sat.test.js . "$HOME/Library/CloudStorage/GoogleDrive-efrain@helgen.tech/Mi unidad/Cotizador Helgen/Catálogos SAT"
node tests/sesion.test.js . # token en doPost, vendedor_* (vendedor falso se ignora)
node tests/proyectos.test.js . # folio PRY, carpetas, proyecto_folio, subcarpetas que se reparan
node tests/interfaz.test.js . # interfaz con DOM y fetch simulados: proyecto rechazado se limpia
node tests/probar-busqueda-sat.js Code.js "<ruta>/SAT_c_ClaveProdServ.csv" # ver qué sugiere la búsqueda por palabras
# Subir y publicar la API (misma API_URL)
npx clasp push --force
npx clasp deploy --deploymentId AKfycbwBjF7ra_4O8INsxBq0eh-4r-BlSsJ4LLxcJgVCPRpx1U1a-Pz1CUSdYtvj_Mh0S6y2 -d "descripción del cambio"
npx clasp list-deployments
# Interfaz en Cloudflare (desde esta carpeta; usa wrangler.jsonc)
npx --yes wrangler@4 deploy
- Antes de
push, confirmar que.clasp.jsonapunta a1GsH8Dja…y que nadie cambió el proyecto en el editor (clonar en una carpeta temporal y comparar). - Las pruebas de cotización y SAT simulan un vendedor (
VENDEDOR_), porqueguardarCotizacionexige sesión. - Revisar sintaxis del JS de la interfaz: extraer los
<script>deweb/cotizador/index.htmlynode --check. - Comprobar la API con
curlusando-dsin-X POST(con-X POST -Lcurl repite el POST en la redirección de Google y sale "No se encontró la página"). Sin token →{"ok":false,"error":"Inicia sesión con tu cuenta @helgen.tech.","codigo":"SESION"}. - El flujo completo exige sesión @helgen.tech: pedir al usuario que pruebe y mande captura; los errores del servidor se ven
con
npx clasp logso en el editor (Ejecuciones). Los datos se pueden revisar en solo lectura con el conector de Drive. - Al agregar funciones que llamen servicios de Google, extender los simulacros de
tests/. Si la interfaz necesita una función nueva, agregarla aAPI_FUNCIONES.
Regla de trabajo: guardar y publicar
Al terminar cualquier cambio:
git commitcon una descripción clara (en español) de qué cambió y por qué, ygit pusha GitHub (origin main).- Si se modificó el código de Apps Script (
Code.js,Logo.js,Index.html,appsscript.json), recordarle al usuario publicar la API:npx clasp push --forceynpx clasp deploy --deploymentId AKfycbwBjF7…(comandos completos arriba). - Si se modificó algo en
web/, recordarle al usuario publicar la interfaz:npx --yes wrangler@4 deploy. El commit y el push no publican nada: el equipo sigue usando la versión anterior hasta publicar con clasp o wrangler.
9. Pendientes e ideas
- Renombrar en Drive / Apps Script (lo hace el usuario): proyecto
1GsH8Dja…→ "Cotizador Helgen (código de la app)"; carpeta1w-T05ql…→ "Cotizador Helgen — API web"; proyecto anterior → "Cotizador Helgen (EN DESUSO — no usar)"; los archivos de prueba sin uso → "ARCHIVO PRUEBAS — …" (§10.4). - OAuth en External/Testing: solo entran las cuentas de la lista de usuarios de prueba (4 agregadas el 19/10/2026). Para un vendedor nuevo hay que agregarlo (§10.1). Ideal: cambiar a "Interna".
- La IA para claves SAT no se ha probado contra la API real; requiere
ANTHROPIC_API_KEYy correrprobarIaSat()en el editor. - Existen archivos previos de Helgen Energy en Mi unidad (no movidos): "Catálogo Maestro de Precios - Helgen Energy" (Sheets), "Catálogo Helgen Energy - Soluciones 2026" (Slides), "COTIZADOR HELGEN ENERGY.html". Se ofreció importar esos precios al catálogo.
- Si Helgen Energy tiene logo/colores propios, aplicarlos solo a sus cotizaciones.
GUIA.mddescribe el proyecto anterior; actualizarla si se va a usar.- Ideas mencionadas: enviar PDF por correo, duplicar cotización, estatus (Enviada/Aceptada/Rechazada), descuentos por renglón.
- Proyectos fase 2: órdenes de compra, pagos, gastos y cotización "Aceptada" (§6.10).
- Mover el proyecto de prueba (ya sin deployment, §10.5) a la carpeta
1w-T05ql…junto a los "ARCHIVO PRUEBAS" (lo hace el usuario).
10. Interfaz web y sesión con Google
10.1 Google Cloud (cliente OAuth)
- Client ID (tipo "Aplicación web"):
884526916417-cn5fh45io0nnpvlad2439thkpius4063.apps.googleusercontent.com. No es secreto; debe coincidir enCLIENT_ID_GOOGLEdeweb/cotizador/index.htmly deCode.js. El secreto del cliente no se usa ni se guarda. - Pantalla de consentimiento: External en modo Testing, con usuarios de prueba agregados a mano (decisión del usuario).
- Solo entran las cuentas de la lista. Para dar acceso a otro vendedor: Google Cloud Console → Pantalla de consentimiento de OAuth (Google Auth Platform) → Público → Usuarios de prueba.
- Con External, Google no limita por dominio: una cuenta @gmail en la lista podría obtener un token. Por eso el
servidor exige
hdyemail_verified(y la prueba lo cubre). - Lo ideal: cambiar a "Interna" (requiere que el proyecto de Cloud esté en la organización helgen.tech).
- Orígenes autorizados de JavaScript:
https://cotizador-helgen.cotizador-helgen.workers.devyhttp://127.0.0.1:8788. Sin URIs de redireccionamiento. Un origen nuevo tarda de minutos a horas; mientras, el botón daorigin_mismatch.
10.2 Interfaz (web/cotizador/index.html, <script id="api-web">)
- Es la interfaz completa con el logo incrustado y una capa que imita
google.script.runconfetch(API_URL)(POSTtext/plainpara evitar la consulta CORS previa; cuerpo{"funcion", "args", "token"}, respuesta{"ok", "datos" | "error", "codigo"?};codigo: "SESION"= falta sesión o no es válida). - Carga Google Identity Services (
https://accounts.google.com/gsi/client). La pantalla#logintapa la app hasta tener token; botón "Acceder con Google" + One Tap, conhd: 'helgen.tech'(solo filtra la lista de cuentas; quien decide es el servidor). - El ID token va en cada llamada. Se guarda en
sessionStoragey se reutiliza hasta 1 min antes de expirar (~1 h); las llamadas sin sesión vigente esperan en cola hasta que se inicia sesión. - Si el servidor responde
codigo: "SESION", se borra el token, se muestra el login y la llamada se repite una vez al entrar (guardar es idempotente poridSolicitud: no duplica folio). - Otros códigos llegan a la interfaz en
err.codigo.codigo: "PROYECTO"(proyecto cancelado, cerrado o inexistente al guardar,errorProyecto_):errorAlGuardarlimpia el campo Proyecto, refresca la lista y muestra el aviso. - Encabezado: nombre del vendedor (del token, solo para mostrar) + botón "Cerrar sesión" (
disableAutoSelect). - Tras publicar, el navegador puede mostrar la versión anterior en caché: recargar la página.
- Wrangler 4 delega Cloudflare Pages "clásico" a Workers: se eligió Worker con assets (
wrangler.jsonc), nowrangler pages. Si pide sesión:npx --yes wrangler@4 login. Probar local:python3 -m http.server 8788 --bind 127.0.0.1dentro deweb/.
10.3 Verificación del token en doPost (Code.js)
doPostlee el cuerpo y, antes de buscar la función, llamaverificarTokenGoogle_(cuerpo.token). Sin token o con token inválido no se ejecuta nada: responde{"ok": false, "error": "…", "codigo": "SESION"}.verificarTokenGoogle_(): busca enCacheServicepor la huella SHA-256 del token (TOK_…). Si no está, consultahttps://oauth2.googleapis.com/tokeninfo?id_token=…conUrlFetchApp: Google revisa firma y expiración; si no responde 200, es sesión inválida.validarClaimsTokenexigeaud === CLIENT_ID_GOOGLE,iss=accounts.google.comohttps://accounts.google.com,expen el futuro,hd === 'helgen.tech',email_verifiedverdadero y correo terminado en@helgen.tech. Devuelve{email, nombre, apellido}(degiven_name/family_name; si faltan, se partename). Todo error llevasesion = true.- El resultado se guarda en caché hasta que el token expire (máx. 10 min) para no consultar a Google en cada clic.
VENDEDOR_vale el vendedor solo durante esa petición (se limpia enfinally).Session.getActiveUser()viene vacío conANYONE_ANONYMOUS; por eso existeusuarioActual_().
doGetno sirve la app: conANYONE_ANONYMOUS, una página de HtmlService permitiría llamar funciones congoogle.script.runsin token.- Columnas
vendedor_*y "Elaboró" (columna K y el PDF) = datos del token; pasan portextoCelda.
10.4 Archivos de prueba que quedaron sin uso
Se usaron para probar la interfaz web con datos inventados (Fases 1 y 2, 19/10/2026). Ya no los usa el código; no apuntar
ninguna propiedad a ellos. Están en la carpeta 1w-T05ql62SXxsm3z4u-xCpMjcdXc0b0p:
- "PRUEBAS — Cotizador Helgen — Base de datos"
10WLYHF2vB2TDmoTXwfAxnG8bp7gDOPPzoj9p6B0tUGY - "PRUEBAS — Catálogo Helgen"
1GQBK0_WXSQRuCOeE-Pv8AA6PFA4tjZwCqae4L7WZsH0 - "PRUEBAS — Cotizaciones Helgen (PDF)"
1iDk4faAM557SZh67EA122dj6eU4RJpsH
10.5 Proyecto de prueba de punta a punta (20/10/2026)
Para probar Proyectos con la sesión real sin tocar la hoja real se creó un proyecto de Apps Script aparte:
"PRUEBAS — Cotizador Helgen (no usar)" (1wp8_SQ…, en la raíz de Mi unidad), deployment AKfycbwVu2q5….
- Código =
Code.js/Logo.js/appsscript.jsonoficiales +Pruebas.jsconconfigurarPruebas(), que apunta sus Script Properties a los archivos "ARCHIVO PRUEBAS — …" (§10.4) y a la carpeta "PRUEBAS — Proyectos" (1hGyzqb08ePBi-EIhKg0ztiE9yWxqm-DE, dentro de "ARCHIVO PRUEBAS — Cotizaciones Helgen (PDF)").Pruebas.jsno existe en este repositorio y nunca debe subirse al proyecto oficial. - Como el candado es por proyecto, solo puede escribir en los archivos PRUEBAS (§6.1). Usa el mismo Client ID de OAuth.
- La interfaz se probó desde una copia de
web/fuera del repositorio, servida enhttp://127.0.0.1:8788, conAPI_URLapuntando al deployment de prueba. ElAPI_URLdel repositorio nunca cambia. - Un proyecto creado con
clasp createresponde 403 "Acceso denegado" hasta que el dueño corre una función desde el editor y acepta los permisos. - Para volver a usarlo: clonar en una carpeta temporal (
clasp clone 1wp8_SQ…), copiar ahíCode.js/Logo.js/appsscript.jsonde este repositorio (sin tocar el.clasp.jsonde aquí),clasp push --forceyclasp deploy(oredeploydel mismo id). Al terminar, borrar la carpeta temporal. - Archivado (20/10/2026):
clasp undeploy AKfycbwVu2q5…desde una carpeta temporal ligada a1wp8_SQ…; la URL responde 404 (clasp list-deploymentspuede seguir mostrándolo un rato). El usuario lo mueve en Drive a la carpeta1w-T05ql…junto a los "ARCHIVO PRUEBAS". No borrarlo: sirve para la fase 2 (para reusarlo,clasp deploycrea un deployment nuevo con otra URL).
11. Migración al proyecto nuevo (19/10/2026)
El proyecto que se usaba para pruebas (1GsH8Dja…) pasó a ser el oficial, en este orden (cada paso confirmado por el usuario):
- Respaldos: copias de la base de datos y del catálogo en "Respaldos 2026-10-19".
- Retiro del proyecto anterior (
1X-qSKgv…): deploymentAKfycbxahS3…en @10 = página "El cotizador se mudó" con botón a Cloudflare;guardarCotizacionyguardarProductolanzan error (para que solo un proyecto escriba en la hoja). No se borró nada. Se hizo justo antes del paso 3 para no tener dos proyectos guardando a la vez. - Propiedades del proyecto nuevo → IDs reales (
SPREADSHEET_ID,FOLDER_ID,CATALOGO_ID,SAT_ID) yCONSECUTIVO_2026 = 4(último folio real: HLG-2026-0004; el contador del anterior también era 4). - Columnas P:S en la hoja real: una función temporal escribió solo P1:S1 (comprobando antes que las filas no cambiaran) y se quitó después. Las filas 0001–0004 quedaron intactas.
- Prueba real: HLG-2026-0005 ("PRUEBA - CANCELADA") con vendedor, PDF en
2026y catálogo real. Comprobado en Drive. - Textos sin "PRUEBAS" en
Code.js(API @3), portada y título de la interfaz (Cloudflare). La carpeta local pasó a ser el código del proyecto nuevo; el anterior quedó enanterior/.
Regresar (solo si hace falta): primero apuntar SPREADSHEET_ID, FOLDER_ID y CATALOGO_ID del proyecto nuevo a los IDs
de prueba (§10.4) para que deje de escribir en la hoja real (no borrarlas: db_() correría setup() y crearía otra base
de datos), y luego, desde anterior/:
npx clasp deploy --deploymentId AKfycbxahS3PbizPrRrQIC1m2uz-2G8PoT64BqPdtGLu0Ms5t9RKs2ucxH_c3tpJXkj8uLE -V 9 -d "regreso a @9".
Nunca tener los dos proyectos guardando a la vez (§6.1).