import { describe, expect, it } from 'vitest'; import { EntradaAbierta, LineaPlanificacion } from '../models/planificacion.js'; import { asignarFifo, consolidarDocumentos, extraerLineasAplicables, puedeCambiarSeleccion } from './fifo.js'; function entrada(docEntry: number, docDate: string, openQty: number, cardCode = 'P1'): EntradaAbierta { return { docEntry, lineNum: 0, docNum: docEntry + 1000, docDate, cardCode, cardName: `Prov ${cardCode}`, openQty }; } describe('asignarFifo', () => { it('reparte la cantidad desde la Entrada más antigua', () => { const entradas = new Map([['A001', [entrada(2, '2026-02-01', 10), entrada(1, '2026-01-01', 4)]]]); const { lineas, advertencias } = asignarFifo([{ itemCode: 'a001', itemName: 'Art', quantity: 6, onHand: 20 }], entradas); expect(advertencias).toEqual([]); expect(lineas.map((l) => [l.tipoFila, l.baseEntry, l.allocatedQty])).toEqual([ ['F', null, null], ['S', 1, 4], ['S', 2, 2], ]); expect(lineas[1]).toMatchObject({ itemCode: 'a001', applied: true, baseType: 20, baseRef: 1001 }); }); it('omite artículos sin stock suficiente', () => { const { lineas, advertencias } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 6, onHand: 5 }], new Map()); expect(lineas).toEqual([]); expect(advertencias[0]).toContain('sin stock'); }); it('advierte cuando las Entradas no alcanzan', () => { const entradas = new Map([['A001', [entrada(1, '2026-01-01', 2)]]]); const { lineas, advertencias } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 5, onHand: 5 }], entradas); expect(lineas).toHaveLength(2); expect(advertencias[0]).toContain('faltan 3'); }); }); describe('extraerLineasAplicables / puedeCambiarSeleccion', () => { const base = asignarFifo( [{ itemCode: 'A001', itemName: 'Art', quantity: 5, onHand: 5 }], new Map([['A001', [entrada(1, '2026-01-01', 2), entrada(2, '2026-01-02', 2), entrada(3, '2026-01-03', 2)]]]), ).lineas; it('devuelve solo Hijos marcados sin documento destino', () => { const lineas: LineaPlanificacion[] = base.map((l) => ({ ...l })); lineas[2].applied = false; lineas[3].targetEntry = 99; expect(extraerLineasAplicables(lineas).map((l) => l.baseEntry)).toEqual([1]); }); it('hereda el ItemCode del Padre si el Hijo no lo trae', () => { const lineas = base.map((l) => (l.tipoFila === 'S' ? { ...l, itemCode: '' } : l)); expect(extraerLineasAplicables(lineas)[0].itemCode).toBe('A001'); }); it('bloquea la selección de líneas con documento destino', () => { expect(puedeCambiarSeleccion(base[1])).toBe(true); expect(puedeCambiarSeleccion({ ...base[1], targetEntry: 10 })).toBe(false); expect(puedeCambiarSeleccion(base[0])).toBe(false); }); }); describe('consolidarDocumentos', () => { it('agrupa por proveedor y corta en lotes', () => { const entradas = new Map([ ['A001', [entrada(1, '2026-01-01', 1, 'P1'), entrada(2, '2026-01-02', 1, 'P2'), entrada(3, '2026-01-03', 1, 'P1'), entrada(4, '2026-01-04', 1, 'P1')]], ]); const { lineas } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 4, onHand: 4 }], entradas); const docs = consolidarDocumentos(extraerLineasAplicables(lineas), 2); expect(docs.map((d) => [d.numero, d.cardCode, d.lineas.map((l) => l.baseEntry)])).toEqual([ [1, 'P1', [1, 3]], [2, 'P1', [4]], [3, 'P2', [2]], ]); }); it('usa 11 líneas si el máximo no es válido', () => { const entradas = new Map([['A001', Array.from({ length: 12 }, (_, i) => entrada(i + 1, '2026-01-01', 1))]]); const { lineas } = asignarFifo([{ itemCode: 'A001', itemName: 'Art', quantity: 12, onHand: 12 }], entradas); expect(consolidarDocumentos(extraerLineasAplicables(lineas), 0).map((d) => d.lineas.length)).toEqual([11, 1]); }); });