Files
devolucion_proveedores/libs/shared/src/domain/fifo.spec.ts
T
georgeandClaude Sonnet 5.5 287b187c14 Aplicación web de Devolución a Proveedores para SAP Business One
Reemplaza al addon .NET Framework (SolucionLibros). Genera Devoluciones de mercancías
enlazadas a sus Entradas por compra a partir de un listado pegado desde Excel.

- libs/shared (@dp/shared): modelos y lógica de dominio pura (FIFO, consolidación por
  proveedor, parseo del portapapeles), portada del addon.
- apps/api (NestJS): cliente del Service Layer con cola por sesión, login con la sesión de
  SAP, metadatos (UDT/UDF/UDO) y botón Reconstruir, catálogos, planificaciones (pegar,
  procesar, reprocesar, marcar líneas, crear Devoluciones, cerrar), listado paginado con
  filtros y descarga de PDF de la Guía de despacho desde BES. Sirve también la web
  compilada, con HTTPS opcional.
- apps/web (Angular): login, Configuración → Metadatos y Planificaciones (listado,
  nueva, detalle).
- deploy/windows y scripts/publicar.mjs: despliegue como servicio de Windows con WinSW.

Verificado contra el Service Layer de SQL Server. Sin probar aún: SAP HANA y la
instalación del servicio en Windows (ver CLAUDE.md, sección "Sin verificar").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 08:09:07 -05:00

83 lines
3.8 KiB
TypeScript

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]);
});
});