feat(web): anticipo total, importe aplicado y saldo pendiente en el pie de facturas y notas de crédito

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 13:03:32 -05:00
1 parent c28207b0fa
commit 0b988733f6
7 files changed
+43 -11

No files matched your search

@@ -1,4 +1,4 @@
import { TAX_DOCUMENT, type DocumentScreen, docDate, docNum, invoiceColumns, marketing, referenceColumns, statusColumn, statusFilter, dateFilter } from '../screen-kit';
import { BILLING_DOCUMENT, TAX_DOCUMENT, type DocumentScreen, docDate, docNum, invoiceColumns, marketing, referenceColumns, statusColumn, statusFilter, dateFilter } from '../screen-kit';
import { purchaseRequestLayout } from './compras.layouts';
/** Compras: solicitudes de compra y documentos de proveedor. */
@@ -22,7 +22,7 @@ export const COMPRAS_SCREENS: readonly DocumentScreen[] = [
marketing('/compras/ofertas', 'PurchaseQuotations', 'Nueva oferta', 'Proveedor', { columns: referenceColumns }),
marketing('/compras/ordenes', 'PurchaseOrders', 'Nueva orden', 'Proveedor', { columns: referenceColumns }),
marketing('/compras/recepcion', 'PurchaseDeliveryNotes', 'Nueva recepción', 'Proveedor', TAX_DOCUMENT),
marketing('/compras/facturas', 'PurchaseInvoices', 'Nueva factura', 'Proveedor', { ...TAX_DOCUMENT, columns: invoiceColumns }),
marketing('/compras/facturas', 'PurchaseInvoices', 'Nueva factura', 'Proveedor', { ...BILLING_DOCUMENT, columns: invoiceColumns }),
marketing('/compras/devoluciones', 'PurchaseReturns', 'Nueva devolución', 'Proveedor'),
marketing('/compras/notas-credito', 'PurchaseCreditNotes', 'Nueva nota de crédito', 'Proveedor', TAX_DOCUMENT),
marketing('/compras/notas-credito', 'PurchaseCreditNotes', 'Nueva nota de crédito', 'Proveedor', BILLING_DOCUMENT),
];
@@ -139,6 +139,17 @@ describe('gastos adicionales en el formulario', () => {
expect(el().querySelector('.status')!.textContent).toContain('indique el gasto');
});
it('las facturas muestran anticipo, importe aplicado y saldo pendiente; una orden no', async () => {
await open('PurchaseInvoices', { ...order([]), DocTotal: 1000, PaidToDate: 400, DownPaymentAmount: 100 });
const row = (label: string) => [...el().querySelectorAll('label')].find((l) => l.textContent?.trim() === label)?.closest('.sbo-row, sbo-text-field, div')?.querySelector('input')?.value;
expect(row('Importe aplicado')).toBe('400.00');
expect(row('Saldo pendiente')).toBe('600.00');
expect(row('Anticipo total')).toBe('100.00');
TestBed.resetTestingModule();
await open('Orders', order([]));
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).not.toContain('Saldo pendiente');
});
it('las entradas de inventario no tienen gastos adicionales', () => {
const screen = DOCUMENT_SCREENS.find((s) => s.resource === 'InventoryGenEntries')!;
expect(JSON.stringify(screen.layout.footer)).not.toContain('expenses');
@@ -421,6 +421,14 @@ export class DocumentFormPage {
return this.fmt(this.totals().expenses);
case 'tax':
return this.fmt(this.totals().tax);
case 'downPayment':
return this.fmt(Number(this.doc()?.['DownPaymentAmount'] ?? 0));
case 'applied':
return this.fmt(Number(this.doc()?.['PaidToDate'] ?? 0));
case 'balance': {
const d = this.doc();
return this.fmt(d ? Number(d['DocTotal'] ?? 0) - Number(d['PaidToDate'] ?? 0) : 0);
}
case 'total':
return this.fmt(this.totals().total);
}
@@ -7,7 +7,7 @@ import { SERIES_SUBTYPE } from './document-objects';
import { ANALYTIC_COLUMNS, DOCUMENT_LINE_COLUMNS, lineColumnsFor, lineFieldsOf, newDocumentLine, newServiceLine, recalcLine, serviceLineColumns, uomEntryOf } from './line-columns';
/** Valores calculados o del socio que una fila de solo lectura puede mostrar. */
export type ComputedSource = 'docNum' | 'partnerName' | 'partnerTaxId' | 'subtotal' | 'expenses' | 'tax' | 'total';
export type ComputedSource = 'docNum' | 'partnerName' | 'partnerTaxId' | 'subtotal' | 'expenses' | 'tax' | 'total' | 'downPayment' | 'applied' | 'balance';
/**
* Un campo de la cabecera (o del pie) del formulario. La pantalla de cada documento declara su lista
@@ -237,6 +237,8 @@ export function marketingLayout(opts: {
uom: 'sales' | 'purchase';
/** Fila del flete en el pie (por defecto sí). */
expenses?: boolean;
/** Facturas y notas de crédito: el pie suma el anticipo total, el importe aplicado y el saldo pendiente. */
billing?: boolean;
/** Valores que se envían siempre al crear (ej. `DocumentSubType: 'bod_Bill'` en las boletas). */
fixed?: DocRecord;
}): DocumentLayout {
@@ -263,10 +265,15 @@ export function marketingLayout(opts: {
right: [
{ type: 'computed', label: 'Total antes del descuento', source: 'subtotal' },
{ type: 'number', key: 'DiscountPercent', label: 'Descuento %', decimals: 2 },
// facturas y notas de crédito: lo que ya se anticipó, se aplicó y falta por pagar (como en SAP)
...(opts.billing ? [{ type: 'computed', label: 'Anticipo total', source: 'downPayment' } as const] : []),
{ type: 'computed', label: 'Gastos adicionales', source: 'expenses' },
{ type: 'rounding', label: 'Redondeo' },
{ type: 'computed', label: 'Impuesto', source: 'tax' },
{ type: 'computed', label: 'Total del documento', source: 'total' },
...(opts.billing
? [{ type: 'computed', label: 'Importe aplicado', source: 'applied' } as const, { type: 'computed', label: 'Saldo pendiente', source: 'balance' } as const]
: []),
],
},
columns,
@@ -142,6 +142,8 @@ export interface MarketingExtras {
header?: FieldColumns;
/** Filtro fijo de la lista y de la navegación entre documentos. */
baseFilter?: string;
/** Documento de cobro o pago (factura, nota de crédito): el pie muestra anticipo, importe aplicado y saldo pendiente. */
billing?: boolean;
/** Valores que se envían siempre al crear (ej. `DocumentSubType`). */
fixed?: DocRecord;
/** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */
@@ -153,6 +155,9 @@ export interface MarketingExtras {
/** Documentos tributarios: la cabecera suma el folio (en el Service Layer `FolioPrefixString` y `FolioNumber`). */
export const TAX_DOCUMENT: MarketingExtras = { headerRight: [FIELDS.folio] };
/** Facturas y notas de crédito: documento tributario que además se cobra o se paga. */
export const BILLING_DOCUMENT: MarketingExtras = { ...TAX_DOCUMENT, billing: true };
/** Ventas y compras: documentos con socio de negocio y total. */
export const marketing = (
path: string,
@@ -171,6 +176,7 @@ export const marketing = (
lineColumns: extras.lineColumns,
lookups: extras.lookups,
fixed: extras.fixed,
billing: extras.billing,
uom: path.startsWith('/ventas') ? 'sales' : 'purchase',
}),
resource,
@@ -1,4 +1,4 @@
import { TAX_DOCUMENT, type DocumentScreen, invoiceColumns, marketing, referenceColumns } from '../screen-kit';
import { BILLING_DOCUMENT, TAX_DOCUMENT, type DocumentScreen, invoiceColumns, marketing, referenceColumns } from '../screen-kit';
/**
* Filtros que separan las cuatro pantallas de `Invoices`: `ReserveInvoice` (reserva sí/no) y el subtipo
@@ -21,29 +21,29 @@ export const VENTAS_SCREENS: readonly DocumentScreen[] = [
// Facturas, boletas y sus reservas son el mismo recurso (`Invoices`); cada pantalla filtra por su
// `DocumentSubType` exacto y por `ReserveInvoice`, y envía ambos al crear.
marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente', {
...TAX_DOCUMENT,
...BILLING_DOCUMENT,
columns: invoiceColumns,
baseFilter: `${NOT_RESERVE} and ${INVOICE}`,
fixed: { DocumentSubType: 'bod_None' },
}),
marketing('/ventas/boletas', 'Invoices', 'Nueva boleta', 'Cliente', {
...TAX_DOCUMENT,
...BILLING_DOCUMENT,
columns: invoiceColumns,
baseFilter: `${NOT_RESERVE} and ${BILL}`,
fixed: { DocumentSubType: 'bod_Bill' },
}),
marketing('/ventas/facturas-reserva', 'Invoices', 'Nueva factura de reserva', 'Cliente', {
...TAX_DOCUMENT,
...BILLING_DOCUMENT,
columns: invoiceColumns,
baseFilter: `${RESERVE} and ${INVOICE}`,
fixed: { ReserveInvoice: 'tYES', DocumentSubType: 'bod_None' },
}),
marketing('/ventas/boletas-reserva', 'Invoices', 'Nueva boleta de reserva', 'Cliente', {
...TAX_DOCUMENT,
...BILLING_DOCUMENT,
columns: invoiceColumns,
baseFilter: `${RESERVE} and ${BILL}`,
fixed: { ReserveInvoice: 'tYES', DocumentSubType: 'bod_Bill' },
}),
marketing('/ventas/devoluciones', 'Returns', 'Nueva devolución', 'Cliente'),
marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', TAX_DOCUMENT),
marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente', BILLING_DOCUMENT),
];