2026-10-13 21:58:01 -05:00
|
|
|
import { describe, expect, it } from 'vitest';
|
2026-10-14 11:06:58 -05:00
|
|
|
import { newDocumentLine } from './line-columns';
|
2026-10-13 21:58:01 -05:00
|
|
|
import {
|
|
|
|
|
computeTotals,
|
|
|
|
|
createPayload,
|
|
|
|
|
isBlankLine,
|
|
|
|
|
isDocumentReadOnly,
|
|
|
|
|
navigationQuery,
|
|
|
|
|
taxRateMap,
|
|
|
|
|
updatePayload,
|
|
|
|
|
validateDocument,
|
|
|
|
|
} from './document-model';
|
|
|
|
|
|
|
|
|
|
const line = (over: Record<string, unknown> = {}) => ({
|
|
|
|
|
ItemCode: 'A1',
|
|
|
|
|
Quantity: 2,
|
|
|
|
|
Price: 100,
|
|
|
|
|
DiscountPercent: 0,
|
|
|
|
|
TaxCode: 'IVA',
|
|
|
|
|
LineTotal: 200,
|
|
|
|
|
...over,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('isDocumentReadOnly', () => {
|
|
|
|
|
it('cerrado o cancelado es solo lectura', () => {
|
|
|
|
|
expect(isDocumentReadOnly({ DocumentStatus: 'bost_Close' })).toBe(true);
|
|
|
|
|
expect(isDocumentReadOnly({ DocumentStatus: 'bost_Open', Cancelled: 'tYES' })).toBe(true);
|
|
|
|
|
expect(isDocumentReadOnly({ DocumentStatus: 'bost_Open', Cancelled: 'tNO' })).toBe(false);
|
|
|
|
|
expect(isDocumentReadOnly(null)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('computeTotals', () => {
|
|
|
|
|
const rates = taxRateMap([{ Code: 'IVA', Rate: 19 }, { Code: 'EXE', Rate: 0 }]);
|
|
|
|
|
|
|
|
|
|
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, 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, total: 214.2 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('un código de impuesto desconocido no suma impuesto', () => {
|
|
|
|
|
expect(computeTotals([line({ TaxCode: '??' })], rates, 0).tax).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('sin líneas, todo en cero', () => {
|
|
|
|
|
expect(computeTotals([], rates, 0)).toEqual({ subtotal: 0, discount: 0, net: 0, tax: 0, total: 0 });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('validateDocument', () => {
|
|
|
|
|
const header = { CardCode: 'C1', DocDate: '2026-01-05' };
|
|
|
|
|
|
|
|
|
|
it('documento correcto no tiene errores', () => {
|
|
|
|
|
expect(validateDocument(header, [line()], 'Cliente')).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('exige socio, fecha y al menos una línea con artículo', () => {
|
|
|
|
|
expect(validateDocument({}, [line({ ItemCode: '' })], 'Cliente')).toEqual([
|
|
|
|
|
'Indique el cliente.',
|
|
|
|
|
'Indique la fecha de contabilización.',
|
|
|
|
|
'Agregue al menos una línea con artículo.',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('exige cantidad mayor que cero (numerando solo las líneas reales)', () => {
|
|
|
|
|
expect(validateDocument(header, [line({ ItemCode: '' }), line({ Quantity: 0 })], 'Cliente')).toEqual([
|
|
|
|
|
'Línea 1: la cantidad debe ser mayor que cero.',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('createPayload', () => {
|
|
|
|
|
it('quita campos vacíos y líneas en blanco, y no envía el total de línea', () => {
|
|
|
|
|
const body = createPayload({ CardCode: 'C1', Comments: '', NumAtCard: ' ' }, [
|
|
|
|
|
line({ ItemDescription: '', WarehouseCode: '01' }),
|
|
|
|
|
line({ ItemCode: '' }),
|
|
|
|
|
]);
|
|
|
|
|
expect(body).toEqual({
|
|
|
|
|
CardCode: 'C1',
|
|
|
|
|
DocumentLines: [{ ItemCode: 'A1', Quantity: 2, Price: 100, DiscountPercent: 0, TaxCode: 'IVA', WarehouseCode: '01' }],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-14 11:06:58 -05:00
|
|
|
it('un precio sin escribir no se envía: SAP aplica la lista de precios', () => {
|
|
|
|
|
const body = createPayload({ CardCode: 'C1' }, [{ ...newDocumentLine(), ItemCode: 'A1' }]);
|
|
|
|
|
expect(body['DocumentLines']).toEqual([{ ItemCode: 'A1', Quantity: 1, DiscountPercent: 0 }]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-14 11:59:07 -05:00
|
|
|
it('envía la referencia al origen solo en las líneas copiadas', () => {
|
|
|
|
|
const body = createPayload({ CardCode: 'C1' }, [
|
|
|
|
|
line({ BaseType: 17, BaseEntry: 315, BaseLine: 0 }),
|
|
|
|
|
line({ BaseType: -1, BaseEntry: null, BaseLine: null }),
|
|
|
|
|
]);
|
|
|
|
|
const [copied, own] = body['DocumentLines'] as Record<string, unknown>[];
|
|
|
|
|
expect(copied).toMatchObject({ BaseType: 17, BaseEntry: 315, BaseLine: 0 });
|
|
|
|
|
expect(own).not.toHaveProperty('BaseType');
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-13 21:58:01 -05:00
|
|
|
it('isBlankLine', () => {
|
|
|
|
|
expect(isBlankLine({ ItemCode: '' })).toBe(true);
|
|
|
|
|
expect(isBlankLine({ ItemCode: 'A1' })).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('updatePayload', () => {
|
|
|
|
|
const original = { CardCode: 'C1', Comments: 'hola', NumAtCard: '' };
|
|
|
|
|
const lines = [line()];
|
|
|
|
|
|
|
|
|
|
it('sin cambios no envía nada', () => {
|
|
|
|
|
expect(updatePayload(original, { ...original }, lines, [line()])).toEqual({ body: {}, replaceLines: false });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('envía solo la cabecera que cambió, y null al vaciar', () => {
|
|
|
|
|
const r = updatePayload(original, { CardCode: 'C1', Comments: '', NumAtCard: 'X' }, lines, lines);
|
|
|
|
|
expect(r).toEqual({ body: { Comments: null, NumAtCard: 'X' }, replaceLines: false });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('si cambian las líneas envía todas para reemplazar la colección', () => {
|
|
|
|
|
const r = updatePayload(original, original, lines, [line(), line({ ItemCode: 'B2' })]);
|
|
|
|
|
expect(r.replaceLines).toBe(true);
|
|
|
|
|
expect((r.body['DocumentLines'] as unknown[]).length).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('una fila en blanco añadida no cuenta como cambio', () => {
|
|
|
|
|
expect(updatePayload(original, original, lines, [line(), line({ ItemCode: '' })]).replaceLines).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('navigationQuery', () => {
|
|
|
|
|
it('primero y último ignoran el documento actual', () => {
|
|
|
|
|
expect(navigationQuery('first', 5)).toEqual({ $orderby: 'DocEntry asc' });
|
|
|
|
|
expect(navigationQuery('last', 5)).toEqual({ $orderby: 'DocEntry desc' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('anterior y siguiente parten del documento actual', () => {
|
|
|
|
|
expect(navigationQuery('prev', 5)).toEqual({ $filter: 'DocEntry lt 5', $orderby: 'DocEntry desc' });
|
|
|
|
|
expect(navigationQuery('next', 5)).toEqual({ $filter: 'DocEntry gt 5', $orderby: 'DocEntry asc' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('desde un documento nuevo, anterior = último y siguiente = primero', () => {
|
|
|
|
|
expect(navigationQuery('prev', null)).toEqual({ $orderby: 'DocEntry desc' });
|
|
|
|
|
expect(navigationQuery('next', null)).toEqual({ $orderby: 'DocEntry asc' });
|
|
|
|
|
});
|
|
|
|
|
});
|