|
|
|
@@ -14,13 +14,23 @@
|
|
|
|
|
* the EFECTIVO* set OR `checkNumber` null + amount > 0 (true cash).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { Prisma } from "@jorgecuadros/database";
|
|
|
|
|
import { Prisma, type ServiceKind } from "@jorgecuadros/database";
|
|
|
|
|
import {
|
|
|
|
|
BALANCE_FLOOR_JOIN,
|
|
|
|
|
BALANCE_FORWARD_TYPE,
|
|
|
|
|
balanceScopeSql,
|
|
|
|
|
NOT_SUPERSEDED,
|
|
|
|
|
notCashJournal,
|
|
|
|
|
periodSourceTable,
|
|
|
|
|
} from "../billing/billing.service";
|
|
|
|
|
import {
|
|
|
|
|
UTILITIES_SYSTEM,
|
|
|
|
|
UTILITIES_TABLE,
|
|
|
|
|
} from "../customers/numid.service";
|
|
|
|
|
import type { PrismaService } from "../prisma/prisma.service";
|
|
|
|
|
import { CONCEPT_BY_KIND } from "../statements/statements.service";
|
|
|
|
|
import {
|
|
|
|
|
type ColumnDef,
|
|
|
|
|
intParam,
|
|
|
|
|
NOT_VOIDED,
|
|
|
|
|
parseDate,
|
|
|
|
@@ -37,19 +47,80 @@ function nameOf(c: { name: string; nameMissing: boolean }): string {
|
|
|
|
|
return c.nameMissing ? "(sin nombre)" : c.name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isoDay = (d: Date | null | undefined) =>
|
|
|
|
|
d ? d.toISOString().slice(0, 10) : "—";
|
|
|
|
|
|
|
|
|
|
/** Every report leads with the legacy customer number (2026-10 meeting). */
|
|
|
|
|
const NUMID_COLUMN: ColumnDef = { key: "numid", label: "NUMid", type: "text" };
|
|
|
|
|
/** Policy reports add the insurance file number beside it. */
|
|
|
|
|
const INSURANCE_NO_COLUMN: ColumnDef = {
|
|
|
|
|
key: "insuranceNo",
|
|
|
|
|
label: "No. seguros",
|
|
|
|
|
type: "text",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy customer numbers for a page of report rows. The office files,
|
|
|
|
|
* captures and talks in NUMids, not platform ids.
|
|
|
|
|
*
|
|
|
|
|
* The utilities NUMid is the portal number, one per customer. insurance/DATGRAL
|
|
|
|
|
* is a separate id space under the same table name, so it is read on its own
|
|
|
|
|
* and never shown as a NUMid; 16 customers hold more than one.
|
|
|
|
|
*/
|
|
|
|
|
async function legacyNumbers(prisma: PrismaService, customerIds: string[]) {
|
|
|
|
|
const ids = [...new Set(customerIds)];
|
|
|
|
|
const refs = ids.length
|
|
|
|
|
? await prisma.customerLegacyRef.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
customerId: { in: ids },
|
|
|
|
|
sourceTable: UTILITIES_TABLE,
|
|
|
|
|
sourceSystem: { in: [UTILITIES_SYSTEM, "insurance"] },
|
|
|
|
|
},
|
|
|
|
|
select: { customerId: true, sourceSystem: true, legacyId: true },
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
const byCustomer = new Map<string, { numid?: string; insurance: string[] }>();
|
|
|
|
|
for (const r of refs) {
|
|
|
|
|
const e = byCustomer.get(r.customerId) ?? { insurance: [] };
|
|
|
|
|
if (r.sourceSystem === UTILITIES_SYSTEM) e.numid = r.legacyId;
|
|
|
|
|
else e.insurance.push(r.legacyId);
|
|
|
|
|
byCustomer.set(r.customerId, e);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
numid: (id: string) => byCustomer.get(id)?.numid ?? "—",
|
|
|
|
|
insurance: (id: string) =>
|
|
|
|
|
byCustomer
|
|
|
|
|
.get(id)
|
|
|
|
|
?.insurance.sort((a, b) => Number(a) - Number(b))
|
|
|
|
|
.join(", ") || "—",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ reports */
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* LISTADO EN ROJO — overdue customers worklist.
|
|
|
|
|
* Same data as the receivables worklist with balance=owing, but presented
|
|
|
|
|
* as a printable report rather than a paginated browser.
|
|
|
|
|
*
|
|
|
|
|
* Legacy printed it from `ALFABETICO EN ROJO`: `SUM(CHARGECREDIT) < 0` over
|
|
|
|
|
* DATOS2, by name. That sum was only honest because DATOS2 held one year plus
|
|
|
|
|
* its BALANCE FORWARD. The platform keeps every year, so the balance here is
|
|
|
|
|
* the statement's own — the floor, the cash journal and the archive window via
|
|
|
|
|
* the same fragments `BillingService.balances()` uses. Summed raw, as this
|
|
|
|
|
* report used to, 195 customers read in the red against 22 real ones: every
|
|
|
|
|
* closed account whose history the opening balance had already settled.
|
|
|
|
|
*
|
|
|
|
|
* Ordered by the most recent real movement (Jorge, 2026-10): the oldest
|
|
|
|
|
* debtors are the accounts that closed, the ones worth chasing are recent.
|
|
|
|
|
* Inactive customers (`status = 0` — the office's own and internal accounts,
|
|
|
|
|
* e.g. TRASPASOS PAYPAL at -12M) stay out unless asked for.
|
|
|
|
|
*/
|
|
|
|
|
const listadoEnRojo: ReportDef = {
|
|
|
|
|
slug: "listado-en-rojo",
|
|
|
|
|
title: "Clientes en rojo",
|
|
|
|
|
description:
|
|
|
|
|
"Cartera vencida: clientes con saldo deudor en la moneda seleccionada, " +
|
|
|
|
|
"ordenados del más antiguo al más reciente.",
|
|
|
|
|
"Cartera vencida: clientes con saldo deudor en la moneda seleccionada — " +
|
|
|
|
|
"el mismo saldo de su estado de cuenta — ordenados por el movimiento " +
|
|
|
|
|
"más reciente. Los clientes inactivos quedan fuera salvo que se pidan.",
|
|
|
|
|
domain: "estado-cuenta",
|
|
|
|
|
legacyName: "LISTADO EN ROJO",
|
|
|
|
|
format: "tabular",
|
|
|
|
@@ -64,6 +135,45 @@ const listadoEnRojo: ReportDef = {
|
|
|
|
|
],
|
|
|
|
|
defaultValue: "MXN",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "domain",
|
|
|
|
|
label: "Ramo",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "ALL", label: "Todos" },
|
|
|
|
|
{ value: "UTILITY", label: "Servicios" },
|
|
|
|
|
{ value: "INSURANCE", label: "Seguros" },
|
|
|
|
|
],
|
|
|
|
|
defaultValue: "ALL",
|
|
|
|
|
},
|
|
|
|
|
{ key: "since", label: "Con movimiento desde", kind: "date" },
|
|
|
|
|
{
|
|
|
|
|
key: "minOwed",
|
|
|
|
|
label: "Adeudo mínimo",
|
|
|
|
|
kind: "number",
|
|
|
|
|
placeholder: "0",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "inactive",
|
|
|
|
|
label: "Clientes inactivos",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "no", label: "Excluir" },
|
|
|
|
|
{ value: "yes", label: "Incluir" },
|
|
|
|
|
],
|
|
|
|
|
defaultValue: "no",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "sort",
|
|
|
|
|
label: "Orden",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "recent", label: "Movimiento más reciente" },
|
|
|
|
|
{ value: "debt", label: "Mayor adeudo" },
|
|
|
|
|
{ value: "name", label: "Nombre" },
|
|
|
|
|
],
|
|
|
|
|
defaultValue: "recent",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "query",
|
|
|
|
|
label: "Buscar (nombre o ciudad)",
|
|
|
|
@@ -72,24 +182,48 @@ const listadoEnRojo: ReportDef = {
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "id", label: "#", type: "text" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
{ key: "name", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "city", label: "Ciudad", type: "text" },
|
|
|
|
|
{ key: "movements", label: "Movs.", type: "number", align: "right" },
|
|
|
|
|
{ key: "balance", label: "Saldo", type: "money", align: "right" },
|
|
|
|
|
{ key: "lastMovement", label: "Último movimiento", type: "date" },
|
|
|
|
|
{ key: "lastPayment", label: "Último abono", type: "date" },
|
|
|
|
|
{ key: "balance", label: "Saldo", type: "money", align: "right" },
|
|
|
|
|
],
|
|
|
|
|
async run(prisma, p) {
|
|
|
|
|
const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD";
|
|
|
|
|
const q = p.query?.trim();
|
|
|
|
|
const nameFilter = q
|
|
|
|
|
? Prisma.sql`AND (c.name LIKE ${`%${q}%`} OR c.city LIKE ${`%${q}%`})`
|
|
|
|
|
const since = parseDate(p.since);
|
|
|
|
|
const minOwed = Math.max(Number(p.minOwed) || 0, 0.005);
|
|
|
|
|
|
|
|
|
|
const filters: Prisma.Sql[] = [];
|
|
|
|
|
if (q) {
|
|
|
|
|
filters.push(Prisma.sql`(c.name LIKE ${`%${q}%`} OR c.city LIKE ${`%${q}%`})`);
|
|
|
|
|
}
|
|
|
|
|
if (p.domain === "UTILITY" || p.domain === "INSURANCE") {
|
|
|
|
|
filters.push(Prisma.sql`t.domain = ${p.domain}`);
|
|
|
|
|
}
|
|
|
|
|
if (p.inactive !== "yes") {
|
|
|
|
|
filters.push(Prisma.sql`c.status = 1 AND c.archivedAt IS NULL`);
|
|
|
|
|
}
|
|
|
|
|
const where = filters.length
|
|
|
|
|
? Prisma.sql`AND ${Prisma.join(filters, " AND ")}`
|
|
|
|
|
: Prisma.empty;
|
|
|
|
|
|
|
|
|
|
const bal =
|
|
|
|
|
currency === "USD"
|
|
|
|
|
? Prisma.sql`SUM(CASE WHEN t.currency = 'USD' THEN t.amount ELSE 0 END)`
|
|
|
|
|
: Prisma.sql`SUM(CASE WHEN t.currency = 'MXN' THEN t.amount ELSE 0 END)`;
|
|
|
|
|
// The Jan 1 BALANCE FORWARD is not a movement — every customer has one, so
|
|
|
|
|
// counting it would date every closed account to this year.
|
|
|
|
|
const real = Prisma.sql`(tt.nameEn IS NULL OR tt.nameEn <> ${BALANCE_FORWARD_TYPE})`;
|
|
|
|
|
const lastMovement = Prisma.sql`MAX(CASE WHEN ${real} THEN t.transactionDate END)`;
|
|
|
|
|
|
|
|
|
|
const orderBy =
|
|
|
|
|
p.sort === "debt"
|
|
|
|
|
? Prisma.sql`ORDER BY balance ASC`
|
|
|
|
|
: p.sort === "name"
|
|
|
|
|
? Prisma.sql`ORDER BY c.nameMissing ASC, c.name ASC`
|
|
|
|
|
: Prisma.sql`ORDER BY lastMovement DESC, balance ASC`;
|
|
|
|
|
|
|
|
|
|
const rows = await prisma.$queryRaw<
|
|
|
|
|
Array<{
|
|
|
|
@@ -97,38 +231,39 @@ const listadoEnRojo: ReportDef = {
|
|
|
|
|
name: string;
|
|
|
|
|
nameMissing: boolean;
|
|
|
|
|
city: string | null;
|
|
|
|
|
movements: bigint | number | string;
|
|
|
|
|
balance: Prisma.Decimal | null;
|
|
|
|
|
lastMovement: Date | null;
|
|
|
|
|
lastPayment: Date | null;
|
|
|
|
|
}>
|
|
|
|
|
>`
|
|
|
|
|
SELECT c.id, c.name, c.nameMissing, c.city,
|
|
|
|
|
COUNT(*) AS movements,
|
|
|
|
|
${bal} AS balance,
|
|
|
|
|
MAX(t.transactionDate) AS lastMovement
|
|
|
|
|
${lastMovement} AS lastMovement,
|
|
|
|
|
MAX(CASE WHEN ${real} AND t.amount > 0 THEN t.transactionDate END) AS lastPayment
|
|
|
|
|
FROM customers c
|
|
|
|
|
JOIN transactions t ON t.customerId = c.id
|
|
|
|
|
WHERE t.voidedAt IS NULL ${nameFilter}
|
|
|
|
|
LEFT JOIN type_transactions tt ON tt.id = t.typeId
|
|
|
|
|
${BALANCE_FLOOR_JOIN}
|
|
|
|
|
WHERE t.voidedAt IS NULL AND t.outstanding = 0 AND ${NOT_SUPERSEDED}
|
|
|
|
|
${balanceScopeSql()} ${where}
|
|
|
|
|
GROUP BY c.id, c.name, c.nameMissing, c.city
|
|
|
|
|
HAVING ${bal} < -0.005
|
|
|
|
|
ORDER BY MAX(t.transactionDate) ASC, c.nameMissing ASC, c.name ASC
|
|
|
|
|
HAVING ${bal} < ${-minOwed}
|
|
|
|
|
${since ? Prisma.sql`AND ${lastMovement} >= ${since}` : Prisma.empty}
|
|
|
|
|
${orderBy}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.id));
|
|
|
|
|
let totalBalance = new Prisma.Decimal(0);
|
|
|
|
|
let totalMovs = 0;
|
|
|
|
|
const out = rows.map((r) => {
|
|
|
|
|
const b = r.balance ?? new Prisma.Decimal(0);
|
|
|
|
|
totalBalance = totalBalance.plus(b);
|
|
|
|
|
totalMovs += Number(r.movements);
|
|
|
|
|
return {
|
|
|
|
|
id: r.id.slice(0, 8),
|
|
|
|
|
numid: nums.numid(r.id),
|
|
|
|
|
name: nameOf(r),
|
|
|
|
|
city: r.city ?? "—",
|
|
|
|
|
movements: Number(r.movements),
|
|
|
|
|
lastMovement: isoDay(r.lastMovement),
|
|
|
|
|
lastPayment: isoDay(r.lastPayment),
|
|
|
|
|
balance: b.toFixed(2),
|
|
|
|
|
lastMovement: r.lastMovement
|
|
|
|
|
? r.lastMovement.toISOString().slice(0, 10)
|
|
|
|
|
: "—",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -136,31 +271,58 @@ const listadoEnRojo: ReportDef = {
|
|
|
|
|
rows: out,
|
|
|
|
|
totals: {
|
|
|
|
|
customers: out.length,
|
|
|
|
|
movements: totalMovs,
|
|
|
|
|
balance: totalBalance.toFixed(2),
|
|
|
|
|
currency,
|
|
|
|
|
},
|
|
|
|
|
subtitle: `Moneda: ${currency} · ${out.length} clientes con saldo deudor`,
|
|
|
|
|
subtitle: `Moneda: ${currency} · ${out.length} clientes con saldo deudor${
|
|
|
|
|
p.inactive === "yes" ? " · incluye inactivos" : ""
|
|
|
|
|
}`,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** The services whose bill the office pays and books as a ledger concept. */
|
|
|
|
|
const SERVICE_OPTIONS: { value: ServiceKind; label: string }[] = [
|
|
|
|
|
{ value: "WATER", label: "Agua" },
|
|
|
|
|
{ value: "ELECTRIC", label: "Luz" },
|
|
|
|
|
{ value: "TELEPHONE", label: "Teléfono" },
|
|
|
|
|
{ value: "GAS", label: "Gas" },
|
|
|
|
|
{ value: "PROPERTY_TAX", label: "Predial" },
|
|
|
|
|
{ value: "FEDERAL_ZONE", label: "Zona federal" },
|
|
|
|
|
{ value: "CABLE", label: "Cable" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const serviceLabel = (kind: string) =>
|
|
|
|
|
SERVICE_OPTIONS.find((o) => o.value === kind)?.label ?? kind;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PAGOS NO EFECTUADOS (AGUA / LUZ / TEL).
|
|
|
|
|
* Customers enrolled in a service (by PropertyService.kind) with no
|
|
|
|
|
* related ledger charge in the last N days. Heuristic: any non-voided
|
|
|
|
|
* charge-type transaction in the window counts as "they paid". The
|
|
|
|
|
* report groups by property so the same customer with two water meters
|
|
|
|
|
* appears once per property.
|
|
|
|
|
* PAGOS NO EFECTUADOS (AGUA / LUZ / TEL / …).
|
|
|
|
|
*
|
|
|
|
|
* Legacy `PAGO AGUA/LUZ/TEL NO`: every customer with the service, LEFT JOIN
|
|
|
|
|
* `LOCALIZA PAGO *` — the rows of THAT service's concept dated in the period
|
|
|
|
|
* the operator typed — where nothing was found. So: "whose water did we not
|
|
|
|
|
* pay this month". The first port asked a different question ("no charge of
|
|
|
|
|
* any kind in N days"), which is why customers who were up to date kept
|
|
|
|
|
* showing: one IZZI or annual-fee row hid a missing water bill, and the
|
|
|
|
|
* reverse. It also passed `ELECTRICITY`/`PHONE`, which are not ServiceKind
|
|
|
|
|
* values, so Luz and Teléfono never returned a row at all.
|
|
|
|
|
*
|
|
|
|
|
* Matched per customer, as legacy did. A charge row carries no link to the
|
|
|
|
|
* service it paid, and its reference only matches the stored account for
|
|
|
|
|
* water and phone (813/822, 501/521) — CFE bills post under a reference that
|
|
|
|
|
* is not the RPU (5 of 1109) — so matching on the reference would list every
|
|
|
|
|
* electric customer as unpaid.
|
|
|
|
|
*
|
|
|
|
|
* A row captured "sin fondos" in the window is not a payment: the customer is
|
|
|
|
|
* listed with that status, which is the follow-up the office actually owes.
|
|
|
|
|
*/
|
|
|
|
|
const pagosNoEfectuados: ReportDef = {
|
|
|
|
|
slug: "pagos-no-efectuados",
|
|
|
|
|
title: "Pagos no efectuados",
|
|
|
|
|
description:
|
|
|
|
|
"Clientes con un servicio contratado (agua, luz o teléfono) sin " +
|
|
|
|
|
"movimientos de cargo en los últimos N días. Heurística basada en el " +
|
|
|
|
|
"servicio registrado en la propiedad y la ausencia de cargos en el " +
|
|
|
|
|
"periodo seleccionado.",
|
|
|
|
|
"Clientes con el servicio contratado sin un pago de ese servicio " +
|
|
|
|
|
"registrado en el periodo. Sin fechas, revisa un ciclo hacia atrás " +
|
|
|
|
|
"desde hoy: 31 días, o 62 para luz (bimestral).",
|
|
|
|
|
domain: "servicios",
|
|
|
|
|
legacyName: "PAGOS NO EFECTUADOS AGUA/LUZ/TEL",
|
|
|
|
|
format: "tabular",
|
|
|
|
@@ -169,139 +331,143 @@ const pagosNoEfectuados: ReportDef = {
|
|
|
|
|
key: "serviceKind",
|
|
|
|
|
label: "Servicio",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "WATER", label: "Agua" },
|
|
|
|
|
{ value: "ELECTRICITY", label: "Luz" },
|
|
|
|
|
{ value: "PHONE", label: "Teléfono" },
|
|
|
|
|
],
|
|
|
|
|
options: SERVICE_OPTIONS,
|
|
|
|
|
defaultValue: "WATER",
|
|
|
|
|
},
|
|
|
|
|
{ key: "from", label: "Desde (por defecto, un ciclo atrás)", kind: "date" },
|
|
|
|
|
{ key: "to", label: "Hasta (por defecto, hoy)", kind: "date", endOfDay: true },
|
|
|
|
|
{
|
|
|
|
|
key: "days",
|
|
|
|
|
label: "Días sin movimiento",
|
|
|
|
|
kind: "number",
|
|
|
|
|
defaultValue: "60",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "currency",
|
|
|
|
|
label: "Moneda",
|
|
|
|
|
key: "inactive",
|
|
|
|
|
label: "Clientes inactivos",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "MXN", label: "MXN" },
|
|
|
|
|
{ value: "USD", label: "USD" },
|
|
|
|
|
{ value: "no", label: "Excluir" },
|
|
|
|
|
{ value: "yes", label: "Incluir" },
|
|
|
|
|
],
|
|
|
|
|
defaultValue: "MXN",
|
|
|
|
|
defaultValue: "no",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "customerId", label: "Cliente #", type: "text" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "propertyAddress", label: "Dirección", type: "text" },
|
|
|
|
|
{ key: "accountNumber", label: "Cuenta / Medidor", type: "text" },
|
|
|
|
|
{ key: "lastCharge", label: "Último cargo", type: "date" },
|
|
|
|
|
{ key: "balance", label: "Saldo", type: "money", align: "right" },
|
|
|
|
|
{ key: "accountNumber", label: "Cuenta", type: "text" },
|
|
|
|
|
{ key: "status", label: "Estado", type: "text" },
|
|
|
|
|
{ key: "lastPaid", label: "Último pago del servicio", type: "date" },
|
|
|
|
|
],
|
|
|
|
|
async run(prisma, p) {
|
|
|
|
|
const kind = (p.serviceKind ?? "WATER") as
|
|
|
|
|
| "WATER"
|
|
|
|
|
| "ELECTRICITY"
|
|
|
|
|
| "PHONE";
|
|
|
|
|
const days = intParam(p, "days", 60, 1, 365);
|
|
|
|
|
const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD";
|
|
|
|
|
const cutoff = new Date(Date.now() - days * 86400000);
|
|
|
|
|
const kind = (
|
|
|
|
|
SERVICE_OPTIONS.some((o) => o.value === p.serviceKind)
|
|
|
|
|
? p.serviceKind
|
|
|
|
|
: "WATER"
|
|
|
|
|
) as ServiceKind;
|
|
|
|
|
const concept = CONCEPT_BY_KIND[kind]!;
|
|
|
|
|
// No period given: one billing cycle back from the end date, so the
|
|
|
|
|
// report means the same on the 2nd as on the 28th. "This month" listed
|
|
|
|
|
// nearly everyone early in the month — the bills are captured ~20th.
|
|
|
|
|
const to = parseDate(p.to, true) ?? new Date();
|
|
|
|
|
const cycleDays = kind === "ELECTRIC" ? 62 : 31;
|
|
|
|
|
const from =
|
|
|
|
|
parseDate(p.from) ?? new Date(to.getTime() - cycleDays * 86400000);
|
|
|
|
|
const inactive =
|
|
|
|
|
p.inactive === "yes"
|
|
|
|
|
? Prisma.empty
|
|
|
|
|
: Prisma.sql`AND c.status = 1`;
|
|
|
|
|
|
|
|
|
|
// One concept row for the customer, not voided, in the window.
|
|
|
|
|
const conceptRow = (outstanding: 0 | 1) => Prisma.sql`
|
|
|
|
|
SELECT 1 FROM transactions t
|
|
|
|
|
JOIN type_transactions tt ON tt.id = t.typeId
|
|
|
|
|
WHERE t.customerId = c.id AND t.voidedAt IS NULL
|
|
|
|
|
AND t.outstanding = ${outstanding} AND tt.nameEn = ${concept}
|
|
|
|
|
AND t.transactionDate >= ${from} AND t.transactionDate <= ${to}`;
|
|
|
|
|
|
|
|
|
|
// Customers enrolled in the service on a non-archived property, with no
|
|
|
|
|
// charge-type transaction in the window. The subquery picks up
|
|
|
|
|
// *anything* the customer paid (any domain, any type) — close enough
|
|
|
|
|
// for the staff's "who's overdue" view; the precise per-service match
|
|
|
|
|
// would need a per-service typeId taxonomy that doesn't exist in the
|
|
|
|
|
// legacy data.
|
|
|
|
|
const rows = await prisma.$queryRaw<
|
|
|
|
|
Array<{
|
|
|
|
|
customerId: string;
|
|
|
|
|
customerName: string;
|
|
|
|
|
nameMissing: boolean;
|
|
|
|
|
propertyId: string;
|
|
|
|
|
propertyAddress: string | null;
|
|
|
|
|
accountNumber: string | null;
|
|
|
|
|
lastCharge: Date | null;
|
|
|
|
|
balance: Prisma.Decimal | null;
|
|
|
|
|
addresses: string | null;
|
|
|
|
|
accounts: string | null;
|
|
|
|
|
unfunded: bigint | number;
|
|
|
|
|
lastPaid: Date | null;
|
|
|
|
|
}>
|
|
|
|
|
>`
|
|
|
|
|
SELECT
|
|
|
|
|
c.id AS customerId,
|
|
|
|
|
c.name AS customerName,
|
|
|
|
|
c.nameMissing AS nameMissing,
|
|
|
|
|
pr.id AS propertyId,
|
|
|
|
|
pr.addressLine1 AS propertyAddress,
|
|
|
|
|
ps.accountNumber AS accountNumber,
|
|
|
|
|
GROUP_CONCAT(DISTINCT pr.addressLine1 ORDER BY pr.addressLine1 SEPARATOR ' / ') AS addresses,
|
|
|
|
|
GROUP_CONCAT(DISTINCT NULLIF(ps.accountNumber, '') SEPARATOR ', ') AS accounts,
|
|
|
|
|
EXISTS(${conceptRow(1)}) AS unfunded,
|
|
|
|
|
(SELECT MAX(t.transactionDate) FROM transactions t
|
|
|
|
|
JOIN type_transactions tt ON tt.id = t.typeId
|
|
|
|
|
WHERE t.customerId = c.id AND t.voidedAt IS NULL
|
|
|
|
|
AND t.amount < 0
|
|
|
|
|
AND t.transactionDate >= ${cutoff}) AS lastCharge,
|
|
|
|
|
(SELECT SUM(t.amount) FROM transactions t
|
|
|
|
|
WHERE t.customerId = c.id AND t.voidedAt IS NULL
|
|
|
|
|
AND t.currency = ${currency}) AS balance
|
|
|
|
|
AND t.outstanding = 0 AND tt.nameEn = ${concept}) AS lastPaid
|
|
|
|
|
FROM property_services ps
|
|
|
|
|
JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL
|
|
|
|
|
JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL
|
|
|
|
|
WHERE ps.kind = ${kind} AND ps.active = 1
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM transactions t
|
|
|
|
|
WHERE t.customerId = c.id AND t.voidedAt IS NULL
|
|
|
|
|
AND t.amount < 0 AND t.transactionDate >= ${cutoff}
|
|
|
|
|
)
|
|
|
|
|
ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC
|
|
|
|
|
WHERE ps.kind = ${kind} AND ps.active = 1 ${inactive}
|
|
|
|
|
AND NOT EXISTS (${conceptRow(0)})
|
|
|
|
|
GROUP BY c.id, c.name, c.nameMissing
|
|
|
|
|
ORDER BY c.nameMissing ASC, c.name ASC
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
let totalBalance = new Prisma.Decimal(0);
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customerId));
|
|
|
|
|
let unfunded = 0;
|
|
|
|
|
const out = rows.map((r) => {
|
|
|
|
|
const b = r.balance ?? new Prisma.Decimal(0);
|
|
|
|
|
totalBalance = totalBalance.plus(b);
|
|
|
|
|
if (Number(r.unfunded)) unfunded++;
|
|
|
|
|
return {
|
|
|
|
|
customerId: r.customerId.slice(0, 8),
|
|
|
|
|
customerName: nameOf({
|
|
|
|
|
name: r.customerName,
|
|
|
|
|
nameMissing: r.nameMissing,
|
|
|
|
|
}),
|
|
|
|
|
propertyAddress: r.propertyAddress ?? "—",
|
|
|
|
|
accountNumber: r.accountNumber ?? "—",
|
|
|
|
|
lastCharge: r.lastCharge
|
|
|
|
|
? r.lastCharge.toISOString().slice(0, 10)
|
|
|
|
|
: "—",
|
|
|
|
|
balance: b.toFixed(2),
|
|
|
|
|
numid: nums.numid(r.customerId),
|
|
|
|
|
customerName: nameOf({ name: r.customerName, nameMissing: r.nameMissing }),
|
|
|
|
|
propertyAddress: r.addresses ?? "—",
|
|
|
|
|
accountNumber: r.accounts ?? "—",
|
|
|
|
|
status: Number(r.unfunded) ? "Sin fondos" : "Sin pago",
|
|
|
|
|
lastPaid: isoDay(r.lastPaid),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rows: out,
|
|
|
|
|
totals: {
|
|
|
|
|
rows: out.length,
|
|
|
|
|
balance: totalBalance.toFixed(2),
|
|
|
|
|
currency,
|
|
|
|
|
clientes: out.length,
|
|
|
|
|
"sin pago": out.length - unfunded,
|
|
|
|
|
"sin fondos": unfunded,
|
|
|
|
|
},
|
|
|
|
|
subtitle: `Servicio: ${
|
|
|
|
|
kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono"
|
|
|
|
|
} · ${days} días · ${out.length} propiedades sin cargo reciente`,
|
|
|
|
|
subtitle: `Servicio: ${serviceLabel(kind)} · ${isoDay(from)} a ${isoDay(to)} · ${out.length} clientes sin pago registrado`,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FALTANTES DE (AGUA / LUZ / TEL).
|
|
|
|
|
* Data-quality report: properties enrolled in a service that are missing
|
|
|
|
|
* the key identifier the legacy system required (account/meter/route).
|
|
|
|
|
* Different `faltante` per service kind in the legacy because the
|
|
|
|
|
* service's identifier fields differ; here we flag any of the three
|
|
|
|
|
* common identifiers being blank.
|
|
|
|
|
* What each service must have on file to be paid and matched — the columns
|
|
|
|
|
* legacy's FALTANTES DE AGUA/LUZ/TEL read (DATMEX.AGUA; MEDIDOR + RPU;
|
|
|
|
|
* TELEFONO), in the field `scopedRefField` stores each one in. The first port
|
|
|
|
|
* demanded account AND meter AND route of every kind; water and phone have no
|
|
|
|
|
* route, so every one of them was flagged.
|
|
|
|
|
*/
|
|
|
|
|
const REQUIRED_FIELDS: Partial<
|
|
|
|
|
Record<ServiceKind, Array<["accountNumber" | "meterNumber", string]>>
|
|
|
|
|
> = {
|
|
|
|
|
WATER: [["accountNumber", "Sin número de cuenta"]],
|
|
|
|
|
ELECTRIC: [
|
|
|
|
|
["accountNumber", "Sin RPU"],
|
|
|
|
|
["meterNumber", "Sin medidor"],
|
|
|
|
|
],
|
|
|
|
|
TELEPHONE: [["accountNumber", "Sin número de teléfono"]],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FALTANTES DE (AGUA / LUZ / TEL) — data quality: active services missing the
|
|
|
|
|
* identifier the office needs to pay and match the bill.
|
|
|
|
|
*/
|
|
|
|
|
const faltantes: ReportDef = {
|
|
|
|
|
slug: "faltantes",
|
|
|
|
|
title: "Faltantes de datos por servicio",
|
|
|
|
|
description:
|
|
|
|
|
"Calidad de datos: propiedades con un servicio contratado que no " +
|
|
|
|
|
"tienen número de cuenta, medidor o ruta registrado. El reporte que " +
|
|
|
|
|
"en la legacy corría como FALTANTES DE AGUA / LUZ / TEL.",
|
|
|
|
|
"Calidad de datos: servicios activos sin el dato que se necesita para " +
|
|
|
|
|
"pagar el recibo — agua: número de cuenta; luz: RPU y medidor; " +
|
|
|
|
|
"teléfono: número.",
|
|
|
|
|
domain: "servicios",
|
|
|
|
|
legacyName: "FALTANTES DE AGUA/LUZ/TEL",
|
|
|
|
|
format: "tabular",
|
|
|
|
@@ -310,37 +476,34 @@ const faltantes: ReportDef = {
|
|
|
|
|
key: "serviceKind",
|
|
|
|
|
label: "Servicio",
|
|
|
|
|
kind: "select",
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "WATER", label: "Agua" },
|
|
|
|
|
{ value: "ELECTRICITY", label: "Luz" },
|
|
|
|
|
{ value: "PHONE", label: "Teléfono" },
|
|
|
|
|
],
|
|
|
|
|
options: SERVICE_OPTIONS.filter((o) => REQUIRED_FIELDS[o.value]),
|
|
|
|
|
defaultValue: "WATER",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "customerId", label: "Cliente #", type: "text" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "propertyAddress", label: "Dirección", type: "text" },
|
|
|
|
|
{ key: "missing", label: "Faltante", type: "text" },
|
|
|
|
|
{ key: "dueDay", label: "Día de vencimiento", type: "text" },
|
|
|
|
|
],
|
|
|
|
|
async run(prisma, p) {
|
|
|
|
|
const kind = (p.serviceKind ?? "WATER") as
|
|
|
|
|
| "WATER"
|
|
|
|
|
| "ELECTRICITY"
|
|
|
|
|
| "PHONE";
|
|
|
|
|
const kind = (
|
|
|
|
|
p.serviceKind && REQUIRED_FIELDS[p.serviceKind as ServiceKind]
|
|
|
|
|
? p.serviceKind
|
|
|
|
|
: "WATER"
|
|
|
|
|
) as ServiceKind;
|
|
|
|
|
const required = REQUIRED_FIELDS[kind]!;
|
|
|
|
|
|
|
|
|
|
// Column names come from the constant above, never from the request.
|
|
|
|
|
const blank = (field: string) =>
|
|
|
|
|
Prisma.sql`(ps.${Prisma.raw(field)} IS NULL OR TRIM(ps.${Prisma.raw(field)}) = '')`;
|
|
|
|
|
|
|
|
|
|
// A row per (property, missing field). The "missing" string describes
|
|
|
|
|
// what's blank so the report is self-explanatory when printed.
|
|
|
|
|
const rows = await prisma.$queryRaw<
|
|
|
|
|
Array<{
|
|
|
|
|
customerId: string;
|
|
|
|
|
customerName: string;
|
|
|
|
|
nameMissing: boolean;
|
|
|
|
|
propertyId: string;
|
|
|
|
|
propertyAddress: string | null;
|
|
|
|
|
dueDay: string | null;
|
|
|
|
|
missing: string;
|
|
|
|
|
}>
|
|
|
|
|
>`
|
|
|
|
@@ -348,44 +511,30 @@ const faltantes: ReportDef = {
|
|
|
|
|
c.id AS customerId,
|
|
|
|
|
c.name AS customerName,
|
|
|
|
|
c.nameMissing AS nameMissing,
|
|
|
|
|
pr.id AS propertyId,
|
|
|
|
|
pr.addressLine1 AS propertyAddress,
|
|
|
|
|
ps.dueDay AS dueDay,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN ps.accountNumber IS NULL OR ps.accountNumber = '' THEN 'Sin número de cuenta'
|
|
|
|
|
WHEN ps.meterNumber IS NULL OR ps.meterNumber = '' THEN 'Sin número de medidor'
|
|
|
|
|
WHEN ps.route IS NULL OR ps.route = '' THEN 'Sin ruta'
|
|
|
|
|
ELSE ''
|
|
|
|
|
END AS missing
|
|
|
|
|
CONCAT_WS(', ', ${Prisma.join(
|
|
|
|
|
required.map(([field, label]) => Prisma.sql`IF(${blank(field)}, ${label}, NULL)`),
|
|
|
|
|
)}) AS missing
|
|
|
|
|
FROM property_services ps
|
|
|
|
|
JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL
|
|
|
|
|
JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL
|
|
|
|
|
WHERE ps.kind = ${kind} AND ps.active = 1
|
|
|
|
|
AND (
|
|
|
|
|
ps.accountNumber IS NULL OR ps.accountNumber = ''
|
|
|
|
|
OR ps.meterNumber IS NULL OR ps.meterNumber = ''
|
|
|
|
|
OR ps.route IS NULL OR ps.route = ''
|
|
|
|
|
)
|
|
|
|
|
WHERE ps.kind = ${kind} AND ps.active = 1 AND c.status = 1
|
|
|
|
|
AND (${Prisma.join(required.map(([field]) => blank(field)), " OR ")})
|
|
|
|
|
ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customerId));
|
|
|
|
|
const out = rows.map((r) => ({
|
|
|
|
|
customerId: r.customerId.slice(0, 8),
|
|
|
|
|
customerName: nameOf({
|
|
|
|
|
name: r.customerName,
|
|
|
|
|
nameMissing: r.nameMissing,
|
|
|
|
|
}),
|
|
|
|
|
numid: nums.numid(r.customerId),
|
|
|
|
|
customerName: nameOf({ name: r.customerName, nameMissing: r.nameMissing }),
|
|
|
|
|
propertyAddress: r.propertyAddress ?? "—",
|
|
|
|
|
missing: r.missing,
|
|
|
|
|
dueDay: r.dueDay ?? "—",
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rows: out,
|
|
|
|
|
totals: { rows: out.length },
|
|
|
|
|
subtitle: `Servicio: ${
|
|
|
|
|
kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono"
|
|
|
|
|
} · ${out.length} propiedades con datos faltantes`,
|
|
|
|
|
subtitle: `Servicio: ${serviceLabel(kind)} · ${out.length} servicios con datos faltantes`,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
@@ -422,6 +571,7 @@ const reporteDeEfectivo: ReportDef = {
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "date", label: "Fecha", type: "date" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "concept", label: "Concepto", type: "text" },
|
|
|
|
|
{ key: "source", label: "Origen", type: "text" },
|
|
|
|
@@ -436,6 +586,8 @@ const reporteDeEfectivo: ReportDef = {
|
|
|
|
|
NOT_VOIDED,
|
|
|
|
|
{ amount: { gt: 0 } },
|
|
|
|
|
{ currency },
|
|
|
|
|
// A credit opening balance has no check number but is not cash.
|
|
|
|
|
{ OR: [{ typeId: null }, { type: { nameEn: { not: BALANCE_FORWARD_TYPE } } }] },
|
|
|
|
|
{
|
|
|
|
|
OR: [
|
|
|
|
|
{ legacySourceTable: { in: ["EFECTIVO", "EFECTIVO_BACKUP"] } },
|
|
|
|
@@ -468,15 +620,17 @@ const reporteDeEfectivo: ReportDef = {
|
|
|
|
|
message: true,
|
|
|
|
|
legacySourceTable: true,
|
|
|
|
|
type: { select: { nameEs: true, nameEn: true } },
|
|
|
|
|
customer: { select: { name: true, nameMissing: true } },
|
|
|
|
|
customer: { select: { id: true, name: true, nameMissing: true } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customer.id));
|
|
|
|
|
let total = new Prisma.Decimal(0);
|
|
|
|
|
const out = rows.map((r) => {
|
|
|
|
|
total = total.plus(r.amount);
|
|
|
|
|
return {
|
|
|
|
|
date: r.transactionDate.toISOString().slice(0, 10),
|
|
|
|
|
numid: nums.numid(r.customer.id),
|
|
|
|
|
customerName: nameOf(r.customer),
|
|
|
|
|
concept: r.message ?? r.type?.nameEs ?? r.type?.nameEn ?? "—",
|
|
|
|
|
source: r.legacySourceTable ?? "—",
|
|
|
|
@@ -541,6 +695,8 @@ const vigente: ReportDef = {
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "policyNumber", label: "Póliza", type: "text" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
INSURANCE_NO_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "provider", label: "Aseguradora", type: "text" },
|
|
|
|
|
{ key: "agent", label: "Agente", type: "text" },
|
|
|
|
@@ -571,11 +727,12 @@ const vigente: ReportDef = {
|
|
|
|
|
policyTo: true,
|
|
|
|
|
netPremium: true,
|
|
|
|
|
agentName: true,
|
|
|
|
|
customer: { select: { name: true, nameMissing: true } },
|
|
|
|
|
customer: { select: { id: true, name: true, nameMissing: true } },
|
|
|
|
|
insuranceProvider: { select: { name: true } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customer.id));
|
|
|
|
|
let totalPremium = new Prisma.Decimal(0);
|
|
|
|
|
const out = rows.map((r) => {
|
|
|
|
|
const daysTo = r.policyTo
|
|
|
|
@@ -586,6 +743,8 @@ const vigente: ReportDef = {
|
|
|
|
|
if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium);
|
|
|
|
|
return {
|
|
|
|
|
policyNumber: r.policyNumber,
|
|
|
|
|
numid: nums.numid(r.customer.id),
|
|
|
|
|
insuranceNo: nums.insurance(r.customer.id),
|
|
|
|
|
customerName: nameOf(r.customer),
|
|
|
|
|
provider: r.insuranceProvider?.name ?? "—",
|
|
|
|
|
agent: r.agentName ?? "—",
|
|
|
|
@@ -689,6 +848,8 @@ const avisoRenovacion: ReportDef = {
|
|
|
|
|
// ReportRunner.tsx) — same trade-off edoCuentaDatos makes for "statement".
|
|
|
|
|
columns: [
|
|
|
|
|
{ key: "policyNumber", label: "Póliza", type: "text" },
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
INSURANCE_NO_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "provider", label: "Aseguradora", type: "text" },
|
|
|
|
|
{ key: "policyTo", label: "Vence", type: "date" },
|
|
|
|
@@ -720,11 +881,16 @@ const avisoRenovacion: ReportDef = {
|
|
|
|
|
select: renewalLetterSelect(generation),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customer.id));
|
|
|
|
|
let totalPremium = new Prisma.Decimal(0);
|
|
|
|
|
let sentCount = 0;
|
|
|
|
|
const out = rows.map((r) => {
|
|
|
|
|
if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium);
|
|
|
|
|
const letter = toRenewalLetterRow(r, generation);
|
|
|
|
|
const letter = {
|
|
|
|
|
...toRenewalLetterRow(r, generation),
|
|
|
|
|
numid: nums.numid(r.customer.id),
|
|
|
|
|
insuranceNo: nums.insurance(r.customer.id),
|
|
|
|
|
};
|
|
|
|
|
if (letter.sentAt) sentCount++;
|
|
|
|
|
return letter;
|
|
|
|
|
});
|
|
|
|
@@ -802,6 +968,7 @@ const edoCuentaDatos: ReportDef = {
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!customer) return { rows: [], subtitle: "Cliente no encontrado" };
|
|
|
|
|
const numid = (await legacyNumbers(prisma, [customer.id])).numid(customer.id);
|
|
|
|
|
|
|
|
|
|
// The source-table exclusion, the balance floor and the year scope below
|
|
|
|
|
// are BillingService.statement's, because this report and
|
|
|
|
@@ -966,6 +1133,7 @@ const edoCuentaDatos: ReportDef = {
|
|
|
|
|
{
|
|
|
|
|
__kind: "header",
|
|
|
|
|
name: nameOf(customer),
|
|
|
|
|
numid,
|
|
|
|
|
address: customer.addressLine1 ?? "",
|
|
|
|
|
city: [customer.city, customer.state].filter(Boolean).join(", "),
|
|
|
|
|
phone: customer.phone ?? "",
|
|
|
|
@@ -983,7 +1151,7 @@ const edoCuentaDatos: ReportDef = {
|
|
|
|
|
...carried,
|
|
|
|
|
...movements,
|
|
|
|
|
],
|
|
|
|
|
subtitle: `${nameOf(customer)} · ${year} · ${visible.length} movimientos`,
|
|
|
|
|
subtitle: `NUMid ${numid} · ${nameOf(customer)} · ${year} · ${visible.length} movimientos`,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
@@ -1021,6 +1189,7 @@ const chequeCount: ReportDef = {
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
columns: [
|
|
|
|
|
NUMID_COLUMN,
|
|
|
|
|
{ key: "customerName", label: "Cliente", type: "text" },
|
|
|
|
|
{ key: "reference", label: "Referencia", type: "text" },
|
|
|
|
|
{ key: "period", label: "Periodo", type: "text" },
|
|
|
|
@@ -1050,7 +1219,7 @@ const chequeCount: ReportDef = {
|
|
|
|
|
period: true,
|
|
|
|
|
outstanding: true,
|
|
|
|
|
type: { select: { nameEn: true, nameEs: true } },
|
|
|
|
|
customer: { select: { name: true, nameMissing: true } },
|
|
|
|
|
customer: { select: { id: true, name: true, nameMissing: true } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -1076,8 +1245,10 @@ const chequeCount: ReportDef = {
|
|
|
|
|
}
|
|
|
|
|
if (outstandingCount) totalsOut["sin fondos"] = outstandingCount;
|
|
|
|
|
|
|
|
|
|
const nums = await legacyNumbers(prisma, rows.map((r) => r.customer.id));
|
|
|
|
|
return {
|
|
|
|
|
rows: rows.map((r) => ({
|
|
|
|
|
numid: nums.numid(r.customer.id),
|
|
|
|
|
customerName: nameOf(r.customer),
|
|
|
|
|
reference: r.reference ?? "—",
|
|
|
|
|
period: r.period ?? "—",
|
|
|
|
|