From 1a02e2df3fc68615652deeaac37b737b9fdab0d9 Mon Sep 17 00:00:00 2001 From: "Efrain (Helgen)" Date: Tue, 20 Oct 2026 18:49:07 -0600 Subject: [PATCH] =?UTF-8?q?Compras:=20=C3=B3rdenes=20de=20compra=20por=20p?= =?UTF-8?q?royecto,=20proveedores=20y=20margen=20(fase=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rol nuevo ROL_COMPRAS (Script Properties): solo ese rol crea, ve, recibe y cancela órdenes; el servidor lo revisa en cada llamada y los demás reciben codigo PERMISO. getDatosIniciales agrega permisos.compras. - Órdenes OC-AAAA-NNNN con el mismo candado e idempotencia, solo en proyectos Activos con contrato firmado. Estados Emitida / Recibida / Cancelada (con motivo; nunca se borran). La línea de negocio sale del contrato. - Libro y carpeta "Cotizador Helgen — Compras" aparte y restringidos con privado_(): ni en la Base de datos ni en la carpeta del proyecto ni en cobranza. Pestañas OrdenesCompra, Proveedores (se llena sola) y Bitacora. - PDF de la orden siempre con la razón social de Helgen y los términos TERMINOS_OC (se agregan solos a Config). Sin tipo de cambio ni línea. - Costo siempre capturado, nunca el precio de venta. Orden en otra moneda que el proyecto: tipo de cambio obligatorio y costo convertido. - Margen (contratado sin IVA − órdenes vigentes sin IVA) solo en la vista de cobranza; preparado para sumar nómina y horas. Con órdenes vigentes la aceptada no puede cambiar de moneda. - Cobranza y compras comparten la mecánica de área privada (carpetaArea_, libroArea_, enRol_). Los proyectos nuevos ya no crean "Órdenes de compra". - Interfaz: pestaña Compras y tarjeta de margen en Cobranza. - Pruebas: tests/compras.test.js nuevo; interfaz, cobranza, sesión, proyectos y cotización actualizadas (oc.html de ejemplo). Las ocho pasan. Sin publicar: falta respaldo, prueba en PRUEBAS, clasp y wrangler (CLAUDE.md §9). Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 123 +++++-- Code.js | 684 ++++++++++++++++++++++++++++++++++++--- tests/cobranza.test.js | 6 +- tests/compras.test.js | 295 +++++++++++++++++ tests/cotizacion.test.js | 14 + tests/interfaz.test.js | 80 ++++- tests/proyectos.test.js | 6 +- tests/sesion.test.js | 2 +- web/cotizador/index.html | 275 +++++++++++++++- 9 files changed, 1415 insertions(+), 70 deletions(-) create mode 100644 tests/compras.test.js diff --git a/CLAUDE.md b/CLAUDE.md index caec513..ec66907 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,8 @@ Desde el **19/10/2026** (§11) la interfaz se sirve desde **Cloudflare** y habla Además: **proyectos** (opcionales: folio `PRY-AAAA-0001` 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), -cotizaciones en **MXN o USD**, historial con búsqueda, catálogo de productos con precios (Google Sheets aparte), clientes sugeridos con +**ó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), cotizaciones en **MXN o USD**, 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. @@ -34,6 +35,7 @@ autorrelleno, y claves SAT internas por producto para contabilidad (con sugerenc | 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`) | | Carpeta "Cotizador Helgen — Cobranza" (`COBRANZA_FOLDER_ID`) | `1A9uoRdhFdWOBacWCxx4Ktw1d9iQH7JRc` (creada el 20/10/2026 con `prepararCobranza()`; fuera de la carpeta maestra, en Mi unidad). Libro "Cotizador Helgen — Cobranza (datos)" (`COBRANZA_DB_ID`) `1uk9FNzTkQPjvWJw2R5wb8HXZl9c8ldYvyB64c8yjHL0`. Lectores: tania@ y leo@ (§6.11) | +| Carpeta "Cotizador Helgen — Compras" (`COMPRAS_FOLDER_ID`) | **Todavía no existe** (fase 3 construida, sin publicar; §9). La crea `prepararCompras()` en Mi unidad, fuera de la carpeta maestra, con su libro "Cotizador Helgen — Compras (datos)" (`COMPRAS_DB_ID`). Anotar aquí sus IDs al crearlos | | 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) | | 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 | @@ -177,6 +179,25 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e (un permiso heredado no se puede quitar en el hijo: 403 `cannotDeletePermission`). Si falla, lanza error. - `prepararCobranza()` — correr desde el editor: crea carpeta y libro (o los repara si ya existen), restringe primero la carpeta y luego el libro, y registra `ROL_COBRANZA`. Sirve para autorizar permisos. No comparte nada. +- Cobranza y compras comparten la mecánica de "área privada": `AREA_COBRANZA` / `AREA_COMPRAS` + `carpetaArea_`, `libroArea_`, + `libroAreaSiExiste_`, `hojaDeArea_(libro, hojas, nombre)`, `filasDeArea_`; roles con `enRol_(propiedad)`. + `contratosFirmados_()` = mapa folio → contrato firmado (lo usan el buscador de cotizaciones y compras). +- `vistaCobranza_` incluye `margen` (`margenDeContrato_`: contratado sin IVA − órdenes de compra no canceladas; si algo falla, + `{error}` y la vista carga igual). `marcarAceptada` rechaza una aceptada de otra moneda si hay órdenes vigentes (como con pagos). + +### 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**), + `getComprasProyecto(folio)` (`vistaCompras_`: proyecto, `moneda`, `marca`, `puedeEmitir`, órdenes, proveedores; sin contrato ni + margen), `guardarOrdenCompra(p)` (candado + idempotencia `OC_`; revisa dentro del candado proyecto Activo y firmado; + folio con `siguienteFolio_('OC', hoja, 'CONSECUTIVO_OC_')`; actualiza el proveedor; PDF; fila; Bitacora), + `marcarOcRecibida(folio, 'AAAA-MM-DD')` (solo Emitida; fecha no futura ni anterior a la orden), `cancelarOrdenCompra(folio, motivo)` + (solo Emitida; motivo ≥ 5). +- 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), + `buscarOrden_`, `bitacora_(…, HOJAS_COMPRAS)`. +- `prepararCompras()` — correr desde el editor: crea o repara carpeta y libro restringidos, agrega `TERMINOS_OC` a Config y + muestra `ROL_COMPRAS`. No comparte nada. ### 4.6 Lógica pura (sin Google; se prueba en Node) - `redondear(n)`, `formatoMoneda(n)`, `escaparHtml(s)`, `importeConLetra(n, moneda)` ("MIL CIENTO SESENTA PESOS 02/100 M.N."; @@ -203,9 +224,20 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e - 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). +- Compras: `normalizarProveedor`, `proveedorId` (RFC válido no genérico o `SIN-RFC:razón social`), `fusionarProveedor` (un dato + vacío no borra uno anterior), `proveedoresDesdeFilas` (con `etiqueta` y `fila`), `normalizarOrdenCompra(p, {moneda, ubicacion, hoy, + ivaTasa})` (entrega obligatoria y no anterior a hoy; lugar = ubicación del proyecto si viene vacío; condiciones obligatorias; IVA + 16/8/0 % (`IVA_OC`); renglones con costo capturado; tipo de cambio obligatorio si la moneda no es la del proyecto; + `subtotalProyecto` sin IVA en la moneda del proyecto), `margenProyecto(contratado, moneda, costos)` (lanza error si un costo + está en otra moneda; `porcentaje` con 1 decimal), `nombreArchivoOc`, `terminosOcParaPdf(cfg)` (si la celda está vacía, los + iniciales), `construirHtmlOrdenCompra(d, cfg, logoUrl)` (mismas reglas que el de cotización; sin tipo de cambio ni línea de negocio). ### 4.7 Interfaz (`web/cotizador/index.html`) -Pestañas: **Nueva cotización**, **Historial**, **Catálogo** y **Cobranza** (solo si `permisos.cobranza`). Moneda: selector +Pestañas: **Nueva cotización**, **Historial**, **Catálogo**, **Cobranza** (solo si `permisos.cobranza`; con tarjeta de margen +`#cobMargen`) y **Compras** (solo si `permisos.compras`): `cargarProyectosCom`, `pintarCompras(vista)` (al cambiar de proyecto +limpia el formulario), `recibirProveedores`/`autorrellenarProveedor`, `agregarRenglonOc` (el catálogo llena clave, descripción y +unidad; el costo queda vacío; listas `#ocCatalogo`/`#ocClaves` sin precio), `recalcularOc`, `pintarTcOc` (tipo de cambio si la +moneda no es la del proyecto), `limpiarOc`, `guardarOc`, `recibirOc` (fecha con `window.prompt`), `cancelarOc` (motivo). Moneda: selector `#moneda`, `monedaActual`, `dinero(n, moneda)`, `cambiarMoneda` (en USD el catálogo llena clave/descripción/unidad pero **no** el precio). Cobranza: `cargarProyectosCob`, `pintarCobranza(vista)`, `pintarEsquema`, `aceptarCot` (confirma con `window.confirm` si el servidor lo pide), `guardarEsquemaCob`, `firmarCob`, `desbloquearCob`, `registrarPago` (lee el archivo con `FileReader`), @@ -226,8 +258,8 @@ Capa de sesión y `fetch` en `',marca:'energy',idSolicitud:'o'+(++nId),...extra}); +let r=ok('pedro','guardarOrdenCompra',oc({idSolicitud:'oc1'})); +assert.deepEqual([r.folio,r.total,r.moneda],[OC('0001'),2320,'MXN']); +let f=H('OrdenesCompra')[1]; +assert.deepEqual([f[0],f[1],f[2],f[3],f[4],f[5],f[7],f[9],f[10],f[11],f[12],f[13],f[14],f[15],f[16],f[17],f[18],f[19],f[20],f[25],f[26],f[28]], + [r.folio,HOY_ISO,p1,'Helgen','SIN-RFC:paneles del bajio','Paneles del Bajío','Luis Pérez','+52 442 111 2233',HOY_ISO,'Querétaro, Qro.', + 'Crédito 30 días','MXN',0.16,2000,320,2320,'MXN','',2000,'pedro@helgen.tech','Pedro Ruiz','Emitida']); +assert.deepEqual(JSON.parse(f[21]).map(x=>x.costo+'/'+x.importe+'/'+('precio' in x)),['1000/2000/false'],'el costo es el capturado; el precio de venta no se guarda'); +const pdfOc=archivos[f[23]]; +assert.equal(ruta(pdfOc.padre),'Mi unidad/Cotizador Helgen — Compras/'+ANIO+'/'+p1); +assert.equal(pdfOc.blob.name,r.folio+' — Paneles del Bajío.pdf'); assert.equal(f[24],r.url); +assert.deepEqual([pdfOc.acceso,pdfOc.padre.acceso,pdfOc.padre.padre.acceso],['PRIVATE','PRIVATE','PRIVATE'],'PDF y subcarpetas restringidos'); +const html=pdfOc.blob.html; +assert(/ORDEN DE COMPRA/.test(html)&&/Helgen Servicios Especializados/.test(html)&&/RFC HSE240304PC1/.test(html)); +for(const t of ['Favor de indicar el folio de esta orden en su factura.','Facturar a nombre de la razón social y RFC indicados arriba.', + 'Precios fijos según esta orden; cualquier cambio requiere una nueva orden.','Entregar en el lugar y fecha indicados.']) assert(html.includes(t),'término: '+t); +assert(/<script>/.test(html)&&!/