mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Cobranza: fechas de pago y firma legibles (Sheets las convertía en fecha)
En la prueba de punta a punta la fecha del pago se mostraba como "Tue Oct 20 2026 00:00:00 GMT-0600…": appendRow convierte "AAAA-MM-DD" en fecha aunque la columna tenga formato de texto. Ahora fecha_pago y fecha_firma se escriben con apóstrofo (texto, sin depender de la zona horaria de la hoja) y al leer se normalizan las filas ya guardadas como fecha. Firmar y desbloquear ya no reescriben aceptada_por / fecha_aceptacion. La prueba simula ese comportamiento de Sheets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
decddbb973
commit
b1b3bb94d0
23
Code.js
23
Code.js
@ -1135,14 +1135,14 @@ function contratoDesdeFila_(r, fila) {
|
|||||||
return {
|
return {
|
||||||
proyectoFolio: String(r[0]), cotizacion: String(r[1]), marca: marcaDeTexto_(r[2]), moneda: String(r[3] || 'MXN'),
|
proyectoFolio: String(r[0]), cotizacion: String(r[1]), marca: marcaDeTexto_(r[2]), moneda: String(r[3] || 'MXN'),
|
||||||
subtotal: Number(r[4]) || 0, iva: Number(r[5]) || 0, total: Number(r[6]) || 0, estado: String(r[7] || ''),
|
subtotal: Number(r[4]) || 0, iva: Number(r[5]) || 0, total: Number(r[6]) || 0, estado: String(r[7] || ''),
|
||||||
aceptadaPor: String(r[8] || ''), fechaAceptacion: fechaTexto_(r[9]), fechaFirma: String(r[10] || ''),
|
aceptadaPor: String(r[8] || ''), fechaAceptacion: fechaTexto_(r[9]), fechaFirma: fechaIso_(r[10]),
|
||||||
firmadoPor: String(r[11] || ''), fila: fila,
|
firmadoPor: String(r[11] || ''), fila: fila,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function pagoDesdeFila_(r, fila) {
|
function pagoDesdeFila_(r, fila) {
|
||||||
return {
|
return {
|
||||||
folio: String(r[0]), fecha: String(r[1] || ''), fechaRegistro: fechaTexto_(r[2]), proyectoFolio: String(r[3]),
|
folio: String(r[0]), fecha: fechaIso_(r[1]), fechaRegistro: fechaTexto_(r[2]), proyectoFolio: String(r[3]),
|
||||||
monto: Number(r[4]) || 0, monedaPago: String(r[5] || ''), tipoCambio: r[6] === '' ? '' : Number(r[6]),
|
monto: Number(r[4]) || 0, monedaPago: String(r[5] || ''), tipoCambio: r[6] === '' ? '' : Number(r[6]),
|
||||||
montoAplicado: Number(r[7]) || 0, monedaProyecto: String(r[8] || ''), metodo: String(r[9] || ''),
|
montoAplicado: Number(r[7]) || 0, monedaProyecto: String(r[8] || ''), metodo: String(r[9] || ''),
|
||||||
referencia: String(r[10] || ''), etapaOrden: Number(r[11]) || 1, etapa: String(r[12] || ''),
|
referencia: String(r[10] || ''), etapaOrden: Number(r[11]) || 1, etapa: String(r[12] || ''),
|
||||||
@ -1155,6 +1155,15 @@ function fechaTexto_(v) {
|
|||||||
return v instanceof Date ? Utilities.formatDate(v, 'America/Mexico_City', 'dd/MM/yyyy HH:mm') : String(v || '');
|
return v instanceof Date ? Utilities.formatDate(v, 'America/Mexico_City', 'dd/MM/yyyy HH:mm') : String(v || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fecha sin hora (pago, firma) como "AAAA-MM-DD". Se escriben con apóstrofo (textoFecha_) para que Sheets no las
|
||||||
|
* convierta en fecha (appendRow lo hace aunque la columna sea texto); las filas guardadas antes vienen como Date.
|
||||||
|
*/
|
||||||
|
function fechaIso_(v) {
|
||||||
|
return v instanceof Date ? Utilities.formatDate(v, 'America/Mexico_City', 'yyyy-MM-dd') : String(v || '');
|
||||||
|
}
|
||||||
|
const textoFecha_ = iso => iso ? "'" + iso : '';
|
||||||
|
|
||||||
const hoyIso_ = () => Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd');
|
const hoyIso_ = () => Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd');
|
||||||
|
|
||||||
/** Todo lo de cobranza de un proyecto, leído del libro. */
|
/** Todo lo de cobranza de un proyecto, leído del libro. */
|
||||||
@ -1380,8 +1389,9 @@ function firmarContrato(proyectoFolio, fecha) {
|
|||||||
formatoMoneda(r.contratado) + ').');
|
formatoMoneda(r.contratado) + ').');
|
||||||
}
|
}
|
||||||
escribirEsquema_(d.hojas.esquemas, proyecto.folio, etapas, c.total);
|
escribirEsquema_(d.hojas.esquemas, proyecto.folio, etapas, c.total);
|
||||||
d.hojas.contratos.getRange(c.fila, 8, 1, 6).setValues([[ESTADO_FIRMADO, c.aceptadaPor, c.fechaAceptacion, fechaFirma,
|
d.hojas.contratos.getRange(c.fila, 8).setValue(ESTADO_FIRMADO);
|
||||||
textoCelda(VENDEDOR_.email), Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm')]]);
|
d.hojas.contratos.getRange(c.fila, 11, 1, 3).setValues([[textoFecha_(fechaFirma), textoCelda(VENDEDOR_.email),
|
||||||
|
Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm')]]);
|
||||||
bitacora_(libro, 'Contrato firmado', proyecto.folio, 'Fecha de firma ' + fechaFirma + ' · ' + c.cotizacion + ' · ' +
|
bitacora_(libro, 'Contrato firmado', proyecto.folio, 'Fecha de firma ' + fechaFirma + ' · ' + c.cotizacion + ' · ' +
|
||||||
formatoMoneda(c.total) + ' ' + c.moneda + ' · ' + etapas.map(e => e.etapa + ' ' + e.porcentaje + '%').join(' · '));
|
formatoMoneda(c.total) + ' ' + c.moneda + ' · ' + etapas.map(e => e.etapa + ' ' + e.porcentaje + '%').join(' · '));
|
||||||
SpreadsheetApp.flush();
|
SpreadsheetApp.flush();
|
||||||
@ -1402,7 +1412,8 @@ function desbloquearContrato(proyectoFolio, motivo) {
|
|||||||
const d = datosCobranza_(libro, proyecto.folio);
|
const d = datosCobranza_(libro, proyecto.folio);
|
||||||
const c = d.contrato;
|
const c = d.contrato;
|
||||||
if (!c || c.estado !== ESTADO_FIRMADO) throw new Error('El contrato de ' + proyecto.folio + ' no está firmado.');
|
if (!c || c.estado !== ESTADO_FIRMADO) throw new Error('El contrato de ' + proyecto.folio + ' no está firmado.');
|
||||||
d.hojas.contratos.getRange(c.fila, 8, 1, 6).setValues([[ESTADO_ACEPTADA, c.aceptadaPor, c.fechaAceptacion, '', '',
|
d.hojas.contratos.getRange(c.fila, 8).setValue(ESTADO_ACEPTADA);
|
||||||
|
d.hojas.contratos.getRange(c.fila, 11, 1, 3).setValues([['', '',
|
||||||
Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm')]]);
|
Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm')]]);
|
||||||
bitacora_(libro, 'Desbloquear contrato', proyecto.folio,
|
bitacora_(libro, 'Desbloquear contrato', proyecto.folio,
|
||||||
'Estaba firmado el ' + c.fechaFirma + ' por ' + c.firmadoPor + ' · ' + c.cotizacion, m);
|
'Estaba firmado el ' + c.fechaFirma + ' por ' + c.firmadoPor + ' · ' + c.cotizacion, m);
|
||||||
@ -1461,7 +1472,7 @@ function registrarPago(p) {
|
|||||||
|
|
||||||
const t = textoCelda;
|
const t = textoCelda;
|
||||||
hojaPagos.appendRow([
|
hojaPagos.appendRow([
|
||||||
folio, pago.fecha, Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm:ss'), proyecto.folio,
|
folio, textoFecha_(pago.fecha), Utilities.formatDate(new Date(), 'America/Mexico_City', 'yyyy-MM-dd HH:mm:ss'), proyecto.folio,
|
||||||
pago.monto, pago.monedaPago, pago.tipoCambio, pago.montoAplicado, c.moneda, pago.metodo, t(pago.referencia),
|
pago.monto, pago.monedaPago, pago.tipoCambio, pago.montoAplicado, c.moneda, pago.metodo, t(pago.referencia),
|
||||||
pago.etapaOrden, t(d.etapas[pago.etapaOrden - 1].etapa), JSON.stringify(aplicacion), archivo.getId(), archivo.getUrl(),
|
pago.etapaOrden, t(d.etapas[pago.etapaOrden - 1].etapa), JSON.stringify(aplicacion), archivo.getId(), archivo.getUrl(),
|
||||||
t(VENDEDOR_.email), t([VENDEDOR_.nombre, VENDEDOR_.apellido].filter(Boolean).join(' ')), 'Vigente', '', '', '',
|
t(VENDEDOR_.email), t([VENDEDOR_.nombre, VENDEDOR_.apellido].filter(Boolean).join(' ')), 'Vigente', '', '', '',
|
||||||
|
|||||||
@ -11,13 +11,18 @@ const pad=n=>String(n).padStart(2,'0');
|
|||||||
const HOY=new Date(), HOY_ISO=HOY.getFullYear()+'-'+pad(HOY.getMonth()+1)+'-'+pad(HOY.getDate());
|
const HOY=new Date(), HOY_ISO=HOY.getFullYear()+'-'+pad(HOY.getMonth()+1)+'-'+pad(HOY.getDate());
|
||||||
|
|
||||||
// ---- Hojas y libros simulados ----
|
// ---- Hojas y libros simulados ----
|
||||||
|
// Como Sheets: un texto que parece fecha ("2026-10-20", "2026-10-20 17:30") se guarda como fecha; con apóstrofo
|
||||||
|
// inicial se guarda como texto (y getValues lo devuelve sin el apóstrofo).
|
||||||
|
const comoSheets=v=>{if(typeof v!=='string')return v;if(v[0]==="'")return v.slice(1);
|
||||||
|
const x=v.match(/^(\d{4})-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/);
|
||||||
|
return x?new Date(+x[1],+x[2]-1,+x[3],+(x[4]||0),+(x[5]||0),+(x[6]||0)):v};
|
||||||
function hoja(m){const h={m,getLastRow:()=>m.length,getMaxRows:()=>1000,setFrozenRows(){},setColumnWidth(){},
|
function hoja(m){const h={m,getLastRow:()=>m.length,getMaxRows:()=>1000,setFrozenRows(){},setColumnWidth(){},
|
||||||
getDataRange:()=>({getValues:()=>m.map(r=>r.slice()),getDisplayValues:()=>m.map(r=>r.map(x=>x instanceof Date?'20/10/2026':String(x??'')))}),
|
getDataRange:()=>({getValues:()=>m.map(r=>r.slice()),getDisplayValues:()=>m.map(r=>r.map(x=>x instanceof Date?'20/10/2026':String(x??'')))}),
|
||||||
appendRow:a=>m.push(a),deleteRow:n=>m.splice(n-1,1),
|
appendRow:a=>m.push(a.map(comoSheets)),deleteRow:n=>m.splice(n-1,1),
|
||||||
getRange:(r,c=1,nr=1,nc=1)=>typeof r==='string'?{setNumberFormat(){return this},setDataValidation(){return this}}:({
|
getRange:(r,c=1,nr=1,nc=1)=>typeof r==='string'?{setNumberFormat(){return this},setDataValidation(){return this}}:({
|
||||||
getValues:()=>Array.from({length:nr},(_,i)=>{const f=(m[r-1+i]||[]).slice(c-1,c-1+nc);while(f.length<nc)f.push('');return f}),
|
getValues:()=>Array.from({length:nr},(_,i)=>{const f=(m[r-1+i]||[]).slice(c-1,c-1+nc);while(f.length<nc)f.push('');return f}),
|
||||||
setValue(v){m[r-1][c-1]=v;return this},
|
setValue(v){m[r-1][c-1]=comoSheets(v);return this},
|
||||||
setValues(vs){vs.forEach((x,i)=>{m[r-1+i]=m[r-1+i]||[];x.forEach((y,j)=>m[r-1+i][c-1+j]=y)});return this},
|
setValues(vs){vs.forEach((x,i)=>{m[r-1+i]=m[r-1+i]||[];x.forEach((y,j)=>m[r-1+i][c-1+j]=comoSheets(y))});return this},
|
||||||
setNumberFormat(){return this},setFontWeight(){return this},setBackground(){return this},setFontColor(){return this},setDataValidation(){return this}})};
|
setNumberFormat(){return this},setFontWeight(){return this},setBackground(){return this},setFontColor(){return this},setDataValidation(){return this}})};
|
||||||
return h}
|
return h}
|
||||||
const libros={};
|
const libros={};
|
||||||
@ -247,6 +252,10 @@ assert.equal(r.folio,'PAG-'+ANIO+'-0001');
|
|||||||
assert.deepEqual(r.vista.resumen.etapas.map(e=>[e.pagado,e.saldo]),[[696,0],[104,244],[0,116]]);
|
assert.deepEqual(r.vista.resumen.etapas.map(e=>[e.pagado,e.saldo]),[[696,0],[104,244],[0,116]]);
|
||||||
assert.deepEqual([r.vista.resumen.pagado,r.vista.resumen.saldo],[800,360]);
|
assert.deepEqual([r.vista.resumen.pagado,r.vista.resumen.saldo],[800,360]);
|
||||||
const fp=H('Pagos')[1];
|
const fp=H('Pagos')[1];
|
||||||
|
// La fecha del pago se guarda como texto AAAA-MM-DD (no la convierte Sheets) y así se muestra
|
||||||
|
assert.equal(r.vista.pagos[0].fecha,HOY_ISO,'fecha del pago legible');
|
||||||
|
assert.equal(r.vista.contrato.fechaFirma,HOY_ISO,'fecha de firma legible');
|
||||||
|
assert(/^\d\d\/\d\d\/\d{4} \d\d:\d\d$/.test(r.vista.contrato.fechaAceptacion),'fecha de aceptación legible: '+r.vista.contrato.fechaAceptacion);
|
||||||
assert.deepEqual([fp[0],fp[1],fp[3],fp[4],fp[5],fp[7],fp[8],fp[9],fp[10],fp[11],fp[12],fp[16],fp[18]],
|
assert.deepEqual([fp[0],fp[1],fp[3],fp[4],fp[5],fp[7],fp[8],fp[9],fp[10],fp[11],fp[12],fp[16],fp[18]],
|
||||||
[r.folio,HOY_ISO,p1,800,'MXN',800,'MXN','Transferencia','SPEI 123',1,'Anticipo','tania@helgen.tech','Vigente']);
|
[r.folio,HOY_ISO,p1,800,'MXN',800,'MXN','Transferencia','SPEI 123',1,'Anticipo','tania@helgen.tech','Vigente']);
|
||||||
assert.deepEqual(JSON.parse(fp[13]).map(a=>a.etapa+':'+a.monto),['Anticipo:696','Instalación:104']);
|
assert.deepEqual(JSON.parse(fp[13]).map(a=>a.etapa+':'+a.monto),['Anticipo:696','Instalación:104']);
|
||||||
@ -282,7 +291,7 @@ v=ok('efrain','desbloquearContrato',p1,'Se firmó con la cotización equivocada'
|
|||||||
assert.deepEqual([v.contrato.estado,v.contrato.fechaFirma],['Aceptada','']); assert.equal(v.pagos.length,2,'los pagos se conservan');
|
assert.deepEqual([v.contrato.estado,v.contrato.fechaFirma],['Aceptada','']); assert.equal(v.pagos.length,2,'los pagos se conservan');
|
||||||
const ub=bit().find(b=>b[2]==='Desbloquear contrato');
|
const ub=bit().find(b=>b[2]==='Desbloquear contrato');
|
||||||
assert.deepEqual([ub[1],ub[3],ub[5]],['efrain@helgen.tech',p1,'Se firmó con la cotización equivocada']);
|
assert.deepEqual([ub[1],ub[3],ub[5]],['efrain@helgen.tech',p1,'Se firmó con la cotización equivocada']);
|
||||||
assert(ub[4].includes('tania@helgen.tech')&&ub[4].includes(HOY_ISO)&&/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/.test(ub[0]),'quién, cuándo y qué estaba');
|
assert(ub[4].includes('tania@helgen.tech')&&ub[4].includes('firmado el '+HOY_ISO)&&ub[0] instanceof Date,'quién, cuándo (fecha de Sheets) y qué estaba');
|
||||||
falla('tania','registrarPago',[pago()],/contrato firmado/,undefined);
|
falla('tania','registrarPago',[pago()],/contrato firmado/,undefined);
|
||||||
// Con pagos en MXN no se puede aceptar una cotización en USD (nunca mezclar monedas)
|
// Con pagos en MXN no se puede aceptar una cotización en USD (nunca mezclar monedas)
|
||||||
falla('tania','marcarAceptada',[p1,cU],/pagos en MXN/);
|
falla('tania','marcarAceptada',[p1,cU],/pagos en MXN/);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user