Cobranza: esquema de Energy editable solo por el administrador; columnas USD en el PDF

- Esquema de Helgen Energy: solo ADMIN_DESBLOQUEO lo edita (antes de firmar),
  con motivo obligatorio que queda en la Bitacora junto con el esquema anterior.
  Al cambiar entre cotizaciones de Energy se conserva el esquema; al pasar de
  Helgen a Energy se pone el 60/30/10 (con confirmación si reemplaza otro).
- PDF en USD: columnas "P. Unitario (USD)" e "Importe (USD)"; en MXN sin cambios.
- Prueba de que la cotización USD imprime el mismo bloque de términos que MXN.
- Pruebas de cobranza e interfaz ampliadas; CLAUDE.md actualizado.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Efrain (Helgen) 2026-10-20 16:49:18 -06:00
parent 745c9e3a66
commit d08f4025f5
6 changed files with 90 additions and 29 deletions

View File

@ -155,8 +155,9 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e
### 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"`).
- *(UI)* `listarProyectosCobranza(busqueda)` (todos los estados + estado del contrato), `getCobranzaProyecto(folio)`,
`marcarAceptada(proyecto, cotizacion, confirmado)` (si es Energy y reemplaza un esquema distinto al 60/30/10 devuelve
`{confirmar, mensaje}` sin cambiar nada), `guardarEsquema(proyecto, etapas)`, `firmarContrato(proyecto, 'AAAA-MM-DD')`,
`marcarAceptada(proyecto, cotizacion, confirmado)` (al pasar de Helgen a Energy pone el 60/30/10; si eso reemplaza un esquema
distinto devuelve `{confirmar, mensaje}` sin cambiar nada; entre cotizaciones de la misma línea conserva el esquema),
`guardarEsquema(proyecto, etapas, motivo)` (Energy: solo `ADMIN_DESBLOQUEO` y motivo obligatorio en la Bitacora), `firmarContrato(proyecto, 'AAAA-MM-DD')`,
`desbloquearContrato(proyecto, motivo)` (además `esAdminDesbloqueo_`), `registrarPago(p)` (con `comprobante.datos` en base64
e idempotencia `PAG_<idSolicitud>`), `cancelarPago(folio, motivo)`. Las que cambian algo usan `conCandado_` y devuelven la vista
(`vistaCobranza_`).
@ -371,8 +372,9 @@ Cobranza: `ROL_COBRANZA` (correos separados por coma: efrain@, tania@, leo@), `A
- **Aceptada:** una por proyecto; se puede cambiar hasta firmar. Monto contratado = total **con IVA**; subtotal de referencia.
**Línea de negocio y moneda del proyecto salen de la aceptada.** Si ya hay pagos (tras un desbloqueo), la nueva aceptada debe
tener la misma moneda.
- **Esquema:** Helgen Energy **fijo** 60% Anticipo / 30% Instalación / 10% Encendido del sistema (el servidor rechaza editarlo);
Helgen: mismas etapas como sugerencia, editables (nombres, % y número de etapas, 1–10) hasta firmar. Suma exacta 100%.
- **Esquema:** Helgen Energy: 60% Anticipo / 30% Instalación / 10% Encendido del sistema; **solo `ADMIN_DESBLOQUEO` lo edita**,
con motivo obligatorio que queda en la Bitacora (con el esquema anterior). Helgen: mismas etapas como sugerencia, editables por
el rol de cobranza (nombres, % y número de etapas, 1–10). En ambos casos solo antes de firmar y con suma exacta de 100%.
- **Contrato firmado** (fecha, no futura): bloquea aceptada, monto y esquema. No se puede firmar si lo pagado supera el nuevo
monto. **Desbloqueo:** solo `ADMIN_DESBLOQUEO`, motivo obligatorio; vuelve a "Aceptada", conserva los pagos y queda en Bitacora.
- **Pagos:** solo con contrato firmado; comprobante **obligatorio** (PDF/JPG/PNG/HEIC, máx. 10 MB, tipo revisado por contenido) en
@ -382,7 +384,9 @@ Cobranza: `ROL_COBRANZA` (correos separados por coma: efrain@, tania@, leo@), `A
obligatorio (pesos por dólar) y se guarda el monto aplicado en la moneda del proyecto: **nunca se suman MXN y USD**.
- **Cancelar pago:** rol de cobranza, con motivo; el pago queda como Cancelado (nunca se borra) y deja de contar.
- **USD en cotizaciones:** el catálogo está en pesos: en USD la interfaz llena clave y descripción, y el precio se captura a mano
(no se convierte con tipo de cambio). El PDF dice "Moneda: USD (dólares estadounidenses)", "TOTAL USD" e importe con letra en dólares.
(no se convierte con tipo de cambio). El PDF dice "Moneda: USD (dólares estadounidenses)", columnas "P. Unitario (USD)" e
"Importe (USD)", "TOTAL USD" e importe con letra en dólares. **Términos y condiciones: el mismo bloque que en MXN, sin cambios**
(incluido "$300 MXN por metro" en Energy; decisión del usuario, 20/10/2026).
## 7. Reglas del proyecto (no romper)
1. Los totales SIEMPRE se recalculan en el servidor (`normalizarCotizacion`). El navegador solo muestra una vista previa.
@ -466,7 +470,6 @@ El commit y el push no publican nada: el equipo sigue usando la versión anterio
- **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).
- Tania y Leo eran **editores** de la Base de datos (20/10/2026); el usuario los baja a lector.
- Los T&C de Helgen Energy dicen "$300 MXN por metro" también en cotizaciones USD: confirmar con el usuario si se ajusta.
- 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

