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:
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),
|
||||
];
|
||||
Reference in new issue
Block a user