feat(web): lotes y series en las líneas de entradas, salidas y transferencias de stock
Una ventana por línea con tabla editable; validación de suma de lotes y una serie por unidad. Probado contra SBODemoCL: entrada de inventario 156 con SU01 y su lote. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5592fac76
commit
abc1cc4ac6
16 files changed
+752
-10
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createPayload } from './document-model';
|
||||
import { DOCUMENT_SCREENS } from './document-screens';
|
||||
import { BATCH_DETAIL, SERIAL_DETAIL, detailHint, detailRowsPayload, validateLineDetails } from './line-details';
|
||||
|
||||
describe('lotes y series de una línea', () => {
|
||||
it('el lote aplica si el artículo se gestiona por lote o la línea ya trae lotes; no si no', () => {
|
||||
expect(BATCH_DETAIL.applies({ _manageBatches: true })).toBe(true);
|
||||
expect(BATCH_DETAIL.applies({ BatchNumbers: [{ BatchNumber: 'L1', Quantity: 2 }] })).toBe(true);
|
||||
expect(BATCH_DETAIL.applies({ _manageBatches: false, BatchNumbers: [] })).toBe(false);
|
||||
expect(BATCH_DETAIL.applies({})).toBe(false);
|
||||
expect(SERIAL_DETAIL.applies({ _manageSerials: true })).toBe(true);
|
||||
expect(SERIAL_DETAIL.applies({ _manageBatches: true })).toBe(false);
|
||||
});
|
||||
|
||||
it('el botón resume lo indicado', () => {
|
||||
expect(BATCH_DETAIL.summary({ _manageBatches: true })).toBe('Indicar lotes');
|
||||
expect(BATCH_DETAIL.summary({ BatchNumbers: [{ BatchNumber: 'A', Quantity: 3 }, { BatchNumber: 'B', Quantity: 2 }] })).toBe('2 lotes · 5');
|
||||
expect(BATCH_DETAIL.summary({ BatchNumbers: [{ BatchNumber: 'A', Quantity: 1 }] })).toBe('1 lote · 1');
|
||||
expect(SERIAL_DETAIL.summary({ _manageSerials: true })).toBe('Indicar series');
|
||||
expect(SERIAL_DETAIL.summary({ SerialNumbers: [{ InternalSerialNumber: 'S1' }, { InternalSerialNumber: 'S2' }] })).toBe('2 series');
|
||||
});
|
||||
|
||||
it('el aviso compara las filas en edición con la cantidad de la línea', () => {
|
||||
expect(detailHint(BATCH_DETAIL, { Quantity: 10 }, [{ Quantity: 6 }, { Quantity: 4 }])).toEqual({ text: 'Cantidad de la línea: 10 · los lotes suman: 10', bad: false });
|
||||
expect(detailHint(BATCH_DETAIL, { Quantity: 10 }, [{ Quantity: 6 }]).bad).toBe(true);
|
||||
expect(detailHint(SERIAL_DETAIL, { Quantity: 2 }, [{ InternalSerialNumber: 'S1' }, { InternalSerialNumber: '' }])).toEqual({ text: 'Series: 1 de 2', bad: true });
|
||||
});
|
||||
|
||||
it('a SAP solo van los campos propios, sin vacíos ni filas sin lote, con fechas AAAA-MM-DD y cada serie con cantidad 1', () => {
|
||||
expect(
|
||||
detailRowsPayload('BatchNumbers', [
|
||||
{ BatchNumber: 'L1', Quantity: 4, ExpiryDate: '2027-01-31T00:00:00Z', Notes: 'x', SystemSerialNumber: 7, ManufacturingDate: '' },
|
||||
{ BatchNumber: ' ', Quantity: 9 },
|
||||
]),
|
||||
).toEqual([{ BatchNumber: 'L1', Quantity: 4, ExpiryDate: '2027-01-31' }]);
|
||||
expect(detailRowsPayload('SerialNumbers', [{ InternalSerialNumber: 'S1', ManufacturerSerialNumber: 'M1' }])).toEqual([
|
||||
{ InternalSerialNumber: 'S1', ManufacturerSerialNumber: 'M1', Quantity: 1 },
|
||||
]);
|
||||
expect(detailRowsPayload('Otro', [{ A: 1 }])).toEqual([]);
|
||||
expect(detailRowsPayload('BatchNumbers', undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
describe('validación', () => {
|
||||
it('un artículo por lote necesita lotes que sumen la cantidad', () => {
|
||||
expect(validateLineDetails([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true }])).toEqual(['Línea 1: indique los lotes de SU01.']);
|
||||
expect(validateLineDetails([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true, BatchNumbers: [{ BatchNumber: 'L1', Quantity: 3 }] }])).toEqual([
|
||||
'Línea 1: los lotes suman 3 y la cantidad es 5.',
|
||||
]);
|
||||
expect(validateLineDetails([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true, BatchNumbers: [{ BatchNumber: 'L1', Quantity: 3 }, { BatchNumber: 'L2', Quantity: 2 }] }])).toEqual([]);
|
||||
});
|
||||
|
||||
it('un artículo por serie necesita una serie por unidad', () => {
|
||||
expect(validateLineDetails([{ ItemCode: 'A6', Quantity: 2, _manageSerials: true, SerialNumbers: [{ InternalSerialNumber: 'S1' }] }])).toEqual([
|
||||
'Línea 2: indique 2 número(s) de serie de A6 (hay 1).'.replace('Línea 2', 'Línea 1'),
|
||||
]);
|
||||
expect(validateLineDetails([{ ItemCode: 'A6', Quantity: 2, _manageSerials: true, SerialNumbers: [{ InternalSerialNumber: 'S1' }, { InternalSerialNumber: 'S2' }] }])).toEqual([]);
|
||||
});
|
||||
|
||||
it('las líneas sin lotes ni series, y las vacías, no se validan; se numera solo lo real', () => {
|
||||
expect(validateLineDetails([{ ItemCode: '', Quantity: 1 }, { ItemCode: 'X', Quantity: 1 }])).toEqual([]);
|
||||
expect(validateLineDetails([{ ItemCode: '', Quantity: 1 }, { ItemCode: 'SU01', Quantity: 1, _manageBatches: true }])).toEqual(['Línea 1: indique los lotes de SU01.']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('qué documentos los llevan', () => {
|
||||
const lines = (p: string) => DOCUMENT_SCREENS.find((s) => s.path === p)!.layout;
|
||||
it('entradas, salidas y transferencias de stock sí; solicitudes de traslado, de compra y ventas no', () => {
|
||||
for (const p of ['/inventario/entradas', '/inventario/salidas', '/inventario/transferencias']) {
|
||||
expect(lines(p).details?.map((d) => d.key), p).toEqual(['BatchNumbers', 'SerialNumbers']);
|
||||
expect(lines(p).lines.fields, p).toEqual(expect.arrayContaining(['BatchNumbers', 'SerialNumbers']));
|
||||
}
|
||||
for (const p of ['/inventario/solicitudes-traslado', '/compras/solicitudes', '/ventas/ordenes']) {
|
||||
expect(lines(p).details, p).toBeUndefined();
|
||||
expect(lines(p).lines.fields, p).not.toContain('BatchNumbers');
|
||||
}
|
||||
});
|
||||
|
||||
it('los lotes viajan dentro de la línea, en la colección que espera SAP', () => {
|
||||
const entry = lines('/inventario/entradas').lines;
|
||||
const body = createPayload({}, [{ ItemCode: 'SU01', Quantity: 5, WarehouseCode: '01', _manageBatches: true, BatchNumbers: [{ BatchNumber: 'L1', Quantity: 5, ExpiryDate: '2027-01-01' }] }], entry);
|
||||
expect(body['DocumentLines']).toEqual([
|
||||
{ ItemCode: 'SU01', Quantity: 5, WarehouseCode: '01', BatchNumbers: [{ BatchNumber: 'L1', Quantity: 5, ExpiryDate: '2027-01-01' }] },
|
||||
]);
|
||||
const transfer = createPayload({}, [{ ItemCode: 'A6', Quantity: 1, FromWarehouseCode: '01', WarehouseCode: '02', SerialNumbers: [{ InternalSerialNumber: 'S1' }] }], lines('/inventario/transferencias').lines);
|
||||
expect(transfer['StockTransferLines']).toEqual([
|
||||
{ ItemCode: 'A6', Quantity: 1, FromWarehouseCode: '01', WarehouseCode: '02', SerialNumbers: [{ InternalSerialNumber: 'S1', Quantity: 1 }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('la marca interna de «gestiona lotes» nunca se envía', () => {
|
||||
const body = createPayload({}, [{ ItemCode: 'A', Quantity: 1, WarehouseCode: '01', _manageBatches: true, _manageSerials: false }], lines('/inventario/entradas').lines);
|
||||
expect(JSON.stringify(body)).not.toContain('_manage');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user