From 09e9b7acfbad86bde0a130baee235054a67bec01 Mon Sep 17 00:00:00 2001 From: "Efrain (Helgen)" Date: Tue, 20 Oct 2026 18:26:03 -0600 Subject: [PATCH] Cobranza: ocultar "Marcar aceptada" y "Contrato firmado" en proyectos cancelados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 9 ++++++--- Code.js | 3 +++ tests/cobranza.test.js | 13 ++++++++++++- tests/interfaz.test.js | 17 +++++++++++++++-- web/cotizador/index.html | 9 +++++++-- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 73a83e2..453ccc1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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')`, `desbloquearContrato(proyecto, motivo)` (además `esAdminDesbloqueo_`), `registrarPago(p)` (con `comprobante.datos` en base64 e idempotencia `PAG_`), `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**: 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_`. @@ -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. - 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"). -- Idea (no hecha): ocultar "Marcar aceptada" (y firmar) en proyectos `Cancelado`. Hoy el botón aparece, pero el servidor lo - rechaza ("El proyecto … está cancelado.", en `marcarAceptada` y `firmarContrato`). +- Proyectos `Cancelado` en Cobranza (20/10/2026, commit de este cambio): la vista trae `puedeAceptar` y `puedeFirmar` en falso y + 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. - 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). diff --git a/Code.js b/Code.js index f893553..a8ee20e 100644 --- a/Code.js +++ b/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(), esquemaEditable: !!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, puedeDesbloquear: !!c && c.estado === ESTADO_FIRMADO && esAdminDesbloqueo_(), metodos: METODOS_PAGO, diff --git a/tests/cobranza.test.js b/tests/cobranza.test.js index a434d72..9f37f0e 100644 --- a/tests/cobranza.test.js +++ b/tests/cobranza.test.js @@ -191,7 +191,7 @@ 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,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); +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','guardarEsquema',[p1,P.esquemaPorOmision()],/Primero marca/); 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); 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.deepEqual([v.puedeAceptar,v.puedeFirmar],[false,false],'firmado: ni aceptar ni firmar'); falla('tania','firmarContrato',[p1,HOY_ISO],/ya está firmado/); falla('tania','marcarAceptada',[p1,cBarata],/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) assert.deepEqual(Object.keys(db.pest).sort(),['Config','Cotizaciones','Proyectos']); 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'); diff --git a/tests/interfaz.test.js b/tests/interfaz.test.js index 7c946c1..8d2a97c 100644 --- a/tests/interfaz.test.js +++ b/tests/interfaz.test.js @@ -137,8 +137,12 @@ const esperar=async(n=10)=>{for(let i=0;isetImmedia 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'}, {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, - puedePagar:false,puedeDesbloquear:false,metodos:['Transferencia','Efectivo','Cheque'],hoy:'2026-10-20',...extra}); + // Como vistaCobranza_: se acepta mientras no esté firmado; se firma con aceptada (proyecto no cancelado) + 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.$('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'); @@ -191,6 +195,15 @@ const esperar=async(n=10)=>{for(let i=0;isetImmedia 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)); + // 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 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}], diff --git a/web/cotizador/index.html b/web/cotizador/index.html index 4effe0b..ef09e70 100644 --- a/web/cotizador/index.html +++ b/web/cotizador/index.html @@ -907,13 +907,16 @@ '' + dinero(q.subtotal, q.moneda) + '' + dinero(q.total, q.moneda) + '' + (q.pdf ? 'Ver' : '') + '' + (esAceptada ? 'Aceptada' - : firmado ? '' : '') + + : !v.puedeAceptar ? '' : '') + ''; }).join('') : 'Este proyecto no tiene cotizaciones.'; // Contrato let h = ''; - if (!c) h = 'Marca la cotización aceptada para definir el monto contratado.'; + const cancelado = v.proyecto.estado === 'Cancelado'; + const avisoCancelado = '
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.
'; + if (!c) h = cancelado ? avisoCancelado : 'Marca la cotización aceptada para definir el monto contratado.'; else { h = '
Monto contratado: ' + dinero(c.total, mon) + ' (con IVA) · subtotal ' + dinero(c.subtotal, mon) + ' · ' + esc(MARCA_TXT[c.marca]) + '
'; @@ -925,6 +928,8 @@ '' + '
'; } + } else if (!v.puedeFirmar) { + h += cancelado ? avisoCancelado : ''; } else { h += '
' + '
' +