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 ,
2026-10-14 15:10:40 -05:00
validateLayout ,
2026-10-19 12:46:56 -05:00
expenseRowsOf ,
expensesChanged ,
expensesPayload ,
validateExpenses ,
2026-10-13 21:58:01 -05:00
} 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 );
2026-10-19 12:46:56 -05:00
expect ( t ). toEqual ({ subtotal : 300 , discount : 0 , net : 300 , tax : 38 , expenses : 0 , total : 338 });
2026-10-13 21:58:01 -05:00
});
it ( 'aplica el descuento de cabecera antes del impuesto' , () => {
const t = computeTotals ([ line ()], rates , 10 );
2026-10-19 12:46:56 -05:00
expect ( t ). toEqual ({ subtotal : 200 , discount : 20 , net : 180 , tax : 34.2 , expenses : 0 , total : 214.2 });
2026-10-13 21:58:01 -05:00
});
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' , () => {
2026-10-19 12:46:56 -05:00
expect ( computeTotals ([], rates , 0 )). toEqual ({ subtotal : 0 , discount : 0 , net : 0 , tax : 0 , expenses : 0 , total : 0 });
2026-10-13 21:58:01 -05:00
});
});
2026-10-14 15:10:40 -05:00
describe ( 'validateLayout' , () => {
2026-10-13 21:58:01 -05:00
const header = { CardCode : 'C1' , DocDate : '2026-01-05' };
2026-10-14 15:10:40 -05:00
const required = [
{ key : 'CardCode' , label : 'el cliente' },
{ key : 'DocDate' , label : 'la fecha de contabilización' },
];
2026-10-13 21:58:01 -05:00
it ( 'documento correcto no tiene errores' , () => {
2026-10-14 15:10:40 -05:00
expect ( validateLayout ( header , [ line ()], required , [])). toEqual ([]);
2026-10-13 21:58:01 -05:00
});
2026-10-14 15:10:40 -05:00
it ( 'exige los campos obligatorios y al menos una línea con artículo' , () => {
expect ( validateLayout ({}, [ line ({ ItemCode : '' })], required , [])). toEqual ([
2026-10-13 21:58:01 -05:00
'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)' , () => {
2026-10-14 15:10:40 -05:00
expect ( validateLayout ( header , [ line ({ ItemCode : '' }), line ({ Quantity : 0 })], required , [])). toEqual ([
2026-10-13 21:58:01 -05:00
'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' });
});
});
2026-10-14 16:09:06 -05:00
2026-10-19 12:46:56 -05:00
describe ( 'gastos adicionales' , () => {
2026-10-14 16:09:06 -05:00
const doc = ( rows : Record < string , unknown >[]) => ({ DocumentAdditionalExpenses : rows });
2026-10-19 12:46:56 -05:00
it ( 'suman al total del documento, con el impuesto de cada gasto' , () => {
2026-10-14 16:09:06 -05:00
const rates = taxRateMap ([{ Code : 'IVA' , Rate : 19 }]);
2026-10-19 12:46:56 -05:00
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 });
2026-10-14 16:09:06 -05:00
});
2026-10-19 12:46:56 -05:00
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 ([]);
2026-10-14 16:09:06 -05:00
});
2026-10-19 12:46:56 -05:00
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' },
]);
2026-10-14 16:09:06 -05:00
});
2026-10-19 12:46:56 -05:00
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 );
2026-10-14 16:09:06 -05:00
});
2026-10-19 12:46:56 -05:00
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.' ]);
2026-10-14 16:09:06 -05:00
});
});
2026-10-19 12:19:18 -05:00
describe ( 'documentos de servicio' , () => {
const service = { key : 'DocumentLines' , fields : [ 'AccountCode' , 'ItemDescription' , 'TaxCode' , 'LineTotal' ], base : true , service : true };
it ( 'una línea de servicio vacía no tiene cuenta, descripción ni total' , () => {
expect ( isBlankLine ({ AccountCode : '' , ItemDescription : '' , LineTotal : 0 }, true )). toBe ( true );
expect ( isBlankLine ({ AccountCode : '_SYS1' , LineTotal : 0 }, true )). toBe ( false );
expect ( isBlankLine ({ ItemDescription : 'Alquiler' , LineTotal : 0 }, true )). toBe ( false );
expect ( isBlankLine ({ ItemCode : '' , LineTotal : 0 }, false )). toBe ( true );
});
it ( 'exige al menos una línea con cuenta y que cada una tenga cuenta y total' , () => {
expect ( validateLayout ({}, [], [], [], true )). toEqual ([ 'Agregue al menos una línea con cuenta contable.' ]);
expect ( validateLayout ({}, [{ ItemDescription : 'x' , LineTotal : 0 }], [], [], true )). toEqual ([
'Línea 1: indique la cuenta contable.' ,
'Línea 1: el total debe ser mayor que cero.' ,
]);
expect ( validateLayout ({}, [{ AccountCode : '_SYS1' , LineTotal : 50 }], [], [], true )). toEqual ([]);
});
it ( 'envía solo los campos de servicio, sin cantidad ni artículo' , () => {
const body = createPayload ({ DocType : 'dDocument_Service' , CardCode : 'P1' }, [{ AccountCode : '_SYS1' , ItemDescription : 'Alquiler' , TaxCode : 'IGV' , LineTotal : 100 , Quantity : 1 , ItemCode : '' }, { AccountCode : '' , LineTotal : 0 }], service );
expect ( body [ 'DocumentLines' ]). toEqual ([{ AccountCode : '_SYS1' , ItemDescription : 'Alquiler' , TaxCode : 'IGV' , LineTotal : 100 }]);
});
});