feat(web): serie, indicador y proyecto en la cabecera; dimensiones, proyecto, cuenta, referencia base y precio con IVA en el detalle
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f003ee4bf
commit
f66ef108cf
22 files changed
+698
-100
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DOCUMENT_LINE_COLUMNS, ITEMS_MULTI_ADD, lineColumnsFor, newDocumentLine, recalcLine, uomEntryOf } from './line-columns';
|
||||
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', () => {
|
||||
@@ -24,7 +24,11 @@ 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', 'DiscountPercent', 'TaxCode', 'LineTotal']);
|
||||
expect(keys).toEqual([
|
||||
'ItemCode', 'ItemDescription', 'Quantity', 'WarehouseCode', 'Price', 'PriceAfterVAT', 'DiscountPercent', 'TaxCode',
|
||||
'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode', 'AccountCode',
|
||||
'BaseType', 'BaseEntry', 'BaseLine', 'LineTotal',
|
||||
]);
|
||||
});
|
||||
|
||||
it('el total es de solo lectura y artículo y cantidad son obligatorios', () => {
|
||||
@@ -34,7 +38,8 @@ describe('columnas estándar de línea', () => {
|
||||
|
||||
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).toEqual(['Items', 'Warehouses', 'SalesTaxCodes']);
|
||||
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', () => {
|
||||
@@ -79,3 +84,67 @@ describe('uomEntryOf', () => {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user