Files
basic-erp/apps/web/src/app/features/documents/line-columns.spec.ts
T

151 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { ANALYTIC_COLUMNS, DOCUMENT_LINE_COLUMNS, ITEMS_MULTI_ADD, lineColumnsFor, lineFieldsOf, newDocumentLine, recalcLine, uomEntryOf } from './line-columns';
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)', () => {
expect(keys).toEqual([
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode', 'LineTotal',
'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode', 'AccountCode',
'BaseType', 'BaseEntry', 'BaseLine',
]);
});
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);
expect(resources.slice(0, 2)).toEqual(['Items', 'Warehouses']);
expect(resources).toContain('SalesTaxCodes');
});
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', () => {
expect(newDocumentLine()).toMatchObject({ Quantity: 1, Price: null, LineTotal: 0, ItemCode: '' });
});
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 });
});
});
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');
});
});
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();
});
});
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 });
});
});