mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Cotizaciones: columna V con el tipo de cambio usado en cotizaciones USD
La interfaz manda el tipo de cambio con que convirtió los precios del catálogo; el servidor lo valida (1–1000, 4 decimales) y lo guarda solo en cotizaciones en USD. En MXN, en filas viejas o con precios capturados sin tipo de cambio queda vacía. El encabezado se agrega solo (patrón de ENCABEZADOS_COTIZACIONES). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6fb43f84a1
commit
decddbb973
10
CLAUDE.md
10
CLAUDE.md
@ -176,7 +176,7 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e
|
|||||||
- `redondear(n)`, `formatoMoneda(n)`, `escaparHtml(s)`, `importeConLetra(n, moneda)` ("MIL CIENTO SESENTA PESOS 02/100 M.N.";
|
- `redondear(n)`, `formatoMoneda(n)`, `escaparHtml(s)`, `importeConLetra(n, moneda)` ("MIL CIENTO SESENTA PESOS 02/100 M.N.";
|
||||||
en USD "MIL CIENTO SESENTA DÓLARES 02/100 USD").
|
en USD "MIL CIENTO SESENTA DÓLARES 02/100 USD").
|
||||||
- `normalizarCotizacion(p, ivaTasa)` — limpia cliente/conceptos, exige nombre y ≥1 concepto, calcula importes y totales, `marca`,
|
- `normalizarCotizacion(p, ivaTasa)` — limpia cliente/conceptos, exige nombre y ≥1 concepto, calcula importes y totales, `marca`,
|
||||||
`moneda` (`monedaDeCotizacion`: vacía = MXN; solo MXN/USD).
|
`moneda` (`monedaDeCotizacion`: vacía = MXN; solo MXN/USD) y `tipoCambio` (solo USD, validado con `tipoCambioDesdeValores`).
|
||||||
- Cobranza: `esquemaPorOmision`, `validarEsquema` (1–10 etapas, nombres únicos, % > 0 con 2 decimales, suma exacta 100),
|
- Cobranza: `esquemaPorOmision`, `validarEsquema` (1–10 etapas, nombres únicos, % > 0 con 2 decimales, suma exacta 100),
|
||||||
`montosDeEsquema` (la última etapa absorbe los centavos), `convertirMoneda`, `repartirPago` (etapa elegida → siguientes →
|
`montosDeEsquema` (la última etapa absorbe los centavos), `convertirMoneda`, `repartirPago` (etapa elegida → siguientes →
|
||||||
anteriores; en centavos), `resumenCobranza` (vuelve a repartir los pagos Vigentes en orden de registro), `validarFechaIso`,
|
anteriores; en centavos), `resumenCobranza` (vuelve a repartir los pagos Vigentes en orden de registro), `validarFechaIso`,
|
||||||
@ -235,10 +235,11 @@ Capa de sesión y `fetch` en `<script id="api-web">` (§10.2).
|
|||||||
El proyecto de la API (`1GsH8Dja…`) está en la carpeta `1w-T05ql62SXxsm3z4u-xCpMjcdXc0b0p` (antes "PRUEBAS — Cotizador web"),
|
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).
|
junto con los archivos de prueba que ya no se usan (§9).
|
||||||
|
|
||||||
**Pestaña `Cotizaciones`** (21 columnas, 1 fila por cotización): Folio, Fecha, Cliente, Empresa, RFC, Correo, Teléfono,
|
**Pestaña `Cotizaciones`** (22 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
|
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`,
|
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"), `moneda` (U: MXN o USD; vacía en filas viejas = MXN). Las columnas P:S se agregaron el 19/10/2026 (§11); las
|
`vendedor_apellido`, `proyecto_folio` (T; vacío = "Sin proyecto"), `moneda` (U: MXN o USD; vacía en filas viejas = MXN), `tipo_cambio` (V: pesos por dólar con que la interfaz convirtió los
|
||||||
|
precios del catálogo, 4 decimales; solo en USD; vacío en MXN, en filas viejas o si los precios se capturaron sin tipo de cambio). 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.
|
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`.
|
`guardarCotizacion` reescribe la fila de encabezados si no coincide con `ENCABEZADOS_COTIZACIONES`.
|
||||||
|
|
||||||
@ -476,8 +477,7 @@ El commit y el push no publican nada: el equipo sigue usando la versión anterio
|
|||||||
- **Cobranza, antes de usarla:** probar en el proyecto PRUEBAS (§10.5) con un usuario sin rol, correr `prepararCobranza()` en la
|
- **Cobranza, antes de usarla:** probar en el proyecto PRUEBAS (§10.5) con un usuario sin rol, correr `prepararCobranza()` en la
|
||||||
API real, capturar `ROL_COBRANZA` y `ADMIN_DESBLOQUEO`, compartir la carpeta de cobranza como lector con tania@ y leo@
|
API real, capturar `ROL_COBRANZA` y `ADMIN_DESBLOQUEO`, compartir la carpeta de cobranza como lector con tania@ y leo@
|
||||||
(Google les manda correo: confirmar con el usuario antes) y anotar su ID en §2.
|
(Google les manda correo: confirmar con el usuario antes) y anotar su ID en §2.
|
||||||
- Precio USD del catálogo (20/10/2026): la cotización no guarda el tipo de cambio que se usó; si contabilidad lo necesita,
|
- Precio USD del catálogo (20/10/2026): el tipo de cambio usado se guarda en `Cotizaciones!V` (no sale en el PDF). Confirmar que leer `GOOGLEFINANCE` desde Apps Script funciona en el catálogo real
|
||||||
agregar una columna en `Cotizaciones`. Confirmar que leer `GOOGLEFINANCE` desde Apps Script funciona en el catálogo real
|
|
||||||
(si no, usar el tipo de cambio manual de la pestaña).
|
(si no, usar el tipo de cambio manual de la pestaña).
|
||||||
- **Acceso del dominio a la Base de datos y a la carpeta maestra** (lector para todo helgen.tech): se queda por ahora; revisarlo
|
- **Acceso del dominio a la Base de datos y a la carpeta maestra** (lector para todo helgen.tech): se queda por ahora; revisarlo
|
||||||
junto con la idea "cada vendedor ve solo sus cotizaciones" (quitarlo impide abrir los PDF desde el Historial).
|
junto con la idea "cada vendedor ve solo sus cotizaciones" (quitarlo impide abrir los PDF desde el Historial).
|
||||||
|
|||||||
9
Code.js
9
Code.js
@ -180,7 +180,7 @@ const CONFIG_EJEMPLO_ANTERIOR = {
|
|||||||
const ENCABEZADOS_COTIZACIONES = [
|
const ENCABEZADOS_COTIZACIONES = [
|
||||||
'Folio', 'Fecha', 'Cliente', 'Empresa', 'RFC', 'Correo', 'Teléfono',
|
'Folio', 'Fecha', 'Cliente', 'Empresa', 'RFC', 'Correo', 'Teléfono',
|
||||||
'Subtotal', 'IVA', 'Total', 'Elaboró', 'PDF', 'Conceptos (JSON)', 'Notas', 'Marca', 'Dirección',
|
'Subtotal', 'IVA', 'Total', 'Elaboró', 'PDF', 'Conceptos (JSON)', 'Notas', 'Marca', 'Dirección',
|
||||||
'vendedor_email', 'vendedor_nombre', 'vendedor_apellido', 'proyecto_folio', 'moneda'
|
'vendedor_email', 'vendedor_nombre', 'vendedor_apellido', 'proyecto_folio', 'moneda', 'tipo_cambio'
|
||||||
];
|
];
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
@ -768,7 +768,7 @@ function guardarCotizacion(payload) {
|
|||||||
t(datos.cliente.email), t(datos.cliente.telefono),
|
t(datos.cliente.email), t(datos.cliente.telefono),
|
||||||
datos.subtotal, datos.iva, datos.total, datos.elaboro, archivo.getUrl(),
|
datos.subtotal, datos.iva, datos.total, datos.elaboro, archivo.getUrl(),
|
||||||
JSON.stringify(datos.items), t(datos.notas), MARCAS[datos.marca], t(datos.cliente.direccion),
|
JSON.stringify(datos.items), t(datos.notas), MARCAS[datos.marca], t(datos.cliente.direccion),
|
||||||
t(vendedor.email), t(vendedor.nombre), t(vendedor.apellido), proyectoFolio, datos.moneda,
|
t(vendedor.email), t(vendedor.nombre), t(vendedor.apellido), proyectoFolio, datos.moneda, datos.tipoCambio,
|
||||||
]);
|
]);
|
||||||
SpreadsheetApp.flush();
|
SpreadsheetApp.flush();
|
||||||
|
|
||||||
@ -1545,12 +1545,15 @@ function normalizarCotizacion(p, ivaTasa) {
|
|||||||
const iva = redondear(subtotal * ivaTasa);
|
const iva = redondear(subtotal * ivaTasa);
|
||||||
const total = redondear(subtotal + iva);
|
const total = redondear(subtotal + iva);
|
||||||
|
|
||||||
return {
|
const datos = {
|
||||||
cliente: cliente, items: items, subtotal: subtotal, iva: iva, total: total, ivaTasa: ivaTasa,
|
cliente: cliente, items: items, subtotal: subtotal, iva: iva, total: total, ivaTasa: ivaTasa,
|
||||||
notas: String((p && p.notas) || '').trim(),
|
notas: String((p && p.notas) || '').trim(),
|
||||||
marca: p && p.marca === 'energy' ? 'energy' : 'helgen',
|
marca: p && p.marca === 'energy' ? 'energy' : 'helgen',
|
||||||
moneda: monedaDeCotizacion(p && p.moneda),
|
moneda: monedaDeCotizacion(p && p.moneda),
|
||||||
};
|
};
|
||||||
|
// Tipo de cambio (pesos por dólar) con que la interfaz convirtió los precios del catálogo; solo en USD y solo si es válido
|
||||||
|
datos.tipoCambio = datos.moneda === 'USD' ? (tipoCambioDesdeValores(Number(p && p.tipoCambio), '').valor || '') : '';
|
||||||
|
return datos;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatearFolio(prefijo, anio, n) {
|
function formatearFolio(prefijo, anio, n) {
|
||||||
|
|||||||
@ -81,12 +81,20 @@ if (ctx.monedaDeCotizacion) {
|
|||||||
assert.throws(()=>ctx.monedaDeCotizacion('EUR'),/Moneda no válida/);
|
assert.throws(()=>ctx.monedaDeCotizacion('EUR'),/Moneda no válida/);
|
||||||
assert.throws(()=>ctx.guardarCotizacion({...base,moneda:'EUR',idSolicitud:'eur'}),/Moneda no válida/);
|
assert.throws(()=>ctx.guardarCotizacion({...base,moneda:'EUR',idSolicitud:'eur'}),/Moneda no válida/);
|
||||||
const n=pdfs.length;
|
const n=pdfs.length;
|
||||||
const rU=ctx.guardarCotizacion({cliente:{nombre:'Solar Corp',empresa:'Solar Corp LLC',direccion:'Austin, TX'},moneda:'USD',marca:'energy',
|
const rU=ctx.guardarCotizacion({cliente:{nombre:'Solar Corp',empresa:'Solar Corp LLC',direccion:'Austin, TX'},moneda:'USD',marca:'energy',tipoCambio:18.456789,
|
||||||
items:[{clave:'PAN-550',descripcion:'Panel solar 550 W',unidad:'PZA',cantidad:12,precio:180},{descripcion:'Inversor 8 kW',cantidad:1,precio:1450.5}],
|
items:[{clave:'PAN-550',descripcion:'Panel solar 550 W',unidad:'PZA',cantidad:12,precio:180},{descripcion:'Inversor 8 kW',cantidad:1,precio:1450.5}],
|
||||||
notas:'Precios en dólares estadounidenses.',idSolicitud:'usd1'});
|
notas:'Precios en dólares estadounidenses.',idSolicitud:'usd1'});
|
||||||
assert.equal(rU.moneda,'USD');
|
assert.equal(rU.moneda,'USD');
|
||||||
const u=pdfs[n].html, fila=cot.m[cot.m.length-1];
|
const u=pdfs[n].html, fila=cot.m[cot.m.length-1];
|
||||||
assert.equal(fila[20],'USD'); assert.equal(cot.m[0][20],'moneda');
|
assert.equal(fila[20],'USD'); assert.equal(cot.m[0][20],'moneda');
|
||||||
|
// Tipo de cambio usado (columna V): solo en USD, 4 decimales; inválido o en MXN = vacío
|
||||||
|
assert.equal(cot.m[0][21],'tipo_cambio'); assert.equal(fila[21],18.4568);
|
||||||
|
assert.equal(ctx.normalizarCotizacion({...base,moneda:'MXN',tipoCambio:18.5},0.16).tipoCambio,'','MXN no guarda tipo de cambio');
|
||||||
|
assert.equal(ctx.normalizarCotizacion({...base,moneda:'USD',tipoCambio:'abc'},0.16).tipoCambio,'');
|
||||||
|
assert.equal(ctx.normalizarCotizacion({...base,moneda:'USD',tipoCambio:0.05},0.16).tipoCambio,'','fuera de rango');
|
||||||
|
assert.equal(ctx.normalizarCotizacion({...base,moneda:'USD'},0.16).tipoCambio,'','precios a mano sin tipo de cambio');
|
||||||
|
ctx.guardarCotizacion({...base,moneda:'MXN',tipoCambio:18.5,idSolicitud:'mxn-tc'});
|
||||||
|
assert.deepEqual(cot.m[cot.m.length-1].slice(20),['MXN',''],'las cotizaciones en MXN quedan vacías en V');
|
||||||
assert(u.includes('TOTAL USD')&&!u.includes('TOTAL MXN')&&u.includes('USD (dólares estadounidenses)'));
|
assert(u.includes('TOTAL USD')&&!u.includes('TOTAL MXN')&&u.includes('USD (dólares estadounidenses)'));
|
||||||
assert(u.includes('DÓLARES')&&u.includes('/100 USD)')&&!u.includes('PESOS'));
|
assert(u.includes('DÓLARES')&&u.includes('/100 USD)')&&!u.includes('PESOS'));
|
||||||
assert(h.includes('TOTAL MXN')&&h.includes('M.N.')&&!h.includes('dólares'),'MXN sin cambios');
|
assert(h.includes('TOTAL MXN')&&h.includes('M.N.')&&!h.includes('dólares'),'MXN sin cambios');
|
||||||
|
|||||||
@ -123,7 +123,9 @@ const esperar=async(n=10)=>{for(let i=0;i<n;i++) await new Promise(r=>setImmedia
|
|||||||
respuestas.guardarCotizacion={ok:true,datos:{folio:'HLG-2026-0009',url:'u',total:208.8,moneda:'USD',proyectoFolio:''}};
|
respuestas.guardarCotizacion={ok:true,datos:{folio:'HLG-2026-0009',url:'u',total:208.8,moneda:'USD',proyectoFolio:''}};
|
||||||
respuestas.getClientes={ok:true,datos:[]};
|
respuestas.getClientes={ok:true,datos:[]};
|
||||||
$('proyecto').value=''; A.run('guardar()'); await esperar();
|
$('proyecto').value=''; A.run('guardar()'); await esperar();
|
||||||
assert.equal(llamadas.filter(c=>c.funcion==='guardarCotizacion').pop().args[0].moneda,'USD');
|
const gU=llamadas.filter(c=>c.funcion==='guardarCotizacion').pop().args[0];
|
||||||
|
assert.deepEqual([gU.moneda,gU.tipoCambio],['USD',18.45],'manda el tipo de cambio con que convirtió');
|
||||||
|
assert.equal(g.args[0].tipoCambio,'','en MXN no manda tipo de cambio');
|
||||||
assert(/\$208\.80 USD/.test($('aviso').innerHTML)); assert.equal($('moneda').value,'MXN','al limpiar vuelve a MXN');
|
assert(/\$208\.80 USD/.test($('aviso').innerHTML)); assert.equal($('moneda').value,'MXN','al limpiar vuelve a MXN');
|
||||||
|
|
||||||
// ===== Cobranza (con permiso) =====
|
// ===== Cobranza (con permiso) =====
|
||||||
|
|||||||
@ -105,7 +105,7 @@ assert.equal(r1.folio,'HLG-2026-0005'); assert.equal(r1.proyectoFolio,'PRY-2026-
|
|||||||
const subCot=c1.hijos.find(h=>h.nombre==='Cotizaciones');
|
const subCot=c1.hijos.find(h=>h.nombre==='Cotizaciones');
|
||||||
assert.deepEqual(subCot.archivos.map(a=>a.name),['HLG-2026-0005 - Solar MX.pdf'],'PDF en Cotizaciones del proyecto');
|
assert.deepEqual(subCot.archivos.map(a=>a.name),['HLG-2026-0005 - Solar MX.pdf'],'PDF en Cotizaciones del proyecto');
|
||||||
// Encabezados: solo cambia la fila 1; la fila vieja no se mueve y queda vacía en T
|
// Encabezados: solo cambia la fila 1; la fila vieja no se mueve y queda vacía en T
|
||||||
assert.equal(cot.m[0].length,21); assert.equal(cot.m[0][19],'proyecto_folio'); assert.equal(cot.m[0][20],'moneda'); assert.deepEqual(cot.m[0].slice(0,19),ENC19);
|
assert.equal(cot.m[0].length,22); assert.equal(cot.m[0][21],'tipo_cambio'); assert.equal(cot.m[0][19],'proyecto_folio'); assert.equal(cot.m[0][20],'moneda'); assert.deepEqual(cot.m[0].slice(0,19),ENC19);
|
||||||
assert.deepEqual(cot.m[1],filaVieja,'fila vieja intacta');
|
assert.deepEqual(cot.m[1],filaVieja,'fila vieja intacta');
|
||||||
assert.equal(cot.m[2][19],'PRY-2026-0001'); assert.equal(cot.m[2][20],'MXN');
|
assert.equal(cot.m[2][19],'PRY-2026-0001'); assert.equal(cot.m[2][20],'MXN');
|
||||||
// Sin proyecto: carpeta del año, columna T vacía
|
// Sin proyecto: carpeta del año, columna T vacía
|
||||||
|
|||||||
@ -71,10 +71,10 @@ fetches=0;
|
|||||||
const r=post({funcion:'guardarCotizacion',token:'t-bueno',
|
const r=post({funcion:'guardarCotizacion',token:'t-bueno',
|
||||||
args:[{...base,idSolicitud:'ok1',vendedor_email:'impostor@helgen.tech',vendedor:{email:'impostor@helgen.tech',nombre:'X'},elaboro:'impostor@helgen.tech'}]});
|
args:[{...base,idSolicitud:'ok1',vendedor_email:'impostor@helgen.tech',vendedor:{email:'impostor@helgen.tech',nombre:'X'},elaboro:'impostor@helgen.tech'}]});
|
||||||
assert(r.ok,r.error); assert.equal(r.datos.folio,'HLG-2026-0002');
|
assert(r.ok,r.error); assert.equal(r.datos.folio,'HLG-2026-0002');
|
||||||
const enc=cot.m[0]; assert.deepEqual(enc.slice(16),['vendedor_email','vendedor_nombre','vendedor_apellido','proyecto_folio','moneda']);
|
const enc=cot.m[0]; assert.deepEqual(enc.slice(16),['vendedor_email','vendedor_nombre','vendedor_apellido','proyecto_folio','moneda','tipo_cambio']);
|
||||||
const fila=cot.m[cot.m.length-1];
|
const fila=cot.m[cot.m.length-1];
|
||||||
assert.equal(fila[10],'ana.lopez@helgen.tech','Elaboró = correo del token');
|
assert.equal(fila[10],'ana.lopez@helgen.tech','Elaboró = correo del token');
|
||||||
assert.deepEqual(fila.slice(16),['ana.lopez@helgen.tech','Ana','López Díaz','','MXN'],'sin proyecto = columna T vacía; sin moneda = MXN');
|
assert.deepEqual(fila.slice(16),['ana.lopez@helgen.tech','Ana','López Díaz','','MXN',''],'sin proyecto = columna T vacía; sin moneda = MXN');
|
||||||
assert(!JSON.stringify(fila).includes('impostor'));
|
assert(!JSON.stringify(fila).includes('impostor'));
|
||||||
// Caché: el token ya se verificó (en la llamada a setup); no se vuelve a consultar a Google
|
// Caché: el token ya se verificó (en la llamada a setup); no se vuelve a consultar a Google
|
||||||
assert.equal(post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).ok,true); assert.equal(fetches,0);
|
assert.equal(post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).ok,true); assert.equal(fetches,0);
|
||||||
|
|||||||
@ -608,6 +608,8 @@
|
|||||||
notas: $('notas').value,
|
notas: $('notas').value,
|
||||||
marca: $('marca').value,
|
marca: $('marca').value,
|
||||||
moneda: monedaActual(),
|
moneda: monedaActual(),
|
||||||
|
// Tipo de cambio con que se convirtieron los precios del catálogo (se guarda en la columna V; solo USD)
|
||||||
|
tipoCambio: monedaActual() === 'USD' && TC && TC.valor ? TC.valor : '',
|
||||||
proyectoFolio: '',
|
proyectoFolio: '',
|
||||||
idSolicitud: ID_SOLICITUD,
|
idSolicitud: ID_SOLICITUD,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user