feat(web): gastos adicionales de los documentos de ventas y compras como tabla de varias filas (antes solo el flete)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 12:46:56 -05:00
1 parent 3c1d02a263
commit 6c2cb30d63
10 files changed
+223 -162

No files matched your search

@@ -9,8 +9,10 @@ import {
taxRateMap,
updatePayload,
validateLayout,
freightOf,
freightPayload,
expenseRowsOf,
expensesChanged,
expensesPayload,
validateExpenses,
} from './document-model';
const line = (over: Record<string, unknown> = {}) => ({
@@ -37,12 +39,12 @@ describe('computeTotals', () => {
it('suma líneas e impuesto por tasa', () => {
const t = computeTotals([line(), line({ TaxCode: 'EXE', LineTotal: 100 })], rates, 0);
expect(t).toEqual({ subtotal: 300, discount: 0, net: 300, tax: 38, freight: 0, total: 338 });
expect(t).toEqual({ subtotal: 300, discount: 0, net: 300, tax: 38, expenses: 0, total: 338 });
});
it('aplica el descuento de cabecera antes del impuesto', () => {
const t = computeTotals([line()], rates, 10);
expect(t).toEqual({ subtotal: 200, discount: 20, net: 180, tax: 34.2, freight: 0, total: 214.2 });
expect(t).toEqual({ subtotal: 200, discount: 20, net: 180, tax: 34.2, expenses: 0, total: 214.2 });
});
it('un código de impuesto desconocido no suma impuesto', () => {
@@ -50,7 +52,7 @@ describe('computeTotals', () => {
});
it('sin líneas, todo en cero', () => {
expect(computeTotals([], rates, 0)).toEqual({ subtotal: 0, discount: 0, net: 0, tax: 0, freight: 0, total: 0 });
expect(computeTotals([], rates, 0)).toEqual({ subtotal: 0, discount: 0, net: 0, tax: 0, expenses: 0, total: 0 });
});
});
@@ -154,40 +156,38 @@ describe('navigationQuery', () => {
});
});
describe('flete (gastos adicionales)', () => {
describe('gastos adicionales', () => {
const doc = (rows: Record<string, unknown>[]) => ({ DocumentAdditionalExpenses: rows });
it('suma el flete al total del documento', () => {
it('suman al total del documento, con el impuesto de cada gasto', () => {
const rates = taxRateMap([{ Code: 'IVA', Rate: 19 }]);
const t = computeTotals([line({ LineTotal: 100, TaxCode: 'IVA' })], rates, 0, 25);
expect(t).toMatchObject({ subtotal: 100, tax: 19, freight: 25, total: 144 });
const t = computeTotals([line({ LineTotal: 100, TaxCode: 'IVA' })], rates, 0, [{ ExpenseCode: 1, LineTotal: 25 }, { ExpenseCode: 2, LineTotal: 100, TaxCode: 'IVA' }]);
expect(t).toMatchObject({ subtotal: 100, expenses: 125, tax: 38, total: 263 });
});
it('lee el flete de SAP: código, importe y si se puede editar', () => {
expect(freightOf(doc([{ ExpenseCode: 2, LineTotal: 1500 }]))).toEqual({ expense: '2', amount: 1500, editable: true });
expect(freightOf(undefined)).toEqual({ expense: '', amount: 0, editable: true });
expect(freightOf(doc([{ ExpenseCode: 1, LineTotal: 10 }, { ExpenseCode: 2, LineTotal: 5 }])).editable).toBe(false);
it('lee las filas de SAP para editarlas', () => {
expect(expenseRowsOf(doc([{ ExpenseCode: 2, LineTotal: 1500 }, { ExpenseCode: 1, LineTotal: 10 }]))).toHaveLength(2);
expect(expenseRowsOf(undefined)).toEqual([]);
});
it('al crear arma DocumentAdditionalExpenses con el gasto por defecto si no se eligió', () => {
const out = freightPayload({ CardCode: 'C1', FreightAmount: 1500, FreightExpense: '' }, { FreightAmount: 1500, FreightExpense: '' }, '2', 'create');
expect(out.body).toEqual({ CardCode: 'C1', DocumentAdditionalExpenses: [{ ExpenseCode: 2, LineTotal: 1500 }] });
expect(out.replace).toBe(true);
it('arma la colección con una fila por gasto, sin filas en blanco ni campos vacíos', () => {
expect(expensesPayload([{ ExpenseCode: '2', LineTotal: 1500.456, TaxCode: '', Remarks: 'Maritimo' }, { ExpenseCode: 1, LineTotal: 10, TaxCode: 'IGV' }, {}, { ExpenseCode: '', LineTotal: 0 }])).toEqual([
{ ExpenseCode: 2, LineTotal: 1500.46, Remarks: 'Maritimo' },
{ ExpenseCode: 1, LineTotal: 10, TaxCode: 'IGV' },
]);
});
it('al crear sin importe no envía flete', () => {
const out = freightPayload({ CardCode: 'C1', FreightAmount: 0 }, { FreightAmount: 0 }, '2', 'create');
expect(out.body).toEqual({ CardCode: 'C1' });
expect(out.replace).toBe(false);
it('detecta si cambiaron respecto del documento', () => {
const original = doc([{ ExpenseCode: 2, LineTotal: 25, LineNum: 0, Status: 'bost_Open' }]);
expect(expensesChanged(original, expenseRowsOf(original))).toBe(false);
expect(expensesChanged(original, [{ ExpenseCode: 2, LineTotal: 40 }])).toBe(true);
expect(expensesChanged(original, [])).toBe(true); // quitar el gasto también es un cambio
expect(expensesChanged(undefined, [{}])).toBe(false);
});
it('al modificar solo envía el flete si cambió, y con importe 0 lo quita', () => {
expect(freightPayload({ Comments: 'x' }, { FreightAmount: 10 }, '2', 'update', true)).toEqual({ body: { Comments: 'x' }, replace: false });
const changed = freightPayload({ FreightAmount: 300 }, { FreightAmount: 300, FreightExpense: '3' }, '2', 'update', true);
expect(changed).toEqual({ body: { DocumentAdditionalExpenses: [{ ExpenseCode: 3, LineTotal: 300 }] }, replace: true });
const cleared = freightPayload({ FreightAmount: 0 }, { FreightAmount: 0, FreightExpense: '3' }, '2', 'update', true);
expect(cleared).toEqual({ body: { DocumentAdditionalExpenses: [] }, replace: true });
expect(freightPayload({ FreightAmount: 0 }, { FreightAmount: 0 }, '2', 'update', false).body).toEqual({});
it('pide el gasto y un importe mayor que cero', () => {
expect(validateExpenses([{ ExpenseCode: 2, LineTotal: 10 }, {}])).toEqual([]);
expect(validateExpenses([{ LineTotal: 10 }, { ExpenseCode: 2, LineTotal: 0 }])).toEqual(['Gasto adicional 1: indique el gasto.', 'Gasto adicional 2: el importe debe ser mayor que cero.']);
});
});