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.";
|
||||
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`,
|
||||
`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),
|
||||
`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`,
|
||||
@ -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"),
|
||||
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
|
||||
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.
|
||||
`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
|
||||
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.
|
||||
- Precio USD del catálogo (20/10/2026): la cotización no guarda el tipo de cambio que se usó; si contabilidad lo necesita,
|
||||
agregar una columna en `Cotizaciones`. Confirmar que leer `GOOGLEFINANCE` desde Apps Script funciona en el catálogo real
|
||||
- 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
|
||||
(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
|
||||
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 = [
|
||||
'Folio', 'Fecha', 'Cliente', 'Empresa', 'RFC', 'Correo', 'Teléfono',
|
||||
'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() {
|
||||
@ -768,7 +768,7 @@ function guardarCotizacion(payload) {
|
||||
t(datos.cliente.email), t(datos.cliente.telefono),
|
||||
datos.subtotal, datos.iva, datos.total, datos.elaboro, archivo.getUrl(),
|
||||
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();
|
||||
|
||||
@ -1545,12 +1545,15 @@ function normalizarCotizacion(p, ivaTasa) {
|
||||
const iva = redondear(subtotal * ivaTasa);
|
||||
const total = redondear(subtotal + iva);
|
||||
|
||||
return {
|
||||
const datos = {
|
||||
cliente: cliente, items: items, subtotal: subtotal, iva: iva, total: total, ivaTasa: ivaTasa,
|
||||
notas: String((p && p.notas) || '').trim(),
|
||||
marca: p && p.marca === 'energy' ? 'energy' : 'helgen',
|
||||
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) {
|
||||
|
||||
@ -81,12 +81,20 @@ if (ctx.monedaDeCotizacion) {
|
||||
assert.throws(()=>ctx.monedaDeCotizacion('EUR'),/Moneda no válida/);
|
||||
assert.throws(()=>ctx.guardarCotizacion({...base,moneda:'EUR',idSolicitud:'eur'}),/Moneda no válida/);
|
||||
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}],
|
||||
notas:'Precios en dólares estadounidenses.',idSolicitud:'usd1'});
|
||||
assert.equal(rU.moneda,'USD');
|
||||
const u=pdfs[n].html, fila=cot.m[cot.m.length-1];
|
||||
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('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');
|
||||
|
||||
@ -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.getClientes={ok:true,datos:[]};
|
||||
$('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');
|
||||
|
||||
// ===== 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');
|
||||
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
|
||||
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.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
|
||||
|
||||
@ -71,10 +71,10 @@ fetches=0;
|
||||
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'}]});
|
||||
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];
|
||||
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'));
|
||||
// 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);
|
||||
|
||||
@ -608,6 +608,8 @@
|
||||
notas: $('notas').value,
|
||||
marca: $('marca').value,
|
||||
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: '',
|
||||
idSolicitud: ID_SOLICITUD,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user