2026-10-13 20:17:52 -05:00
|
|
|
|
import { describe, expect, it } from 'vitest';
|
2026-10-16 11:07:38 -05:00
|
|
|
|
import { ANALYTIC_COLUMNS, DOCUMENT_LINE_COLUMNS, ITEMS_MULTI_ADD, lineColumnsFor, lineFieldsOf, newDocumentLine, recalcLine, uomEntryOf } from './line-columns';
|
2026-10-13 20:17:52 -05:00
|
|
|
|
|
|
|
|
|
|
describe('recalcLine', () => {
|
|
|
|
|
|
it('total = cantidad × precio, redondeado a 2 decimales', () => {
|
|
|
|
|
|
expect(recalcLine({ Quantity: 5, Price: 10000 })['LineTotal']).toBe(50000);
|
|
|
|
|
|
expect(recalcLine({ Quantity: 3, Price: 0.1 })['LineTotal']).toBe(0.3);
|
|
|
|
|
|
expect(recalcLine({ Quantity: 1.5, Price: 19.99 })['LineTotal']).toBe(29.99);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('aplica el % de descuento', () => {
|
|
|
|
|
|
expect(recalcLine({ Quantity: 2, Price: 100, DiscountPercent: 10 })['LineTotal']).toBe(180);
|
|
|
|
|
|
expect(recalcLine({ Quantity: 2, Price: 100, DiscountPercent: 100 })['LineTotal']).toBe(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('valores vacíos o no numéricos cuentan como 0 y se conserva el resto de la línea', () => {
|
|
|
|
|
|
const line = recalcLine({ ItemCode: 'A', Quantity: null, Price: 'abc' });
|
|
|
|
|
|
expect(line['LineTotal']).toBe(0);
|
|
|
|
|
|
expect(line['ItemCode']).toBe('A');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('columnas estándar de línea', () => {
|
|
|
|
|
|
const keys = DOCUMENT_LINE_COLUMNS.map((c) => c.key);
|
|
|
|
|
|
|
|
|
|
|
|
it('son las de una línea de documento de SAP (con TaxCode)', () => {
|
2026-10-16 11:07:38 -05:00
|
|
|
|
expect(keys).toEqual([
|
2026-10-16 11:09:47 -05:00
|
|
|
|
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode', 'LineTotal',
|
2026-10-16 11:07:38 -05:00
|
|
|
|
'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode', 'AccountCode',
|
2026-10-16 11:09:47 -05:00
|
|
|
|
'BaseType', 'BaseEntry', 'BaseLine',
|
2026-10-16 11:07:38 -05:00
|
|
|
|
]);
|
2026-10-13 20:17:52 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('el total es de solo lectura y artículo y cantidad son obligatorios', () => {
|
|
|
|
|
|
expect(DOCUMENT_LINE_COLUMNS.find((c) => c.key === 'LineTotal')?.readonly).toBe(true);
|
|
|
|
|
|
expect(DOCUMENT_LINE_COLUMNS.filter((c) => c.required).map((c) => c.key)).toEqual(['ItemCode', 'Quantity']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('artículo, almacén e impuesto tienen lista de valores sobre recursos registrados', () => {
|
|
|
|
|
|
const resources = DOCUMENT_LINE_COLUMNS.filter((c) => c.lookup).map((c) => c.lookup!.config.resource);
|
2026-10-16 11:07:38 -05:00
|
|
|
|
expect(resources.slice(0, 2)).toEqual(['Items', 'Warehouses']);
|
|
|
|
|
|
expect(resources).toContain('SalesTaxCodes');
|
2026-10-13 20:17:52 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('elegir un artículo rellena código y descripción sin perder el resto', () => {
|
|
|
|
|
|
const apply = DOCUMENT_LINE_COLUMNS[0].lookup!.apply;
|
|
|
|
|
|
expect(apply({ ItemCode: 'I1', ItemName: 'Disco' }, { Quantity: 3 })).toMatchObject({ ItemCode: 'I1', ItemDescription: 'Disco', Quantity: 3 });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('elegir almacén o impuesto solo cambia ese campo', () => {
|
|
|
|
|
|
const wh = DOCUMENT_LINE_COLUMNS.find((c) => c.key === 'WarehouseCode')!.lookup!.apply;
|
|
|
|
|
|
const tax = DOCUMENT_LINE_COLUMNS.find((c) => c.key === 'TaxCode')!.lookup!.apply;
|
|
|
|
|
|
expect(wh({ WarehouseCode: '01', WarehouseName: 'Principal' }, { ItemCode: 'A' })).toEqual({ ItemCode: 'A', WarehouseCode: '01' });
|
|
|
|
|
|
expect(tax({ Code: 'IVA', Rate: 19 }, { ItemCode: 'A' })).toEqual({ ItemCode: 'A', TaxCode: 'IVA' });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('línea nueva y "catálogo completo"', () => {
|
|
|
|
|
|
it('la línea nueva trae cantidad 1 y total 0', () => {
|
2026-10-14 11:06:58 -05:00
|
|
|
|
expect(newDocumentLine()).toMatchObject({ Quantity: 1, Price: null, LineTotal: 0, ItemCode: '' });
|
2026-10-13 20:17:52 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('cada artículo elegido se convierte en una línea nueva', () => {
|
|
|
|
|
|
expect(ITEMS_MULTI_ADD.toLine({ ItemCode: 'I9', ItemName: 'Cable' })).toMatchObject({ ItemCode: 'I9', ItemDescription: 'Cable', Quantity: 1 });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-10-13 22:00:54 -05:00
|
|
|
|
|
|
|
|
|
|
describe('lineColumnsFor', () => {
|
|
|
|
|
|
it('ventas conserva las columnas; compras cambia el maestro de impuestos', () => {
|
|
|
|
|
|
expect(lineColumnsFor('SalesTaxCodes')).toBe(DOCUMENT_LINE_COLUMNS);
|
|
|
|
|
|
const tax = lineColumnsFor('PurchaseTaxCodes').find((c) => c.key === 'TaxCode');
|
|
|
|
|
|
expect(tax?.lookup?.config.resource).toBe('PurchaseTaxCodes');
|
|
|
|
|
|
expect(DOCUMENT_LINE_COLUMNS.find((c) => c.key === 'TaxCode')?.lookup?.config.resource).toBe('SalesTaxCodes');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-10-14 14:37:01 -05:00
|
|
|
|
|
|
|
|
|
|
describe('uomEntryOf', () => {
|
|
|
|
|
|
it('usa la unidad de compra por defecto y, si no hay, la de inventario', () => {
|
|
|
|
|
|
expect(uomEntryOf({ UoMGroupEntry: 1, DefaultPurchasingUoMEntry: 3, InventoryUoMEntry: 1 })).toBe(3);
|
|
|
|
|
|
expect(uomEntryOf({ UoMGroupEntry: 1, DefaultPurchasingUoMEntry: null, InventoryUoMEntry: 1 })).toBe(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
it('un artículo de unidad manual (sin grupo) no lleva unidad: SAP no la admite', () => {
|
|
|
|
|
|
expect(uomEntryOf({ UoMGroupEntry: -1, InventoryUoMEntry: -1 })).toBeUndefined();
|
|
|
|
|
|
expect(uomEntryOf({ InventoryUoMEntry: 1 })).toBeUndefined();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-10-16 11:07:38 -05:00
|
|
|
|
|
|
|
|
|
|
describe('dimensiones, proyecto, cuenta y referencia base', () => {
|
|
|
|
|
|
const col = (key: string) => DOCUMENT_LINE_COLUMNS.find((c) => c.key === key)!;
|
|
|
|
|
|
|
|
|
|
|
|
it('cada dimensión de costos abre su ventana de selección, de su propia dimensión', () => {
|
|
|
|
|
|
for (const n of [1, 2, 3, 4, 5]) {
|
|
|
|
|
|
const c = col(n === 1 ? 'CostingCode' : `CostingCode${n}`);
|
|
|
|
|
|
expect(c.lookup!.config.resource).toBe('DistributionRules');
|
|
|
|
|
|
expect(c.lookup!.config.baseFilter).toContain(`InWhichDimension eq ${n}`);
|
|
|
|
|
|
expect(c.lookup!.apply({ FactorCode: 'CC9' }, {})).toEqual({ [c.key]: 'CC9' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('el proyecto y la cuenta también se eligen de su ventana', () => {
|
|
|
|
|
|
expect(col('ProjectCode').lookup!.config.resource).toBe('Projects');
|
|
|
|
|
|
expect(col('ProjectCode').lookup!.apply({ Code: 'PE001' }, {})).toEqual({ ProjectCode: 'PE001' });
|
|
|
|
|
|
expect(col('AccountCode').lookup!.config.resource).toBe('ChartOfAccounts');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('las columnas analíticas de inventario son las cinco dimensiones y el proyecto', () => {
|
|
|
|
|
|
expect(ANALYTIC_COLUMNS.map((c) => c.key)).toEqual(['CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('BaseType, BaseEntry y BaseLine son de solo lectura y solo se muestran si la línea viene copiada', () => {
|
|
|
|
|
|
const copied = { BaseType: 17, BaseEntry: 315, BaseLine: 2 };
|
|
|
|
|
|
const plain = { BaseType: -1, BaseEntry: null, BaseLine: null };
|
|
|
|
|
|
for (const k of ['BaseType', 'BaseEntry', 'BaseLine']) expect(col(k).readonly).toBe(true);
|
|
|
|
|
|
expect(col('BaseType').format!(copied)).toBe('Órdenes de venta');
|
|
|
|
|
|
expect(col('BaseEntry').format!(copied)).toBe('315');
|
|
|
|
|
|
expect(col('BaseLine').format!(copied)).toBe('2');
|
|
|
|
|
|
for (const k of ['BaseType', 'BaseEntry', 'BaseLine']) expect(col(k).format!(plain)).toBe('');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('las columnas de solo lectura (referencia base y total) no se envían a SAP', () => {
|
|
|
|
|
|
expect(lineFieldsOf(DOCUMENT_LINE_COLUMNS)).not.toEqual(expect.arrayContaining(['BaseType', 'BaseEntry', 'BaseLine', 'LineTotal']));
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('precio con y sin impuesto', () => {
|
|
|
|
|
|
const rates = new Map([['IVA', 19], ['EXE', 0]]);
|
|
|
|
|
|
const base = { Quantity: 2, DiscountPercent: 0, TaxCode: 'IVA' };
|
|
|
|
|
|
|
|
|
|
|
|
it('al escribir el precio calcula el precio con IVA', () => {
|
|
|
|
|
|
const l = recalcLine({ ...base, Price: 100 }, 'Price', rates);
|
|
|
|
|
|
expect(l).toMatchObject({ PriceAfterVAT: 119, LineTotal: 200, _priceBasis: 'net' });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('al escribir el precio con IVA calcula el precio neto y el total', () => {
|
|
|
|
|
|
const l = recalcLine({ ...base, PriceAfterVAT: 119 }, 'PriceAfterVAT', rates);
|
|
|
|
|
|
expect(l).toMatchObject({ Price: 100, LineTotal: 200, _priceBasis: 'gross' });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('al cambiar el impuesto mantiene lo último que escribió el usuario', () => {
|
|
|
|
|
|
const net = recalcLine({ ...base, Price: 100, _priceBasis: 'net' }, 'TaxCode', rates);
|
|
|
|
|
|
expect(net['PriceAfterVAT']).toBe(119);
|
|
|
|
|
|
const gross = recalcLine({ ...base, PriceAfterVAT: 119, Price: 100, _priceBasis: 'gross', TaxCode: 'EXE' }, 'TaxCode', rates);
|
|
|
|
|
|
expect(gross).toMatchObject({ Price: 119, LineTotal: 238 });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('vaciar un precio vacía el otro; sin tasas conocidas no toca los precios', () => {
|
|
|
|
|
|
expect(recalcLine({ ...base, Price: null }, 'Price', rates)['PriceAfterVAT']).toBeNull();
|
|
|
|
|
|
expect(recalcLine({ ...base, Price: 50, PriceAfterVAT: 80 }, 'Quantity')).toMatchObject({ Price: 50, PriceAfterVAT: 80, LineTotal: 100 });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|