mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Cobranza: ocultar "Marcar aceptada" y "Contrato firmado" en proyectos cancelados
vistaCobranza_ manda puedeAceptar y puedeFirmar (falsos si el proyecto está Cancelado o, en el caso de aceptar, si ya está firmado); la interfaz oculta esos botones y muestra un aviso con cómo reactivarlo. El servidor ya rechazaba ambas acciones. Pruebas de cobranza e interfaz ampliadas. Publicar la API antes que la interfaz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
847c5441c8
commit
09e9b7acfb
@ -167,7 +167,8 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e
|
|||||||
`guardarEsquema(proyecto, etapas, motivo)` (Energy: solo `ADMIN_DESBLOQUEO` y motivo obligatorio en la Bitacora), `firmarContrato(proyecto, 'AAAA-MM-DD')`,
|
`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
|
`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
|
e idempotencia `PAG_<idSolicitud>`), `cancelarPago(folio, motivo)`. Las que cambian algo usan `conCandado_` y devuelven la vista
|
||||||
(`vistaCobranza_`).
|
(`vistaCobranza_`: además de los datos, `puedeAceptar`, `puedeFirmar` —falsos si el proyecto está `Cancelado`—, `puedePagar`,
|
||||||
|
`puedeDesbloquear`, `esquemaEditable`, `esquemaPideMotivo`; la interfaz muestra los botones según estos campos).
|
||||||
- Internas: `carpetaCobranza_` y `libroCobranza_` (se crean solas la primera vez; si el ID existe pero no se encuentra, **error**:
|
- Internas: `carpetaCobranza_` y `libroCobranza_` (se crean solas la primera vez; si el ID existe pero no se encuentra, **error**:
|
||||||
no se crea otra a escondidas), `libroCobranzaSiExiste_` (las cotizaciones no crean el libro), `hojaCobranza_(libro, nombre)`
|
no se crea otra a escondidas), `libroCobranzaSiExiste_` (las cotizaciones no crean el libro), `hojaCobranza_(libro, nombre)`
|
||||||
(encabezados que se migran solos, `HOJAS_COBRANZA`), `datosCobranza_`, `escribirEsquema_`, `bitacora_`, `cotizacionesDeProyecto_`.
|
(encabezados que se migran solos, `HOJAS_COBRANZA`), `datosCobranza_`, `escribirEsquema_`, `bitacora_`, `cotizacionesDeProyecto_`.
|
||||||
@ -530,8 +531,10 @@ El commit y el push no publican nada: el equipo sigue usando la versión anterio
|
|||||||
solas (B4 = 18.4250); libro de cobranza con encabezados y sin datos.
|
solas (B4 = 18.4250); libro de cobranza con encabezados y sin datos.
|
||||||
- Catálogo real: solo tiene el renglón `HSE-EJ` "EJEMPLO" ($1,000, sin "Actualizado"). Parece de ejemplo: borrarlo o cargar los
|
- Catálogo real: solo tiene el renglón `HSE-EJ` "EJEMPLO" ($1,000, sin "Actualizado"). Parece de ejemplo: borrarlo o cargar los
|
||||||
productos reales (ver la idea de importar el "Catálogo Maestro de Precios - Helgen Energy").
|
productos reales (ver la idea de importar el "Catálogo Maestro de Precios - Helgen Energy").
|
||||||
- Idea (no hecha): ocultar "Marcar aceptada" (y firmar) en proyectos `Cancelado`. Hoy el botón aparece, pero el servidor lo
|
- Proyectos `Cancelado` en Cobranza (20/10/2026, commit de este cambio): la vista trae `puedeAceptar` y `puedeFirmar` en falso y
|
||||||
rechaza ("El proyecto … está cancelado.", en `marcarAceptada` y `firmarContrato`).
|
la interfaz oculta "Marcar aceptada" y "Contrato firmado" con un aviso (reactivar = Estado Activo en la hoja). El servidor ya lo
|
||||||
|
rechazaba. **Publicar primero la API y después la interfaz**: la interfaz nueva con la API vieja (sin esos campos) ocultaría
|
||||||
|
los botones en todos los proyectos.
|
||||||
- Rol de cobranza **sin** desbloqueo (Tania) solo se probó con las pruebas automáticas, no con su cuenta real.
|
- Rol de cobranza **sin** desbloqueo (Tania) solo se probó con las pruebas automáticas, no con su cuenta real.
|
||||||
- Precio USD del catálogo (20/10/2026): el tipo de cambio usado se guarda en `Cotizaciones!V` (no sale en el PDF). Leer
|
- Precio USD del catálogo (20/10/2026): el tipo de cambio usado se guarda en `Cotizaciones!V` (no sale en el PDF). Leer
|
||||||
`GOOGLEFINANCE` desde Apps Script **sí funciona** (comprobado en PRUEBAS: 18.4248); si un día falla, usar el valor manual (B3).
|
`GOOGLEFINANCE` desde Apps Script **sí funciona** (comprobado en PRUEBAS: 18.4248); si un día falla, usar el valor manual (B3).
|
||||||
|
|||||||
3
Code.js
3
Code.js
@ -1276,6 +1276,9 @@ function vistaCobranza_(libro, proyecto) {
|
|||||||
pagos: d.pagos.map(p => { const o = Object.assign({}, p); delete o.fila; return o; }).reverse(),
|
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' || esAdminDesbloqueo_()),
|
esquemaEditable: !!c && c.estado !== ESTADO_FIRMADO && (c.marca !== 'energy' || esAdminDesbloqueo_()),
|
||||||
esquemaPideMotivo: !!c && c.estado !== ESTADO_FIRMADO && c.marca === 'energy' && esAdminDesbloqueo_(),
|
esquemaPideMotivo: !!c && c.estado !== ESTADO_FIRMADO && c.marca === 'energy' && esAdminDesbloqueo_(),
|
||||||
|
// Proyecto cancelado: no se acepta ni se firma (marcarAceptada y firmarContrato también lo rechazan)
|
||||||
|
puedeAceptar: proyecto.estado !== 'Cancelado' && !(c && c.estado === ESTADO_FIRMADO),
|
||||||
|
puedeFirmar: proyecto.estado !== 'Cancelado' && !!c && c.estado === ESTADO_ACEPTADA,
|
||||||
puedePagar: !!c && c.estado === ESTADO_FIRMADO,
|
puedePagar: !!c && c.estado === ESTADO_FIRMADO,
|
||||||
puedeDesbloquear: !!c && c.estado === ESTADO_FIRMADO && esAdminDesbloqueo_(),
|
puedeDesbloquear: !!c && c.estado === ESTADO_FIRMADO && esAdminDesbloqueo_(),
|
||||||
metodos: METODOS_PAGO,
|
metodos: METODOS_PAGO,
|
||||||
|
|||||||
@ -191,7 +191,7 @@ const H=n=>LC.pest[n].m, bit=()=>H('Bitacora').slice(1);
|
|||||||
let v=ok('tania','getCobranzaProyecto',p1.toLowerCase());
|
let v=ok('tania','getCobranzaProyecto',p1.toLowerCase());
|
||||||
assert.equal(v.contrato,null); assert.deepEqual(v.cotizaciones.map(c=>c.folio),[cH,cE,cU,cBarata,cE2]);
|
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.deepEqual(v.cotizaciones.map(c=>c.moneda+'/'+c.marca),['MXN/helgen','MXN/energy','USD/helgen','MXN/helgen','MXN/energy']);
|
||||||
assert.equal(v.puedePagar,false);
|
assert.equal(v.puedePagar,false); assert.deepEqual([v.puedeAceptar,v.puedeFirmar],[true,false],'sin aceptada: se acepta, no se firma');
|
||||||
falla('tania','firmarContrato',[p1,HOY_ISO],/Primero marca la cotización aceptada/);
|
falla('tania','firmarContrato',[p1,HOY_ISO],/Primero marca la cotización aceptada/);
|
||||||
falla('tania','guardarEsquema',[p1,P.esquemaPorOmision()],/Primero marca/);
|
falla('tania','guardarEsquema',[p1,P.esquemaPorOmision()],/Primero marca/);
|
||||||
falla('tania','registrarPago',[{proyectoFolio:p1,comprobante:{datos:PDF.toString('base64')}}],/contrato firmado/);
|
falla('tania','registrarPago',[{proyectoFolio:p1,comprobante:{datos:PDF.toString('base64')}}],/contrato firmado/);
|
||||||
@ -243,6 +243,7 @@ falla('tania','firmarContrato',[p1,''],/obligatoria/);
|
|||||||
v=ok('tania','firmarContrato',p1,HOY_ISO);
|
v=ok('tania','firmarContrato',p1,HOY_ISO);
|
||||||
assert.deepEqual([v.contrato.estado,v.contrato.fechaFirma,v.contrato.firmadoPor],['Firmado',HOY_ISO,'tania@helgen.tech']);
|
assert.deepEqual([v.contrato.estado,v.contrato.fechaFirma,v.contrato.firmadoPor],['Firmado',HOY_ISO,'tania@helgen.tech']);
|
||||||
assert.equal(v.puedePagar,true); assert.equal(v.esquemaEditable,false); assert.equal(v.puedeDesbloquear,false,'Tania no desbloquea');
|
assert.equal(v.puedePagar,true); assert.equal(v.esquemaEditable,false); assert.equal(v.puedeDesbloquear,false,'Tania no desbloquea');
|
||||||
|
assert.deepEqual([v.puedeAceptar,v.puedeFirmar],[false,false],'firmado: ni aceptar ni firmar');
|
||||||
falla('tania','firmarContrato',[p1,HOY_ISO],/ya está firmado/);
|
falla('tania','firmarContrato',[p1,HOY_ISO],/ya está firmado/);
|
||||||
falla('tania','marcarAceptada',[p1,cBarata],/Contrato firmado/);
|
falla('tania','marcarAceptada',[p1,cBarata],/Contrato firmado/);
|
||||||
falla('tania','guardarEsquema',[p1,P.esquemaPorOmision()],/Contrato firmado/);
|
falla('tania','guardarEsquema',[p1,P.esquemaPorOmision()],/Contrato firmado/);
|
||||||
@ -340,4 +341,14 @@ assert.deepEqual([carpCob.acceso,archivos[LC.id].acceso],['PRIVATE','PRIVATE'],'
|
|||||||
// Los datos de cobranza nunca van en la Base de datos (todo el dominio la puede leer)
|
// Los datos de cobranza nunca van en la Base de datos (todo el dominio la puede leer)
|
||||||
assert.deepEqual(Object.keys(db.pest).sort(),['Config','Cotizaciones','Proyectos']);
|
assert.deepEqual(Object.keys(db.pest).sort(),['Config','Cotizaciones','Proyectos']);
|
||||||
assert(!JSON.stringify(db.pest).includes('PAG-'));
|
assert(!JSON.stringify(db.pest).includes('PAG-'));
|
||||||
|
// --- Proyecto cancelado: la vista no ofrece aceptar ni firmar y el servidor lo rechaza ---
|
||||||
|
const p3=ok('ana','crearProyecto',{nombre:'Se canceló',cliente:{nombre:'Luis'},idSolicitud:'p3'}).folio;
|
||||||
|
const c3=cot(p3,100), c3b=cot(p3,200);
|
||||||
|
v=ok('tania','marcarAceptada',p3,c3); assert.deepEqual([v.puedeAceptar,v.puedeFirmar],[true,true],'aceptada: se puede firmar');
|
||||||
|
const filaP3=db.pest.Proyectos.m.findIndex(r=>r[0]===p3); db.pest.Proyectos.m[filaP3][6]='Cancelado';
|
||||||
|
v=ok('tania','getCobranzaProyecto',p3);
|
||||||
|
assert.deepEqual([v.proyecto.estado,v.puedeAceptar,v.puedeFirmar],['Cancelado',false,false]);
|
||||||
|
falla('tania','marcarAceptada',[p3,c3b],/está cancelado/); falla('tania','firmarContrato',[p3,HOY_ISO],/está cancelado/);
|
||||||
|
db.pest.Proyectos.m[filaP3][6]='Activo';
|
||||||
|
assert.deepEqual([ok('tania','getCobranzaProyecto',p3).puedeAceptar,ok('tania','getCobranzaProyecto',p3).puedeFirmar],[true,true],'reactivado');
|
||||||
console.log('OK cobranza');
|
console.log('OK cobranza');
|
||||||
|
|||||||
@ -137,8 +137,12 @@ const esperar=async(n=10)=>{for(let i=0;i<n;i++) await new Promise(r=>setImmedia
|
|||||||
assert(/PRY-2026-0001 — Luis — Solar MX — Techo/.test(B.$('cobProyectosList').innerHTML)&&/Sin aceptada/.test(B.$('cobProyectosList').innerHTML));
|
assert(/PRY-2026-0001 — Luis — Solar MX — Techo/.test(B.$('cobProyectosList').innerHTML)&&/Sin aceptada/.test(B.$('cobProyectosList').innerHTML));
|
||||||
const cots=[{folio:'HLG-2026-0010',fecha:'20/10/2026',marca:'helgen',moneda:'MXN',subtotal:1000,total:1160,pdf:'p'},
|
const cots=[{folio:'HLG-2026-0010',fecha:'20/10/2026',marca:'helgen',moneda:'MXN',subtotal:1000,total:1160,pdf:'p'},
|
||||||
{folio:'HLG-2026-0011',fecha:'20/10/2026',marca:'energy',moneda:'MXN',subtotal:2000,total:2320,pdf:'p'}];
|
{folio:'HLG-2026-0011',fecha:'20/10/2026',marca:'energy',moneda:'MXN',subtotal:2000,total:2320,pdf:'p'}];
|
||||||
const vista=(extra)=>({proyecto:PRY,cotizaciones:cots,contrato:null,esquema:[],resumen:null,pagos:[],esquemaEditable:false,
|
// Como vistaCobranza_: se acepta mientras no esté firmado; se firma con aceptada (proyecto no cancelado)
|
||||||
puedePagar:false,puedeDesbloquear:false,metodos:['Transferencia','Efectivo','Cheque'],hoy:'2026-10-20',...extra});
|
const vista=(extra={})=>{const v={proyecto:PRY,cotizaciones:cots,contrato:null,esquema:[],resumen:null,pagos:[],esquemaEditable:false,
|
||||||
|
puedePagar:false,puedeDesbloquear:false,metodos:['Transferencia','Efectivo','Cheque'],hoy:'2026-10-20',...extra};
|
||||||
|
if(!('puedeAceptar' in extra)) v.puedeAceptar=!(v.contrato&&v.contrato.estado==='Firmado');
|
||||||
|
if(!('puedeFirmar' in extra)) v.puedeFirmar=!!v.contrato&&v.contrato.estado==='Aceptada';
|
||||||
|
return v};
|
||||||
B.respuestas.getCobranzaProyecto={ok:true,datos:vista()};
|
B.respuestas.getCobranzaProyecto={ok:true,datos:vista()};
|
||||||
B.$('cobProyecto').value='PRY-2026-0001 — Luis — Solar MX — Techo'; B.$('cobProyecto').disparar('change'); await esperar();
|
B.$('cobProyecto').value='PRY-2026-0001 — Luis — Solar MX — Techo'; B.$('cobProyecto').disparar('change'); await esperar();
|
||||||
assert.equal(B.llamadas.find(c=>c.funcion==='getCobranzaProyecto').args[0],'PRY-2026-0001');
|
assert.equal(B.llamadas.find(c=>c.funcion==='getCobranzaProyecto').args[0],'PRY-2026-0001');
|
||||||
@ -191,6 +195,15 @@ 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();
|
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.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));
|
assert(/No tienes permiso de cobranza/.test(B.$('aviso').innerHTML));
|
||||||
|
// Proyecto cancelado: sin "Marcar aceptada" ni "Contrato firmado", con aviso
|
||||||
|
const cancel=vista({proyecto:{...PRY,estado:'Cancelado'},puedeAceptar:false,puedeFirmar:false,
|
||||||
|
contrato:{cotizacion:'HLG-2026-0010',marca:'helgen',moneda:'MXN',subtotal:1000,iva:160,total:1160,estado:'Aceptada'},
|
||||||
|
esquema:[{etapa:'Anticipo',porcentaje:100}]});
|
||||||
|
B.run('pintarCobranza('+JSON.stringify(cancel)+')');
|
||||||
|
assert(!/Marcar aceptada/.test(B.$('cobCots').innerHTML)&&!/btnFirmar|Contrato firmado/.test(B.$('cobContrato').innerHTML),'cancelado: sin aceptar ni firmar');
|
||||||
|
assert(/Proyecto cancelado: no se puede marcar/.test(B.$('cobContrato').innerHTML)&&/chip cancelado">Cancelado/.test(B.$('cobInfo').innerHTML));
|
||||||
|
B.run('pintarCobranza('+JSON.stringify(vista({proyecto:{...PRY,estado:'Cancelado'},puedeAceptar:false,puedeFirmar:false}))+')');
|
||||||
|
assert(!/Marcar aceptada/.test(B.$('cobCots').innerHTML)&&/Proyecto cancelado/.test(B.$('cobContrato').innerHTML),'cancelado sin aceptada');
|
||||||
// Administrador en un contrato de Energy sin firmar: edita el esquema con motivo obligatorio
|
// 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'},
|
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}],
|
esquema:[{etapa:'Anticipo',porcentaje:60},{etapa:'Instalación',porcentaje:30},{etapa:'Encendido del sistema',porcentaje:10}],
|
||||||
|
|||||||
@ -907,13 +907,16 @@
|
|||||||
'</td><td class="num">' + dinero(q.subtotal, q.moneda) + '</td><td class="num">' + dinero(q.total, q.moneda) + '</td><td>' +
|
'</td><td class="num">' + dinero(q.subtotal, q.moneda) + '</td><td class="num">' + dinero(q.total, q.moneda) + '</td><td>' +
|
||||||
(q.pdf ? '<a href="' + esc(q.pdf) + '" target="_blank">Ver</a>' : '') + '</td><td>' +
|
(q.pdf ? '<a href="' + esc(q.pdf) + '" target="_blank">Ver</a>' : '') + '</td><td>' +
|
||||||
(esAceptada ? '<span class="chip ' + (firmado ? 'firmado' : 'aceptada') + '">Aceptada</span>'
|
(esAceptada ? '<span class="chip ' + (firmado ? 'firmado' : 'aceptada') + '">Aceptada</span>'
|
||||||
: firmado ? '' : '<button type="button" class="btn btn-sec" onclick="aceptarCot(\'' + esc(q.folio) + '\')">Marcar aceptada</button>') +
|
: !v.puedeAceptar ? '' : '<button type="button" class="btn btn-sec" onclick="aceptarCot(\'' + esc(q.folio) + '\')">Marcar aceptada</button>') +
|
||||||
'</td></tr>';
|
'</td></tr>';
|
||||||
}).join('') : '<tr><td colspan="8" class="nota">Este proyecto no tiene cotizaciones.</td></tr>';
|
}).join('') : '<tr><td colspan="8" class="nota">Este proyecto no tiene cotizaciones.</td></tr>';
|
||||||
|
|
||||||
// Contrato
|
// Contrato
|
||||||
let h = '';
|
let h = '';
|
||||||
if (!c) h = '<span class="nota">Marca la cotización aceptada para definir el monto contratado.</span>';
|
const cancelado = v.proyecto.estado === 'Cancelado';
|
||||||
|
const avisoCancelado = '<div class="nota aviso-moneda" style="margin-top:6px">Proyecto cancelado: no se puede marcar la cotización ' +
|
||||||
|
'aceptada ni registrar el contrato firmado. Para reactivarlo, cambia su Estado a Activo en la hoja Proyectos.</div>';
|
||||||
|
if (!c) h = cancelado ? avisoCancelado : '<span class="nota">Marca la cotización aceptada para definir el monto contratado.</span>';
|
||||||
else {
|
else {
|
||||||
h = '<div class="sat-titulo">Monto contratado: ' + dinero(c.total, mon) + ' (con IVA) · subtotal ' + dinero(c.subtotal, mon) +
|
h = '<div class="sat-titulo">Monto contratado: ' + dinero(c.total, mon) + ' (con IVA) · subtotal ' + dinero(c.subtotal, mon) +
|
||||||
' · ' + esc(MARCA_TXT[c.marca]) + '</div>';
|
' · ' + esc(MARCA_TXT[c.marca]) + '</div>';
|
||||||
@ -925,6 +928,8 @@
|
|||||||
'<input id="cobMotivo" maxlength="500" placeholder="Ej. Se firmó con la cotización equivocada"></div>' +
|
'<input id="cobMotivo" maxlength="500" placeholder="Ej. Se firmó con la cotización equivocada"></div>' +
|
||||||
'<div class="acciones"><button type="button" class="btn btn-sec" id="btnDesbloquear" onclick="desbloquearCob()">Desbloquear contrato</button></div>';
|
'<div class="acciones"><button type="button" class="btn btn-sec" id="btnDesbloquear" onclick="desbloquearCob()">Desbloquear contrato</button></div>';
|
||||||
}
|
}
|
||||||
|
} else if (!v.puedeFirmar) {
|
||||||
|
h += cancelado ? avisoCancelado : '';
|
||||||
} else {
|
} else {
|
||||||
h += '<div class="proy-fila" style="margin-top:8px"><div style="max-width:220px"><label>Fecha de firma *</label>' +
|
h += '<div class="proy-fila" style="margin-top:8px"><div style="max-width:220px"><label>Fecha de firma *</label>' +
|
||||||
'<input id="cobFechaFirma" type="date" max="' + esc(v.hoy) + '" value="' + esc(v.hoy) + '"></div>' +
|
'<input id="cobFechaFirma" type="date" max="' + esc(v.hoy) + '" value="' + esc(v.hoy) + '"></div>' +
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user