38
Code.js
View File

@ -1167,7 +1167,8 @@ function vistaCobranza_(libro, proyecto) {
esquema: d.etapas,
resumen: resumen,
pagos: d.pagos.map(p => { const o = Object.assign({}, p); delete o.fila; return o; }).reverse(),
esquemaEditable: !!c && c.estado !== ESTADO_FIRMADO && c.marca !== 'energy',
esquemaEditable: !!c && c.estado !== ESTADO_FIRMADO && (c.marca !== 'energy' || esAdminDesbloqueo_()),
esquemaPideMotivo: !!c && c.estado !== ESTADO_FIRMADO && c.marca === 'energy' && esAdminDesbloqueo_(),
puedePagar: !!c && c.estado === ESTADO_FIRMADO,
puedeDesbloquear: !!c && c.estado === ESTADO_FIRMADO && esAdminDesbloqueo_(),
metodos: METODOS_PAGO,
@ -1226,15 +1227,17 @@ function marcarAceptada(proyectoFolio, cotizacionFolio, confirmado) {
vigentes[0].monedaProyecto + '.');
}
// Esquema: Helgen Energy siempre 60/30/10; Helgen conserva el que ya tenga (o el sugerido)
// Esquema: al pasar a Helgen Energy se pone el 60/30/10 (después solo el administrador lo cambia); entre cotizaciones
// de la misma línea, o de Energy a Helgen, se conserva el que ya tenga
const porOmision = esquemaPorOmision();
const igualAlDeOmision = d.etapas.length === porOmision.length &&
d.etapas.every((e, i) => e.etapa === porOmision[i].etapa && e.porcentaje === porOmision[i].porcentaje);
if (cot.marca === 'energy' && d.etapas.length && !igualAlDeOmision && !confirmado) {
return { confirmar: true, mensaje: 'La cotización ' + cot.folio + ' es de Helgen Energy: su esquema de pagos es fijo ' +
'(60% Anticipo, 30% Instalación, 10% Encendido del sistema) y reemplazará el esquema actual. ¿Continuar?' };
const pasaAEnergy = cot.marca === 'energy' && (!c || c.marca !== 'energy');
if (pasaAEnergy && d.etapas.length && !igualAlDeOmision && !confirmado) {
return { confirmar: true, mensaje: 'La cotización ' + cot.folio + ' es de Helgen Energy: su esquema de pagos es ' +
'60% Anticipo, 30% Instalación, 10% Encendido del sistema, y reemplazará el esquema actual. ¿Continuar?' };
}
const etapas = cot.marca === 'energy' || !d.etapas.length ? porOmision : d.etapas;
const etapas = pasaAEnergy || !d.etapas.length ? porOmision : d.etapas;
const t = textoCelda;
const fila = [proyecto.folio, cot.folio, MARCAS[cot.marca], cot.moneda, cot.subtotal, cot.iva, cot.total, ESTADO_ACEPTADA,
@ -1251,10 +1254,14 @@ function marcarAceptada(proyectoFolio, cotizacionFolio, confirmado) {
});
}
/** Guarda el esquema de pagos (solo Helgen y solo antes de firmar). */
function guardarEsquema(proyectoFolio, etapas) {
/**
* Guarda el esquema de pagos (solo antes de firmar). Helgen: rol de cobranza. Helgen Energy (60/30/10 por omisión): solo
* ADMIN_DESBLOQUEO, con motivo que queda en la Bitacora.
*/
function guardarEsquema(proyectoFolio, etapas, motivo) {
exigirCobranza_();
const limpio = validarEsquema(etapas);
const m = String(motivo || '').trim();
const libro = libroCobranza_();
return conCandado_(() => {
const proyecto = proyectoParaCobranza_(proyectoFolio);
@ -1262,9 +1269,16 @@ function guardarEsquema(proyectoFolio, etapas) {
const c = d.contrato;
if (!c) throw new Error('Primero marca la cotización aceptada del proyecto.');
if (c.estado === ESTADO_FIRMADO) throw new Error('Contrato firmado: el esquema de pagos ya no se puede cambiar.');
if (c.marca === 'energy') throw new Error('El esquema de Helgen Energy es fijo (60% / 30% / 10%).');
if (c.marca === 'energy') {
if (!esAdminDesbloqueo_()) throw errorPermiso_('El esquema de Helgen Energy solo lo puede cambiar el administrador.');
if (m.length < 5) throw new Error('Escribe el motivo del cambio al esquema de Helgen Energy.');
if (m.length > 500) throw new Error('El motivo debe tener máximo 500 caracteres.');
}
const antes = d.etapas.map(e => e.etapa + ' ' + e.porcentaje + '%').join(' · ');
escribirEsquema_(d.hojas.esquemas, proyecto.folio, limpio, c.total);
bitacora_(libro, 'Editar esquema de pagos', proyecto.folio, limpio.map(e => e.etapa + ' ' + e.porcentaje + '%').join(' · '));
bitacora_(libro, c.marca === 'energy' ? 'Editar esquema de pagos (Helgen Energy)' : 'Editar esquema de pagos', proyecto.folio,
limpio.map(e => e.etapa + ' ' + e.porcentaje + '%').join(' · ') + (antes ? ' (antes ' + antes + ')' : ''),
c.marca === 'energy' ? m : '');
SpreadsheetApp.flush();
return vistaCobranza_(libro, proyecto);
});
@ -2201,7 +2215,9 @@ function construirHtmlPdf(d, cfg, logoUrl) {
'<table class="conceptos"><tr>' +
'<th class="c" style="width:22px;">#</th><th style="width:60px;">Clave</th><th>Descripción</th>' +
'<th class="c" style="width:45px;">Unidad</th><th class="r" style="width:45px;">Cant.</th>' +
'<th class="r" style="width:80px;">P. Unitario</th><th class="r" style="width:85px;">Importe</th></tr>' +
(d.moneda === 'USD'
? '<th class="r" style="width:95px;">P. Unitario (USD)</th><th class="r" style="width:95px;">Importe (USD)</th></tr>'
: '<th class="r" style="width:80px;">P. Unitario</th><th class="r" style="width:85px;">Importe</th></tr>') +
filas + '</table>' +
// Módulo 3: Totales

View File

@ -138,6 +138,7 @@ const cH=cot(p1,1000); // Helgen MXN: total 1160
const cE=cot(p1,2000,{marca:'energy'}); // Energy MXN: total 2320
const cU=cot(p1,500,{moneda:'USD'}); // USD en el mismo proyecto
const cBarata=cot(p1,100); // 116
const cE2=cot(p1,1000,{marca:'energy'}); // otra de Energy: total 1160
const cOtro=cot(p2,862.07,{moneda:'USD'}); // Proyecto USD: total 1000.00
const sinProy=cot('',50);
@ -166,8 +167,8 @@ assert.deepEqual(Object.keys(LC.pest).sort(),['Bitacora','Contratos','Esquemas',
const H=n=>LC.pest[n].m, bit=()=>H('Bitacora').slice(1);
let v=ok('tania','getCobranzaProyecto',p1.toLowerCase());
assert.equal(v.contrato,null); assert.deepEqual(v.cotizaciones.map(c=>c.folio),[cH,cE,cU,cBarata]);
assert.deepEqual(v.cotizaciones.map(c=>c.moneda+'/'+c.marca),['MXN/helgen','MXN/energy','USD/helgen','MXN/helgen']);
assert.equal(v.contrato,null); assert.deepEqual(v.cotizaciones.map(c=>c.folio),[cH,cE,cU,cBarata,cE2]);
assert.deepEqual(v.cotizaciones.map(c=>c.moneda+'/'+c.marca),['MXN/helgen','MXN/energy','USD/helgen','MXN/helgen','MXN/energy']);
assert.equal(v.puedePagar,false);
falla('tania','firmarContrato',[p1,HOY_ISO],/Primero marca la cotización aceptada/);
falla('tania','guardarEsquema',[p1,P.esquemaPorOmision()],/Primero marca/);
@ -191,11 +192,27 @@ assert.equal(H('Contratos')[1][1],cH,'sin confirmar no cambia');
v=ok('tania','marcarAceptada',p1,cE,true);
assert.deepEqual([v.contrato.cotizacion,v.contrato.marca,v.contrato.total],[cE,'energy',2320]);
assert.deepEqual(v.resumen.etapas.map(e=>e.etapa+':'+e.monto),['Anticipo:1392','Instalación:696','Encendido del sistema:232']);
assert.equal(v.esquemaEditable,false);
falla('tania','guardarEsquema',[p1,[{etapa:'A',porcentaje:100}]],/Helgen Energy es fijo/);
// De regreso a Helgen: conserva el 60/30/10 (ahora editable), sin pedir confirmación
assert.deepEqual([v.esquemaEditable,v.esquemaPideMotivo],[false,false],'Tania no edita el de Energy');
// Esquema de Energy: solo el administrador y con motivo (queda en la Bitacora)
falla('tania','guardarEsquema',[p1,[{etapa:'A',porcentaje:100}],'el cliente lo pidió'],/solo lo puede cambiar el administrador/,'PERMISO');
v=ok('efrain','getCobranzaProyecto',p1); assert.deepEqual([v.esquemaEditable,v.esquemaPideMotivo],[true,true]);
falla('efrain','guardarEsquema',[p1,[{etapa:'Anticipo',porcentaje:50},{etapa:'Cierre',porcentaje:50}]],/motivo del cambio/);
falla('efrain','guardarEsquema',[p1,[{etapa:'Anticipo',porcentaje:50},{etapa:'Cierre',porcentaje:40}],'negociado'],/suman 90%/);
v=ok('efrain','guardarEsquema',p1,[{etapa:'Anticipo',porcentaje:50},{etapa:'Cierre',porcentaje:50}],'El cliente negoció 50% de anticipo');
assert.deepEqual(v.resumen.etapas.map(e=>e.etapa+':'+e.monto),['Anticipo:1160','Cierre:1160']);
const be=bit().filter(b=>b[2]==='Editar esquema de pagos (Helgen Energy)').pop();
assert.deepEqual([be[1],be[5]],['efrain@helgen.tech','El cliente negoció 50% de anticipo']);
assert(/Anticipo 50% · Cierre 50% \(antes Anticipo 60% · Instalación 30% · Encendido del sistema 10%\)/.test(be[4]));
// Otra cotización de Energy: conserva el esquema que puso el administrador (sin pedir confirmación)
v=ok('tania','marcarAceptada',p1,cE2);
assert.equal(v.confirmar,undefined); assert.deepEqual(v.resumen.etapas.map(e=>e.monto),[580,580]);
// De regreso a Helgen: conserva el esquema (ahora editable por cobranza), sin pedir confirmación
v=ok('tania','marcarAceptada',p1,cH);
assert.equal(v.confirmar,undefined); assert.deepEqual(v.resumen.etapas.map(e=>e.monto),[696,348,116]);
assert.equal(v.confirmar,undefined); assert.deepEqual(v.resumen.etapas.map(e=>e.monto),[580,580]);
assert.deepEqual([v.esquemaEditable,v.esquemaPideMotivo],[true,false]);
v=ok('tania','guardarEsquema',p1,P.esquemaPorOmision());
assert.deepEqual(v.resumen.etapas.map(e=>e.monto),[696,348,116]);
assert.equal(bit().pop()[5],'','en Helgen el cambio no pide motivo');
assert.equal(H('Contratos').length,2,'una sola aceptada por proyecto');
// Firma: fecha obligatoria y no futura

View File

@ -90,6 +90,13 @@ if (ctx.monedaDeCotizacion) {
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');
// Encabezados de columna con la moneda solo en USD
assert(u.includes('P. Unitario (USD)</th>')&&u.includes('Importe (USD)</th>'));
assert(h.includes('>P. Unitario</th>')&&h.includes('>Importe</th>')&&!h.includes('(USD)'),'MXN sin moneda en columnas');
// Términos: el mismo bloque completo que en MXN (Energy: 4 renglones, texto idéntico, incluido "$300 MXN")
const bloque=x=>x.slice(x.indexOf('TÉRMINOS Y CONDICIONES'),x.indexOf('</ul>'));
assert.equal(li(u),4); assert.equal(bloque(u),bloque(e),'USD muestra los mismos términos que MXN');
assert(bloque(u).includes('$300 MXN por metro'));
fs.writeFileSync(process.argv[3]+'/usd.html',u);
}
console.log('OK');

View File

@ -136,7 +136,7 @@ const esperar=async(n=10)=>{for(let i=0;i<n;i++) await new Promise(r=>setImmedia
B.dialogos.confirmar=true; B.run("aceptarCot('HLG-2026-0011')"); await esperar();
const ma=B.llamadas.filter(c=>c.funcion==='marcarAceptada');
assert.equal(ma.length,3); assert.deepEqual(ma[2].args,['PRY-2026-0001','HLG-2026-0011',true]);
assert(/Energy: esquema fijo/.test(B.$('cobEsqNota').textContent)&&!/input/.test(B.$('cobEsquema').innerHTML),'Energy no editable');
assert(/Solo el administrador lo puede cambiar/.test(B.$('cobEsqNota').textContent)&&!/input/.test(B.$('cobEsquema').innerHTML),'Energy no editable para Tania');
assert(/\$1,392\.00 MXN/.test(B.$('cobEsquema').innerHTML),'monto 60% de 2320');
assert(/Contrato firmado/.test(B.$('cobContrato').innerHTML)&&B.$('cobFormPago').classList.contains('oculto'));
@ -173,6 +173,19 @@ const esperar=async(n=10)=>{for(let i=0;i<n;i++) await new Promise(r=>setImmedia
B.dialogos.prompt='Se capturó dos veces'; B.run("cancelarPagoCob('PAG-2026-0001')"); await esperar();
assert.deepEqual(B.llamadas.find(c=>c.funcion==='cancelarPago').args,['PAG-2026-0001','Se capturó dos veces']);
assert(/No tienes permiso de cobranza/.test(B.$('aviso').innerHTML));
// Administrador en un contrato de Energy sin firmar: edita el esquema con motivo obligatorio
const energyAdmin=vista({contrato:{cotizacion:'HLG-2026-0011',marca:'energy',moneda:'MXN',subtotal:2000,iva:320,total:2320,estado:'Aceptada'},
esquema:[{etapa:'Anticipo',porcentaje:60},{etapa:'Instalación',porcentaje:30},{etapa:'Encendido del sistema',porcentaje:10}],
esquemaEditable:true,esquemaPideMotivo:true});
B.run('pintarCobranza('+JSON.stringify(energyAdmin)+')');
assert(/<input/.test(B.$('cobEsquema').innerHTML)&&/cobMotivoEsq/.test(B.$('cobEsqAcciones').innerHTML)&&/motivo queda registrado/.test(B.$('cobEsqNota').textContent));
B.$('cobMotivoEsq').value=''; B.run('guardarEsquemaCob()'); await esperar();
assert(/motivo del cambio/.test(B.$('aviso').innerHTML)); assert(!B.llamadas.some(c=>c.funcion==='guardarEsquema'));
B.respuestas.guardarEsquema={ok:true,datos:energyAdmin};
B.run("editarEtapa(0,'porcentaje','50'); editarEtapa(1,'porcentaje','40')");
B.$('cobMotivoEsq').value='Negociado con el cliente'; B.run('guardarEsquemaCob()'); await esperar();
const ge=B.llamadas.find(c=>c.funcion==='guardarEsquema').args;
assert.deepEqual([ge[0],ge[1].map(e=>e.porcentaje),ge[2]],['PRY-2026-0001',['50','40',10],'Negociado con el cliente']);
// Administrador: ve el desbloqueo
B.run('pintarCobranza('+JSON.stringify({...firmada,puedeDesbloquear:true})+')');
assert(/Desbloquear contrato/.test(B.$('cobContrato').innerHTML));

View File

@ -907,9 +907,10 @@
// Esquema
$('cobModEsquema').classList.toggle('oculto', !c);
ESQ = (v.esquema || []).map(e => ({ etapa: e.etapa, porcentaje: e.porcentaje }));
$('cobEsqNota').textContent = !c ? '' : v.esquemaEditable
? 'Helgen: puedes cambiar nombres, porcentajes y número de etapas hasta firmar el contrato. Deben sumar 100%.'
: c.marca === 'energy' && !firmado ? 'Helgen Energy: esquema fijo (60% / 30% / 10%).' : 'Esquema bloqueado por el contrato firmado.';
$('cobEsqNota').textContent = !c ? '' : firmado ? 'Esquema bloqueado por el contrato firmado.'
: v.esquemaPideMotivo ? 'Helgen Energy: solo el administrador cambia este esquema; el motivo queda registrado. Deben sumar 100%.'
: v.esquemaEditable ? 'Helgen: puedes cambiar nombres, porcentajes y número de etapas hasta firmar el contrato. Deben sumar 100%.'
: 'Helgen Energy: esquema 60% / 30% / 10%. Solo el administrador lo puede cambiar.';
pintarEsquema();
// Pagos
@ -958,7 +959,9 @@
'</td><td>' + (edit && ESQ.length > 1 ? '<button type="button" class="quitar" title="Quitar etapa" onclick="quitarEtapa(' + i + ')">×</button>' : '') + '</td></tr>';
}).join('') + (ESQ.length ? '<tr><td></td><td><b>Suma</b></td><td class="num"><b' + (Math.abs(suma - 100) > 0.001 ? ' class="aviso-moneda"' : '') + '>' +
suma + '%</b></td><td class="num"><b>' + dinero(total, mon) + '</b></td><td colspan="4"></td></tr>' : '');
$('cobEsqAcciones').innerHTML = edit ? '<button type="button" class="btn btn-sec" onclick="agregarEtapa()">+ Agregar etapa</button>' +
$('cobEsqAcciones').innerHTML = edit ? (v.esquemaPideMotivo ? '<div style="flex:1;min-width:260px"><label>Motivo del cambio (queda registrado) *</label>' +
'<input id="cobMotivoEsq" maxlength="500" placeholder="Ej. El cliente negoció 50% de anticipo"></div>' : '') +
'<button type="button" class="btn btn-sec" onclick="agregarEtapa()">+ Agregar etapa</button>' +
'<button type="button" class="btn" id="btnEsquema" onclick="guardarEsquemaCob()">Guardar esquema</button>' : '';
}
// Al editar un nombre no se vuelve a pintar (no se pierde el cursor); el % sí actualiza montos y suma
@ -984,7 +987,9 @@
});
}
function guardarEsquemaCob() {
accionCob('btnEsquema', 'guardarEsquema', [COB.proyecto.folio, ESQ.map(e => ({ etapa: e.etapa, porcentaje: e.porcentaje }))],
const motivo = COB.esquemaPideMotivo ? $('cobMotivoEsq').value.trim() : '';
if (COB.esquemaPideMotivo && !motivo) { mostrarError('Escribe el motivo del cambio al esquema de Helgen Energy.'); return; }
accionCob('btnEsquema', 'guardarEsquema', [COB.proyecto.folio, ESQ.map(e => ({ etapa: e.etapa, porcentaje: e.porcentaje })), motivo],
v => { pintarCobranza(v); aviso('ok', 'Esquema de pagos guardado.'); });
}
function firmarCob() {