mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Cobranza: resumen de todos los proyectos (cobrado, por cobrar, margen) y CSV
- resumenCobranzaGeneral() (solo ROL_COBRANZA; solo lee): una lectura de Contratos, Esquemas, Pagos, Proyectos y órdenes de compra. Usa resumenCobranza por proyecto: mismas cifras que la vista de cada proyecto. - Totales por moneda (nunca MXN + USD), solo de contratos firmados. Los "Por firmar" se muestran sin sumar; los proyectos cancelados no aparecen. - Por proyecto: contratado, cobrado, saldo, avance, siguiente etapa, último pago, costo de órdenes y margen (costosDeOrdenes, compartida con la vista). - Interfaz: el resumen es la pantalla inicial de Cobranza; filtros, clic para abrir el proyecto, "← Resumen" y Descargar CSV (UTF-8 con BOM, texto con = + - @ neutralizado). - Pruebas: cobranza, compras e interfaz ampliadas; las ocho pasan. Sin publicar (CLAUDE.md §9). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
294cc13bd3
commit
c99e89d91f
24
CLAUDE.md
24
CLAUDE.md
@ -165,6 +165,8 @@ 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)`,
|
||||
`resumenCobranzaGeneral()` (resumen de todos los proyectos; solo lee: una lectura de Contratos, Esquemas, Pagos, Proyectos y
|
||||
órdenes de compra; calcula con `resumenGeneralCobranza`),
|
||||
`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')`,
|
||||
@ -225,16 +227,21 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e
|
||||
- SAT: `palabrasSat`, `SINONIMOS_SAT`, `PALABRAS_SERVICIO_SAT`, `buscarClavesSat(consulta, filas, n)`, `candidatosSat`,
|
||||
`solicitudIaSat`, `leerRespuestaIaSat` (solo acepta claves que estaban entre los candidatos). Modelo: `MODELO_IA_SAT`.
|
||||
- `construirHtmlPdf(d, cfg, logoUrl)` — HTML del PDF (ver reglas 3–4).
|
||||
- Resumen de cobranza: `resumenGeneralCobranza({contratos, esquemas, pagos, proyectos, ordenes})` → `{totales (por moneda), proyectos}`
|
||||
(ver §6.11 "Resumen de cobranza"); usa `resumenCobranza` por proyecto, así las cifras son las mismas que en su vista.
|
||||
- Compras: `normalizarProveedor`, `proveedorId` (RFC válido no genérico o `SIN-RFC:razón social`), `fusionarProveedor` (un dato
|
||||
vacío no borra uno anterior), `proveedoresDesdeFilas` (con `etiqueta` y `fila`), `normalizarOrdenCompra(p, {moneda, ubicacion, hoy,
|
||||
ivaTasa})` (entrega obligatoria y no anterior a hoy; lugar = ubicación del proyecto si viene vacío; condiciones obligatorias; IVA
|
||||
16/8/0 % (`IVA_OC`); renglones con costo capturado; tipo de cambio obligatorio si la moneda no es la del proyecto;
|
||||
`subtotalProyecto` sin IVA en la moneda del proyecto), `margenProyecto(contratado, moneda, costos)` (lanza error si un costo
|
||||
`subtotalProyecto` sin IVA en la moneda del proyecto), `costosDeOrdenes(ordenes, monedaContrato)` (suma `subtotal_proyecto`
|
||||
por moneda; la usan el margen de la vista y el resumen), `margenProyecto(contratado, moneda, costos)` (lanza error si un costo
|
||||
está en otra moneda; `porcentaje` con 1 decimal), `nombreArchivoOc`, `terminosOcParaPdf(cfg)` (si la celda está vacía, los
|
||||
iniciales), `construirHtmlOrdenCompra(d, cfg, logoUrl)` (mismas reglas que el de cotización; sin tipo de cambio ni línea de negocio).
|
||||
|
||||
### 4.7 Interfaz (`web/cotizador/index.html`)
|
||||
Pestañas: **Nueva cotización**, **Historial**, **Catálogo**, **Cobranza** (solo si `permisos.cobranza`; con tarjeta de margen
|
||||
Pestañas: **Nueva cotización**, **Historial**, **Catálogo**, **Cobranza** (solo si `permisos.cobranza`; al entrar muestra el
|
||||
**resumen** `#cobVistaResumen` — `verResumenCob`, `cargarResumenCob`, `filtrarResumenCob`, `pintarResumenCob`, `csvResumenCob`,
|
||||
`descargarCsvCob` —; clic en un renglón → `abrirProyectoCob` muestra `#cobVistaProyecto` con "← Resumen"; tarjeta de margen
|
||||
`#cobMargen`) y **Compras** (solo si `permisos.compras`): `cargarProyectosCom`, `pintarCompras(vista)` (al cambiar de proyecto
|
||||
limpia el formulario), `recibirProveedores`/`autorrellenarProveedor`, `agregarRenglonOc` (el catálogo llena clave, descripción y
|
||||
unidad; el costo queda vacío; listas `#ocCatalogo`/`#ocClaves` sin precio), `recalcularOc`, `pintarTcOc` (tipo de cambio si la
|
||||
@ -469,6 +476,13 @@ Compras: `ROL_COMPRAS` (correos separados por coma: efrain@, tania@, leo@; cread
|
||||
proyecto por más de **1.00** (moneda del proyecto) se rechaza. Si la moneda del pago es otra, tipo de cambio del dí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.
|
||||
- **Resumen de cobranza** (construido 21/10/2026; decisiones del usuario): pantalla inicial de la pestaña Cobranza. Totales por
|
||||
moneda (nunca MXN + USD): contratado con IVA, cobrado, por cobrar, avance, proyectos con saldo y margen sin IVA. Tabla por
|
||||
proyecto: contratado, cobrado, saldo, avance, siguiente etapa por cobrar, último pago, costo de órdenes y margen; firmados del
|
||||
mayor saldo al menor y luego los **"Por firmar"** (aceptada sin firmar: se muestran pero **no suman** a los totales). Los
|
||||
proyectos **Cancelados no aparecen** (no deberían tener contrato; si lo tienen, se excluyen). Filtros (búsqueda, moneda,
|
||||
línea, solo con saldo) solo para la tabla; los totales son de todos. **Descargar CSV** de la tabla filtrada (UTF-8 con BOM
|
||||
para Excel; texto que empiece con `= + - @` con apóstrofo). Solo lee: no escribe en ningún libro.
|
||||
- **USD en cotizaciones:** el catálogo guarda el precio en pesos y la columna I lo convierte a dólares con el tipo de cambio del
|
||||
día (Google Finance, o el manual de la pestaña "Tipo de cambio"; **no es el FIX de Banxico**). En USD la interfaz pone el precio en
|
||||
dólares al elegir un producto; al cambiar de moneda convierte los renglones del catálogo cuyo precio no se tocó (los capturados a
|
||||
@ -535,7 +549,7 @@ node tests/sat.test.js . "$HOME/Library/CloudStorage/GoogleDrive-efrain@helgen.t
|
||||
node tests/sesion.test.js . # token en doPost, vendedor_* (vendedor falso se ignora)
|
||||
node tests/proyectos.test.js . # folio PRY, carpetas, proyecto_folio, subcarpetas que se reparan
|
||||
node tests/interfaz.test.js . # interfaz con DOM y fetch simulados: proyecto rechazado, USD, pestañas Cobranza (con margen) y Compras
|
||||
node tests/cobranza.test.js . # cobranza por doPost: permisos, aceptada, esquema, firma, desbloqueo, pagos, monedas
|
||||
node tests/cobranza.test.js . # cobranza por doPost: permisos, aceptada, esquema, firma, desbloqueo, pagos, monedas, resumen
|
||||
node tests/compras.test.js . # compras por doPost: ROL_COMPRAS, órdenes, PDF, proveedores, recibir/cancelar, margen, monedas
|
||||
node tests/probar-busqueda-sat.js Code.js "<ruta>/SAT_c_ClaveProdServ.csv" # ver qué sugiere la búsqueda por palabras
|
||||
|
||||
@ -613,6 +627,10 @@ El commit y el push no publican nada: el equipo sigue usando la versión anterio
|
||||
**Revisión en el cotizador real (21/10/2026, Claude in Chrome, sin crear nada):** sesión sin errores, pestañas Cobranza y
|
||||
Compras visibles con los dos roles, Compras sin proyectos ("No hay proyectos activos con contrato firmado": en la hoja real
|
||||
ningún contrato está firmado), vista de cobranza de `PRY-2026-0001` (Cancelado) carga con `margen: null`. Libro de compras vacío.
|
||||
- **Resumen de cobranza: construido el 21/10/2026, SIN PUBLICAR** (código y pruebas en Node; revisado en local con datos de
|
||||
ejemplo). Falta: prueba en PRUEBAS (`PRY-2026-0003`/`0004` tienen pagos y órdenes; tú sin cobranza = `pruebasComprasSinCobranza()`),
|
||||
`clasp push` + comprobar, `clasp deploy` (@9), `wrangler deploy` y revisión en el cotizador real. Solo lee: no requiere respaldo
|
||||
ni permisos nuevos de Google.
|
||||
- Tania y Leo (rol de compras) no se han probado con sus cuentas reales; la primera orden real será también la primera prueba de
|
||||
PDF en la carpeta real.
|
||||
- Subcarpetas "Órdenes de compra" de los proyectos viejos: quedan vacías y sin uso. Si el usuario lo pide, comprobar con el
|
||||
|
||||
128
Code.js
128
Code.js
@ -98,6 +98,7 @@ const API_FUNCIONES = {
|
||||
// Cobranza (solo ROL_COBRANZA; cada función lo revisa)
|
||||
listarProyectosCobranza: listarProyectosCobranza,
|
||||
getCobranzaProyecto: getCobranzaProyecto,
|
||||
resumenCobranzaGeneral: resumenCobranzaGeneral,
|
||||
marcarAceptada: marcarAceptada,
|
||||
guardarEsquema: guardarEsquema,
|
||||
firmarContrato: firmarContrato,
|
||||
@ -1337,16 +1338,7 @@ function vistaCobranza_(libro, proyecto) {
|
||||
function margenDeContrato_(contrato) {
|
||||
try {
|
||||
const ordenes = ordenesDeProyecto_(libroComprasSiExiste_(), contrato.proyectoFolio).filter(o => o.estado !== OC_CANCELADA);
|
||||
const porMoneda = {};
|
||||
ordenes.forEach(o => {
|
||||
const m = o.monedaProyecto || contrato.moneda;
|
||||
const x = porMoneda[m] || (porMoneda[m] = { concepto: 'Órdenes de compra', moneda: m, monto: 0, cantidad: 0 });
|
||||
x.monto = (aCentavos(x.monto) + aCentavos(o.subtotalProyecto)) / 100;
|
||||
x.cantidad++;
|
||||
});
|
||||
const costos = Object.keys(porMoneda).map(m => porMoneda[m]);
|
||||
if (!costos.length) costos.push({ concepto: 'Órdenes de compra', moneda: contrato.moneda, monto: 0, cantidad: 0 });
|
||||
return margenProyecto(contrato.subtotal, contrato.moneda, costos);
|
||||
return margenProyecto(contrato.subtotal, contrato.moneda, costosDeOrdenes(ordenes, contrato.moneda));
|
||||
} catch (e) {
|
||||
return { error: 'No se pudo calcular el margen: ' + e.message };
|
||||
}
|
||||
@ -1378,6 +1370,30 @@ function getCobranzaProyecto(folio) {
|
||||
return vistaCobranza_(libro, proyectoParaCobranza_(folio));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen de cobranza de todos los proyectos con contrato (solo rol de cobranza; solo lee). Lee una vez Contratos, Esquemas,
|
||||
* Pagos, Proyectos y las órdenes de compra, y calcula con resumenGeneralCobranza (las mismas cifras que la vista de cada proyecto).
|
||||
*/
|
||||
function resumenCobranzaGeneral() {
|
||||
exigirCobranza_();
|
||||
const libro = libroCobranza_();
|
||||
const contratos = filasCobranza_(hojaCobranza_(libro, 'Contratos'), 'Contratos')
|
||||
.map((r, i) => contratoDesdeFila_(r, i + 2)).filter(c => c.proyectoFolio);
|
||||
const esquemas = filasCobranza_(hojaCobranza_(libro, 'Esquemas'), 'Esquemas')
|
||||
.map(r => ({ proyectoFolio: String(r[0]), orden: Number(r[1]), etapa: String(r[2]), porcentaje: Number(r[3]) }));
|
||||
const pagos = filasCobranza_(hojaCobranza_(libro, 'Pagos'), 'Pagos').map((r, i) => pagoDesdeFila_(r, i + 2));
|
||||
const hoja = hojaProyectos_();
|
||||
const proyectos = proyectosDesdeFilas(hoja.getLastRow() > 1
|
||||
? hoja.getRange(2, 1, hoja.getLastRow() - 1, ENCABEZADOS_PROYECTOS.length).getValues() : []);
|
||||
const libroCompras = libroComprasSiExiste_();
|
||||
const ordenes = libroCompras
|
||||
? filasDeArea_(hojaCompras_(libroCompras, 'OrdenesCompra'), HOJAS_COMPRAS, 'OrdenesCompra')
|
||||
.filter(r => String(r[0] || '').trim()).map((r, i) => ordenDesdeFila_(r, i + 2))
|
||||
: [];
|
||||
return Object.assign(resumenGeneralCobranza({ contratos: contratos, esquemas: esquemas, pagos: pagos, proyectos: proyectos,
|
||||
ordenes: ordenes }), { hoy: hoyIso_() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca la cotización aceptada del proyecto (una sola; se puede cambiar hasta firmar). Copia sus montos, marca y
|
||||
* moneda al contrato. Si la nueva es de Helgen Energy y el esquema actual no es el 60/30/10, pide confirmar
|
||||
@ -2447,6 +2463,98 @@ function margenProyecto(contratado, moneda, costos) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Costos de órdenes de compra para margenProyecto: suma de subtotal_proyecto (sin IVA) de las órdenes recibidas (el que llama
|
||||
* quita las canceladas), agrupada por moneda del proyecto. Sin órdenes: un costo en 0 en la moneda del contrato.
|
||||
*/
|
||||
function costosDeOrdenes(ordenes, monedaContrato) {
|
||||
const porMoneda = {};
|
||||
(ordenes || []).forEach(o => {
|
||||
const m = o.monedaProyecto || monedaContrato;
|
||||
const x = porMoneda[m] || (porMoneda[m] = { concepto: 'Órdenes de compra', moneda: m, monto: 0, cantidad: 0 });
|
||||
x.monto = (aCentavos(x.monto) + aCentavos(o.subtotalProyecto)) / 100;
|
||||
x.cantidad++;
|
||||
});
|
||||
const costos = Object.keys(porMoneda).map(m => porMoneda[m]);
|
||||
return costos.length ? costos : [{ concepto: 'Órdenes de compra', moneda: monedaContrato, monto: 0, cantidad: 0 }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen de cobranza de todos los proyectos. d = {contratos, esquemas: [{proyectoFolio, orden, etapa, porcentaje}], pagos,
|
||||
* proyectos, ordenes}. Los proyectos Cancelados no aparecen. Un renglón por contrato: "Firmado" o "Por firmar" (aceptada sin
|
||||
* firmar: se muestra pero no suma a los totales). Pagado y saldo salen de resumenCobranza (lo mismo que la vista del proyecto);
|
||||
* margen = contratado sin IVA − órdenes de compra no canceladas. Totales por moneda, solo de contratos firmados: nunca se suman
|
||||
* MXN y USD. Renglones: firmados primero, del mayor saldo al menor; luego los por firmar.
|
||||
*/
|
||||
function resumenGeneralCobranza(d) {
|
||||
const may = s => String(s || '').trim().toUpperCase();
|
||||
const proyectos = {};
|
||||
(d.proyectos || []).forEach(p => { proyectos[may(p.folio)] = p; });
|
||||
const agrupar = (lista, campo) => {
|
||||
const m = {};
|
||||
(lista || []).forEach(x => { const k = may(x[campo]); (m[k] = m[k] || []).push(x); });
|
||||
return m;
|
||||
};
|
||||
const esquemas = agrupar(d.esquemas, 'proyectoFolio'), pagos = agrupar(d.pagos, 'proyectoFolio');
|
||||
const ordenes = agrupar(d.ordenes, 'proyectoFolio');
|
||||
|
||||
const renglones = [];
|
||||
(d.contratos || []).forEach(c => {
|
||||
const folio = may(c.proyectoFolio);
|
||||
const p = proyectos[folio];
|
||||
if (!p || p.estado === 'Cancelado') return;
|
||||
let etapas = (esquemas[folio] || []).slice().sort((a, b) => a.orden - b.orden)
|
||||
.map(e => ({ etapa: e.etapa, porcentaje: e.porcentaje }));
|
||||
if (!etapas.length) etapas = [{ etapa: 'Total', porcentaje: 100 }];
|
||||
const pagosP = pagos[folio] || [];
|
||||
const r = resumenCobranza(c, etapas, pagosP);
|
||||
const siguiente = r.etapas.find(e => aCentavos(e.saldo) > 0);
|
||||
const ultimoPago = pagosP.filter(x => x.estado === 'Vigente').map(x => String(x.fecha || '')).sort().pop() || '';
|
||||
const ocs = (ordenes[folio] || []).filter(o => o.estado !== OC_CANCELADA);
|
||||
let margen = null, margenError = '';
|
||||
try {
|
||||
margen = margenProyecto(c.subtotal, c.moneda, costosDeOrdenes(ocs, c.moneda));
|
||||
} catch (e) {
|
||||
margenError = e.message;
|
||||
}
|
||||
renglones.push({
|
||||
folio: p.folio, nombre: p.nombre, cliente: p.cliente, estadoProyecto: p.estado, marca: c.marca, moneda: c.moneda,
|
||||
estadoContrato: c.estado === ESTADO_FIRMADO ? 'Firmado' : 'Por firmar', cotizacion: c.cotizacion, fechaFirma: c.fechaFirma,
|
||||
contratado: r.contratado, subtotal: r.subtotal, pagado: r.pagado, saldo: r.saldo, avance: r.avance,
|
||||
siguienteEtapa: siguiente ? siguiente.etapa : '', siguienteSaldo: siguiente ? siguiente.saldo : 0, ultimoPago: ultimoPago,
|
||||
ordenes: ocs.length, costoOc: margen ? margen.totalCostos : null, margen: margen ? margen.margen : null,
|
||||
margenPorcentaje: margen ? margen.porcentaje : null, margenError: margenError,
|
||||
});
|
||||
});
|
||||
renglones.sort((a, b) => (a.estadoContrato === b.estadoContrato ? 0 : a.estadoContrato === 'Firmado' ? -1 : 1) ||
|
||||
b.saldo - a.saldo || (a.folio < b.folio ? -1 : 1));
|
||||
|
||||
// Totales por moneda (en centavos para no acumular errores de redondeo)
|
||||
const tot = {};
|
||||
renglones.forEach(x => {
|
||||
const t = tot[x.moneda] || (tot[x.moneda] = { moneda: x.moneda, proyectos: 0, conSaldo: 0, contratado: 0, cobrado: 0,
|
||||
porCobrar: 0, contratadoSinIva: 0, costoOc: 0, porFirmar: 0, montoPorFirmar: 0, margenError: false });
|
||||
if (x.estadoContrato !== 'Firmado') { t.porFirmar++; t.montoPorFirmar += aCentavos(x.contratado); return; }
|
||||
t.proyectos++;
|
||||
if (aCentavos(x.saldo) > 0) t.conSaldo++;
|
||||
t.contratado += aCentavos(x.contratado); t.cobrado += aCentavos(x.pagado); t.porCobrar += aCentavos(x.saldo);
|
||||
t.contratadoSinIva += aCentavos(x.subtotal);
|
||||
if (x.margenError) t.margenError = true; else t.costoOc += aCentavos(x.costoOc);
|
||||
});
|
||||
const totales = Object.keys(tot).sort().map(k => {
|
||||
const t = tot[k];
|
||||
const margenCts = t.contratadoSinIva - t.costoOc;
|
||||
return {
|
||||
moneda: t.moneda, proyectos: t.proyectos, conSaldo: t.conSaldo, contratado: t.contratado / 100, cobrado: t.cobrado / 100,
|
||||
porCobrar: t.porCobrar / 100, avance: t.contratado > 0 ? Math.min(100, Math.round(t.cobrado / t.contratado * 1000) / 10) : 0,
|
||||
contratadoSinIva: t.contratadoSinIva / 100, costoOc: t.costoOc / 100, margen: t.margenError ? null : margenCts / 100,
|
||||
margenPorcentaje: t.margenError || t.contratadoSinIva <= 0 ? null : Math.round(margenCts / t.contratadoSinIva * 1000) / 10,
|
||||
porFirmar: t.porFirmar, montoPorFirmar: t.montoPorFirmar / 100,
|
||||
};
|
||||
});
|
||||
return { totales: totales, proyectos: renglones };
|
||||
}
|
||||
|
||||
/** Nombre del PDF de una orden: "OC-2026-0001 — <proveedor>.pdf", sin caracteres raros. */
|
||||
function nombreArchivoOc(folio, proveedor) {
|
||||
const limpio = s => String(s || '').replace(/[\/\\:*?"<>|\u0000-\u001f]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
|
||||
@ -351,4 +351,38 @@ assert.deepEqual([v.proyecto.estado,v.puedeAceptar,v.puedeFirmar],['Cancelado',f
|
||||
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');
|
||||
|
||||
// --- Resumen de cobranza (todos los proyectos): solo rol de cobranza; mismas cifras que la vista de cada proyecto ---
|
||||
falla('ana','resumenCobranzaGeneral',[],/No tienes permiso de cobranza/,'PERMISO');
|
||||
falla('intruso','resumenCobranzaGeneral',[],/@helgen.tech/,'SESION');
|
||||
let rs=ok('tania','resumenCobranzaGeneral');
|
||||
assert.deepEqual(rs.proyectos.map(x=>x.folio+':'+x.estadoContrato),[p1+':Firmado',p2+':Firmado',p3+':Por firmar'],'firmados por saldo, luego por firmar');
|
||||
const r1=rs.proyectos[0], v1=ok('tania','getCobranzaProyecto',p1);
|
||||
assert.deepEqual([r1.contratado,r1.pagado,r1.saldo,r1.avance],[v1.resumen.contratado,v1.resumen.pagado,v1.resumen.saldo,v1.resumen.avance],'igual que la vista');
|
||||
assert.deepEqual([r1.contratado,r1.pagado,r1.saldo,r1.avance,r1.siguienteEtapa,r1.siguienteSaldo,r1.ultimoPago,r1.moneda],
|
||||
[1160,800,360,69,'Instalación',244,HOY_ISO,'MXN'],'el pago cancelado no cuenta');
|
||||
assert.deepEqual([r1.costoOc,r1.margen,r1.margenPorcentaje,r1.ordenes],[0,1000,100,0],'sin órdenes: margen = contratado sin IVA');
|
||||
const r2=rs.proyectos[1];
|
||||
assert.deepEqual([r2.moneda,r2.contratado,r2.pagado,r2.saldo,r2.siguienteEtapa,r2.margen],['USD',1000,1000,0,'',862.07],'pago en MXN convertido a USD');
|
||||
assert.deepEqual([rs.proyectos[2].contratado,rs.proyectos[2].pagado],[116,0]);
|
||||
const tMXN=rs.totales.find(t=>t.moneda==='MXN'), tUSD=rs.totales.find(t=>t.moneda==='USD');
|
||||
assert.deepEqual([tMXN.proyectos,tMXN.conSaldo,tMXN.contratado,tMXN.cobrado,tMXN.porCobrar,tMXN.avance,tMXN.margen,tMXN.margenPorcentaje,tMXN.porFirmar,tMXN.montoPorFirmar],
|
||||
[1,1,1160,800,360,69,1000,100,1,116],'MXN: el por firmar no suma');
|
||||
assert.deepEqual([tUSD.proyectos,tUSD.conSaldo,tUSD.contratado,tUSD.cobrado,tUSD.porCobrar,tUSD.avance],[1,0,1000,1000,0,100],'USD aparte: nunca se suma con MXN');
|
||||
assert.deepEqual(rs.totales.map(t=>t.moneda),['MXN','USD']);
|
||||
// Proyecto cancelado: no aparece ni suma
|
||||
db.pest.Proyectos.m[filaP3][6]='Cancelado';
|
||||
rs=ok('tania','resumenCobranzaGeneral');
|
||||
assert(!rs.proyectos.some(x=>x.folio===p3)); assert.equal(rs.totales.find(t=>t.moneda==='MXN').porFirmar,0);
|
||||
db.pest.Proyectos.m[filaP3][6]='Activo';
|
||||
// Solo lee: no escribe en ningún libro
|
||||
const antes=JSON.stringify(Object.values(libros).map(L=>Object.keys(L.pest).map(k=>L.pest[k].m.length)));
|
||||
ok('tania','resumenCobranzaGeneral');
|
||||
assert.equal(JSON.stringify(Object.values(libros).map(L=>Object.keys(L.pest).map(k=>L.pest[k].m.length))),antes,'el resumen no escribe');
|
||||
// Lógica pura: sin esquema (etapa única), orden en otra moneda → margen a revisar (y totales sin margen)
|
||||
const pu=P.resumenGeneralCobranza({contratos:[{proyectoFolio:'PRY-1',estado:'Firmado',moneda:'MXN',marca:'helgen',total:116,subtotal:100}],
|
||||
esquemas:[],pagos:[],proyectos:[{folio:'PRY-1',estado:'Cerrado',nombre:'N',cliente:'C'}],
|
||||
ordenes:[{proyectoFolio:'PRY-1',estado:'Emitida',monedaProyecto:'USD',subtotalProyecto:5}]});
|
||||
assert.deepEqual([pu.proyectos[0].siguienteEtapa,pu.proyectos[0].saldo,pu.proyectos[0].margen,!!pu.proyectos[0].margenError,pu.totales[0].margen],
|
||||
['Total',116,null,true,null],'cerrado sí aparece; margen con monedas mezcladas se marca');
|
||||
console.log('OK cobranza');
|
||||
|
||||
@ -271,6 +271,17 @@ v=ok('tania','getCobranzaProyecto',p2);
|
||||
assert.deepEqual([v.margen.moneda,v.margen.contratado,v.margen.totalCostos,v.margen.margen,v.margen.porcentaje],['USD',5000,100,4900,98]);
|
||||
v=ok('tania','getCobranzaProyecto',p3); assert.deepEqual([v.margen.totalCostos,v.margen.margen],[0,100],'sin órdenes');
|
||||
|
||||
// --- Resumen de cobranza: costo de órdenes y margen por proyecto (las mismas cifras que la vista) ---
|
||||
const rs=ok('tania','resumenCobranzaGeneral');
|
||||
const rp1=rs.proyectos.find(x=>x.folio===p1), rp2=rs.proyectos.find(x=>x.folio===p2);
|
||||
assert.deepEqual([rp1.ordenes,rp1.costoOc,rp1.margen,rp1.margenPorcentaje],[2,3850,6150,61.5],'la cancelada no cuenta');
|
||||
assert.deepEqual([rp2.moneda,rp2.costoOc,rp2.margen,rp2.margenPorcentaje],['USD',100,4900,98]);
|
||||
assert(!rs.proyectos.some(x=>x.folio===p4),'proyecto cancelado fuera del resumen');
|
||||
assert.deepEqual(rs.proyectos.filter(x=>x.estadoContrato==='Por firmar').map(x=>x.folio),[p3]);
|
||||
const tm=rs.totales.find(t=>t.moneda==='MXN');
|
||||
assert.deepEqual([tm.contratadoSinIva,tm.costoOc,tm.margen,tm.margenPorcentaje],[10000,3850,6150,61.5],'el por firmar no suma al margen');
|
||||
falla('pedro','resumenCobranzaGeneral',[],/No tienes permiso de cobranza/,'PERMISO');
|
||||
|
||||
// --- Con órdenes vigentes en MXN, la aceptada no puede cambiar a USD (tras desbloquear) ---
|
||||
ok('efrain','desbloquearContrato',p1,'Prueba de moneda con órdenes');
|
||||
falla('efrain','marcarAceptada',[p1,c1usd],/órdenes de compra en MXN/);
|
||||
|
||||
@ -137,7 +137,47 @@ const esperar=async(n=10)=>{for(let i=0;i<n;i++) await new Promise(r=>setImmedia
|
||||
assert(!B.$('tabCob').classList.contains('oculto'),'con permiso ve la pestaña');
|
||||
assert(B.$('tabCom').classList.contains('oculto'),'cobranza sin compras no ve Compras');
|
||||
B.respuestas.listarProyectosCobranza={ok:true,datos:[{...PRY,contrato:''}]};
|
||||
// Resumen de todos los proyectos: aparece al entrar a la pestaña
|
||||
const RES={hoy:'2026-10-21',totales:[{moneda:'MXN',proyectos:2,conSaldo:1,contratado:11600,cobrado:5000,porCobrar:6600,avance:43.1,
|
||||
contratadoSinIva:10000,costoOc:4850,margen:5150,margenPorcentaje:51.5,porFirmar:1,montoPorFirmar:116},
|
||||
{moneda:'USD',proyectos:1,conSaldo:0,contratado:1000,cobrado:1000,porCobrar:0,avance:100,contratadoSinIva:862.07,costoOc:1200,margen:-337.93,margenPorcentaje:-39.2,porFirmar:0,montoPorFirmar:0}],
|
||||
proyectos:[{folio:'PRY-2026-0003',nombre:'Techo',cliente:'=Luis, "SA"',estadoProyecto:'Activo',marca:'helgen',moneda:'MXN',estadoContrato:'Firmado',
|
||||
contratado:11600,subtotal:10000,pagado:5000,saldo:6600,avance:43.1,siguienteEtapa:'Instalación',siguienteSaldo:3480,ultimoPago:'2026-10-20',ordenes:2,costoOc:4850,margen:5150,margenPorcentaje:51.5,margenError:''},
|
||||
{folio:'PRY-2026-0004',nombre:'Texas',cliente:'Solar Corp',estadoProyecto:'Cerrado',marca:'energy',moneda:'USD',estadoContrato:'Firmado',
|
||||
contratado:1000,subtotal:862.07,pagado:1000,saldo:0,avance:100,siguienteEtapa:'',siguienteSaldo:0,ultimoPago:'2026-10-19',ordenes:1,costoOc:1200,margen:-337.93,margenPorcentaje:-39.2,margenError:''},
|
||||
{folio:'PRY-2026-0005',nombre:'Nuevo',cliente:'Ana',estadoProyecto:'Activo',marca:'helgen',moneda:'MXN',estadoContrato:'Por firmar',
|
||||
contratado:116,subtotal:100,pagado:0,saldo:116,avance:0,siguienteEtapa:'Anticipo',siguienteSaldo:69.6,ultimoPago:'',ordenes:0,costoOc:null,margen:null,margenPorcentaje:null,margenError:'hay costos en USD'}]};
|
||||
B.respuestas.resumenCobranzaGeneral={ok:true,datos:RES};
|
||||
B.run("verTab('cob')"); await esperar();
|
||||
assert(!B.$('cobVistaResumen').classList.contains('oculto')&&B.$('cobVistaProyecto').classList.contains('oculto'),'al entrar se ve el resumen');
|
||||
const tt=B.$('resTotales').innerHTML;
|
||||
assert(/MXN · 2 contratos firmados · 1 por firmar \(\$116\.00 MXN, no suman\)/.test(tt)&&/Por cobrar<\/span><b>\$6,600\.00 MXN/.test(tt)&&/1 de 2/.test(tt),tt);
|
||||
assert(/Margen \(sin IVA\)<\/span><b>\$5,150\.00 MXN · 51\.5%/.test(tt)&&/class="negativo">-\$337\.93 USD · -39\.2%/.test(tt),'margen por moneda');
|
||||
let rl=B.$('resLista').innerHTML;
|
||||
assert.deepEqual((rl.match(/data-folio="([^"]+)"/g)||[]).length,3);
|
||||
assert(/chip aceptada" style="white-space:nowrap">Por firmar/.test(rl)&&/Instalación · \$3,480\.00 MXN/.test(rl)&&/Último pago 20\/10\/2026/.test(rl)&&/>Pagado</.test(rl)&&/Sin pagos/.test(rl)&&/· Cerrado/.test(rl)&&/· Helgen Energy/.test(rl),rl);
|
||||
assert(/aviso-moneda" title="hay costos en USD">Revisar/.test(rl)&&/aviso-moneda">-\$337\.93 USD/.test(rl));
|
||||
// Filtros (solo la tabla)
|
||||
B.$('resMoneda').value='USD'; B.run('pintarResumenCob()'); assert.deepEqual(B.$('resLista').innerHTML.match(/data-folio="([^"]+)"/g),['data-folio="PRY-2026-0004"']);
|
||||
B.$('resMoneda').value=''; B.$('resSaldo').checked=true; B.run('pintarResumenCob()');
|
||||
assert.deepEqual(B.$('resLista').innerHTML.match(/data-folio="([^"]+)"/g),['data-folio="PRY-2026-0003"'],'solo con saldo: firmados con saldo');
|
||||
B.$('resSaldo').checked=false; B.$('resBuscar').value='ana'; B.run('pintarResumenCob()');
|
||||
assert.deepEqual(B.$('resLista').innerHTML.match(/data-folio="([^"]+)"/g),['data-folio="PRY-2026-0005"']);
|
||||
B.$('resBuscar').value='zzz'; B.run('pintarResumenCob()'); assert(/Ningún proyecto coincide/.test(B.$('resLista').innerHTML));
|
||||
B.$('resBuscar').value='';
|
||||
// CSV: encabezados, números con punto, texto entrecomillado, fórmulas neutralizadas
|
||||
const csv=B.run('csvResumenCob(RESUMEN.proyectos)').split('\r\n');
|
||||
assert.equal(csv[0].charCodeAt(0),0xFEFF,'BOM para Excel'); assert(csv[0].includes('"Contratado con IVA","Cobrado","Saldo"'));
|
||||
assert.equal(csv[1],'"PRY-2026-0003","Techo","\'=Luis, ""SA""","Helgen","Firmado","MXN",11600.00,5000.00,6600.00,43.1,"Instalación",3480.00,"2026-10-20",2,4850.00,5150.00,51.5');
|
||||
assert(csv[3].endsWith(',0,,,'),'sin margen: celdas vacías'); assert.equal(csv.length,4);
|
||||
// Clic en un renglón abre el proyecto; "← Resumen" regresa
|
||||
B.respuestas.getCobranzaProyecto={ok:true,datos:{proyecto:PRY,cotizaciones:[],contrato:null,esquema:[],resumen:null,pagos:[],puedeAceptar:true,puedeFirmar:false,metodos:[],hoy:'2026-10-20'}};
|
||||
B.run("abrirProyectoCob('PRY-2026-0001')"); await esperar();
|
||||
assert(B.$('cobVistaResumen').classList.contains('oculto')&&!B.$('cobVistaProyecto').classList.contains('oculto'),'se abre el proyecto');
|
||||
const nRes=B.llamadas.filter(c=>c.funcion==='resumenCobranzaGeneral').length;
|
||||
B.run('verResumenCob()'); await esperar();
|
||||
assert(!B.$('cobVistaResumen').classList.contains('oculto')&&B.$('cobVistaProyecto').classList.contains('oculto'));
|
||||
assert.equal(B.llamadas.filter(c=>c.funcion==='resumenCobranzaGeneral').length,nRes+1,'al regresar se actualiza el resumen');
|
||||
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'}];
|
||||
|
||||
@ -274,13 +274,49 @@
|
||||
|
||||
<!-- ===== COBRANZA (solo rol de cobranza; el servidor lo vuelve a revisar en cada llamada) ===== -->
|
||||
<section id="vistaCob" class="oculto">
|
||||
<!-- Resumen de todos los proyectos (al entrar a la pestaña); clic en un renglón abre el proyecto -->
|
||||
<div id="cobVistaResumen">
|
||||
<div class="modulo">
|
||||
<h2>1 · Resumen de cobranza</h2>
|
||||
<div id="resTotales" class="nota">Cargando…</div>
|
||||
<div class="acciones">
|
||||
<input id="resBuscar" placeholder="Buscar por folio, cliente o nombre…" oninput="pintarResumenCob()" style="flex:1; min-width:220px">
|
||||
<select id="resMoneda" onchange="pintarResumenCob()" style="width:auto">
|
||||
<option value="">Todas las monedas</option><option value="MXN">MXN</option><option value="USD">USD</option>
|
||||
</select>
|
||||
<select id="resMarca" onchange="pintarResumenCob()" style="width:auto">
|
||||
<option value="">Todas las líneas</option><option value="helgen">Helgen</option><option value="energy">Helgen Energy</option>
|
||||
</select>
|
||||
<label style="display:flex; align-items:center; gap:6px; margin:0; white-space:nowrap">
|
||||
<input type="checkbox" id="resSaldo" onchange="pintarResumenCob()" style="width:auto"> Solo con saldo</label>
|
||||
<button type="button" class="btn btn-sec" onclick="cargarResumenCob()">Actualizar</button>
|
||||
<button type="button" class="btn btn-sec" onclick="descargarCsvCob()">Descargar CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modulo">
|
||||
<h2>2 · Proyectos</h2>
|
||||
<div style="overflow-x:auto">
|
||||
<table style="font-size:12px">
|
||||
<thead><tr><th>Proyecto</th><th>Contrato</th><th class="num">Contratado</th><th class="num">Cobrado</th>
|
||||
<th class="num">Saldo</th><th style="width:90px">Avance</th><th>Siguiente etapa · último pago</th>
|
||||
<th class="num">Costo OC</th><th class="num">Margen</th></tr></thead>
|
||||
<tbody id="resLista"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="nota" style="margin-top:8px">Haz clic en un proyecto para ver su contrato, esquema y pagos. Contratado, cobrado y saldo
|
||||
con IVA; costo de órdenes de compra y margen sin IVA. Los contratos "Por firmar" no suman a los totales; los proyectos
|
||||
cancelados no aparecen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cobVistaProyecto" class="oculto">
|
||||
<div class="modulo">
|
||||
<h2>1 · Proyecto y contrato</h2>
|
||||
<div class="proy-fila">
|
||||
<div><label>Proyecto</label>
|
||||
<input id="cobProyecto" list="cobProyectosList" autocomplete="off" placeholder="Busca por folio, cliente o nombre">
|
||||
<datalist id="cobProyectosList"></datalist></div>
|
||||
<button type="button" class="btn btn-sec" onclick="cargarProyectosCob()">Actualizar lista</button>
|
||||
<button type="button" class="btn btn-sec" onclick="verResumenCob()">← Resumen</button>
|
||||
</div>
|
||||
<div id="cobInfo" class="nota" style="margin-top:6px">Elige un proyecto para ver su contrato, esquema y pagos.</div>
|
||||
<div id="cobDetalle" class="oculto">
|
||||
@ -331,6 +367,7 @@
|
||||
<tbody id="cobPagos"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== COMPRAS (solo rol de compras; el servidor lo vuelve a revisar en cada llamada) ===== -->
|
||||
@ -975,10 +1012,86 @@
|
||||
$('cobProyecto').addEventListener('change', () => { const p = proyectoCobElegido(); if (p) abrirProyectoCob(p.folio); });
|
||||
|
||||
function abrirProyectoCob(folio) {
|
||||
$('cobVistaResumen').classList.add('oculto'); $('cobVistaProyecto').classList.remove('oculto');
|
||||
$('cobInfo').textContent = 'Cargando…';
|
||||
google.script.run.withSuccessHandler(pintarCobranza).withFailureHandler(mostrarError).getCobranzaProyecto(folio);
|
||||
}
|
||||
|
||||
// ---- Resumen de cobranza (todos los proyectos; lo calcula el servidor con las mismas cifras que cada proyecto) ----
|
||||
let RESUMEN = null;
|
||||
|
||||
function verResumenCob() {
|
||||
$('cobVistaProyecto').classList.add('oculto'); $('cobVistaResumen').classList.remove('oculto');
|
||||
cargarResumenCob();
|
||||
}
|
||||
|
||||
function cargarResumenCob() {
|
||||
if (!RESUMEN) $('resTotales').textContent = 'Cargando…';
|
||||
google.script.run.withSuccessHandler(r => { RESUMEN = r; pintarResumenCob(); })
|
||||
.withFailureHandler(mostrarError).resumenCobranzaGeneral();
|
||||
}
|
||||
|
||||
/** Renglones del resumen que pasan los filtros (búsqueda, moneda, línea, solo con saldo). */
|
||||
function filtrarResumenCob() {
|
||||
const q = $('resBuscar').value.trim().toLowerCase(), m = $('resMoneda').value, l = $('resMarca').value, conSaldo = $('resSaldo').checked;
|
||||
return ((RESUMEN && RESUMEN.proyectos) || []).filter(p => (!q || (p.folio + ' ' + p.cliente + ' ' + p.nombre).toLowerCase().includes(q)) &&
|
||||
(!m || p.moneda === m) && (!l || p.marca === l) && (!conSaldo || (p.estadoContrato === 'Firmado' && p.saldo > 0.004)));
|
||||
}
|
||||
|
||||
function pintarResumenCob() {
|
||||
if (!RESUMEN) return;
|
||||
const tile = (k, x, clase) => '<div class="tile"><span>' + esc(k) + '</span><b' + (clase ? ' class="' + clase + '"' : '') + '>' + x + '</b></div>';
|
||||
// Totales: uno por moneda, solo contratos firmados (los filtros aplican a la tabla, no a los totales)
|
||||
$('resTotales').innerHTML = RESUMEN.totales.length ? RESUMEN.totales.map(t =>
|
||||
'<div class="sat-titulo" style="margin-top:4px">' + esc(t.moneda) + ' · ' + t.proyectos + (t.proyectos === 1 ? ' contrato firmado' : ' contratos firmados') +
|
||||
(t.porFirmar ? ' · ' + t.porFirmar + ' por firmar (' + dinero(t.montoPorFirmar, t.moneda) + ', no suman)' : '') + '</div>' +
|
||||
'<div class="tiles">' + tile('Contratado (con IVA)', dinero(t.contratado, t.moneda)) + tile('Cobrado', dinero(t.cobrado, t.moneda)) +
|
||||
tile('Por cobrar', dinero(t.porCobrar, t.moneda)) + tile('Avance', t.avance + '%') +
|
||||
tile('Proyectos con saldo', t.conSaldo + ' de ' + t.proyectos) +
|
||||
tile('Margen (sin IVA)', t.margen == null ? '—' : dinero(t.margen, t.moneda) + (t.margenPorcentaje == null ? '' : ' · ' + t.margenPorcentaje + '%'),
|
||||
t.margen != null && t.margen < 0 ? 'negativo' : '') + '</div>').join('')
|
||||
: '<span class="nota">Todavía no hay proyectos con cotización aceptada.</span>';
|
||||
const lista = filtrarResumenCob();
|
||||
$('resLista').innerHTML = lista.length ? lista.map(p => {
|
||||
const firmado = p.estadoContrato === 'Firmado';
|
||||
return '<tr class="fila-cat" data-folio="' + esc(p.folio) + '"><td style="min-width:190px"><b style="white-space:nowrap">' + esc(p.folio) + '</b>' +
|
||||
' <span class="nota">· ' + esc(MARCA_TXT[p.marca] || '') + '</span><div class="nota">' + esc(p.cliente) + ' · ' + esc(p.nombre) +
|
||||
(p.estadoProyecto !== 'Activo' ? ' · ' + esc(p.estadoProyecto) : '') + '</div></td><td>' +
|
||||
'<span class="chip ' + (firmado ? 'firmado' : 'aceptada') + '" style="white-space:nowrap">' + esc(p.estadoContrato) + '</span></td>' +
|
||||
'<td class="num">' + dinero(p.contratado, p.moneda) + '</td><td class="num">' + dinero(p.pagado, p.moneda) + '</td>' +
|
||||
'<td class="num"><b>' + dinero(p.saldo, p.moneda) + '</b></td>' +
|
||||
'<td><div class="barra" title="' + p.avance + '%"><i style="width:' + Math.max(0, Math.min(100, p.avance)) + '%"></i></div><span class="nota">' + p.avance + '%</span></td>' +
|
||||
'<td>' + (p.siguienteEtapa ? esc(p.siguienteEtapa) + ' · ' + dinero(p.siguienteSaldo, p.moneda) : 'Pagado') +
|
||||
'<div class="nota">' + (p.ultimoPago ? 'Último pago ' + esc(fechaMx(p.ultimoPago)) : 'Sin pagos') + '</div></td>' +
|
||||
'<td class="num">' + (p.costoOc == null ? '—' : dinero(p.costoOc, p.moneda) + '<div class="nota">' + p.ordenes + (p.ordenes === 1 ? ' orden' : ' órdenes') + '</div>') + '</td>' +
|
||||
'<td class="num">' + (p.margen == null ? '<span class="nota aviso-moneda" title="' + esc(p.margenError) + '">Revisar</span>'
|
||||
: '<b' + (p.margen < 0 ? ' class="aviso-moneda"' : '') + '>' + dinero(p.margen, p.moneda) + '</b><div class="nota">' + p.margenPorcentaje + '%</div>') + '</td></tr>';
|
||||
}).join('') : '<tr><td colspan="9" class="nota">' + ((RESUMEN.proyectos || []).length ? 'Ningún proyecto coincide con los filtros.' : 'Sin proyectos con contrato.') + '</td></tr>';
|
||||
$('resLista').querySelectorAll('tr[data-folio]').forEach(tr => tr.onclick = () => abrirProyectoCob(tr.dataset.folio));
|
||||
}
|
||||
|
||||
/** CSV de los renglones filtrados (UTF-8 con BOM para que Excel lea los acentos). Texto que empiece con = + - @ va con apóstrofo. */
|
||||
function csvResumenCob(lista) {
|
||||
const texto = s => { const v = String(s ?? ''); return '"' + (/^[=+\-@]/.test(v) ? "'" + v : v).replace(/"/g, '""') + '"'; };
|
||||
const num = n => n == null ? '' : r2(n).toFixed(2);
|
||||
const enc = ['Folio', 'Proyecto', 'Cliente', 'Línea de negocio', 'Contrato', 'Moneda', 'Contratado con IVA', 'Cobrado', 'Saldo', 'Avance %',
|
||||
'Siguiente etapa', 'Saldo siguiente etapa', 'Último pago', 'Órdenes de compra', 'Costo OC sin IVA', 'Margen sin IVA', 'Margen %'];
|
||||
const filas = lista.map(p => [texto(p.folio), texto(p.nombre), texto(p.cliente), texto(MARCA_TXT[p.marca] || ''), texto(p.estadoContrato),
|
||||
texto(p.moneda), num(p.contratado), num(p.pagado), num(p.saldo), String(p.avance), texto(p.siguienteEtapa),
|
||||
p.siguienteEtapa ? num(p.siguienteSaldo) : '', texto(p.ultimoPago), String(p.ordenes), num(p.costoOc), num(p.margen),
|
||||
p.margenPorcentaje == null ? '' : String(p.margenPorcentaje)].join(','));
|
||||
return '' + [enc.map(texto).join(',')].concat(filas).join('\r\n');
|
||||
}
|
||||
|
||||
function descargarCsvCob() {
|
||||
if (!RESUMEN) { mostrarError('Espera a que cargue el resumen.'); return; }
|
||||
const url = URL.createObjectURL(new Blob([csvResumenCob(filtrarResumenCob())], { type: 'text/csv;charset=utf-8' }));
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = 'cobranza-' + (RESUMEN.hoy || 'resumen') + '.csv';
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
/** Pinta todo a partir de la respuesta del servidor (vistaCobranza_). */
|
||||
function pintarCobranza(v) {
|
||||
COB = v;
|
||||
@ -1368,7 +1481,7 @@
|
||||
$('tabHist').classList.toggle('activo', t === 'hist');
|
||||
if (t === 'hist') cargarHistorial();
|
||||
if (t === 'cat') cargarCatalogo();
|
||||
if (t === 'cob') cargarProyectosCob();
|
||||
if (t === 'cob') { cargarProyectosCob(); verResumenCob(); }
|
||||
if (t === 'com') cargarProyectosCom();
|
||||
$('aviso').className = 'aviso';
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user