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
@@ -490,3 +490,93 @@ describe('LinesGrid: alineación de las columnas', () => {
|
||||
expect(cells).toEqual(['left', 'right', 'right', 'center', 'left']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinesGrid: detalles de línea (lotes)', () => {
|
||||
const BATCHES = {
|
||||
key: 'BatchNumbers',
|
||||
title: 'Lotes',
|
||||
applies: (l: GridLine) => l['_manageBatches'] === true || (Array.isArray(l['BatchNumbers']) && l['BatchNumbers'].length > 0),
|
||||
columns: [
|
||||
{ key: 'BatchNumber', title: 'Lote', type: 'text' as const },
|
||||
{ key: 'Quantity', title: 'Cantidad', type: 'number' as const },
|
||||
],
|
||||
summary: (l: GridLine) => (Array.isArray(l['BatchNumbers']) && l['BatchNumbers'].length ? `${l['BatchNumbers'].length} lote(s)` : 'Indicar lotes'),
|
||||
};
|
||||
|
||||
@Component({
|
||||
imports: [LinesGrid],
|
||||
template: `<sbo-lines-grid [columns]="columns" [(lines)]="lines" [readonly]="readonly()" [newLine]="newLine" [details]="details" />`,
|
||||
})
|
||||
class DetailHost {
|
||||
columns = [{ key: 'ItemCode', title: 'Artículo' }, { key: 'Quantity', title: 'Cantidad', type: 'number' as const }];
|
||||
lines = signal<readonly GridLine[]>([]);
|
||||
readonly = signal(false);
|
||||
newLine = () => ({});
|
||||
details = [BATCHES];
|
||||
}
|
||||
|
||||
async function make(lines: GridLine[], readonly = false) {
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({ imports: [DetailHost], providers: [provideHttpClient(), provideHttpClientTesting()] }).compileComponents();
|
||||
const fixture = TestBed.createComponent(DetailHost);
|
||||
fixture.componentInstance.lines.set(lines);
|
||||
fixture.componentInstance.readonly.set(readonly);
|
||||
const tick = async () => {
|
||||
fixture.detectChanges();
|
||||
await new Promise((r) => setTimeout(r));
|
||||
fixture.detectChanges();
|
||||
};
|
||||
await tick();
|
||||
return { fixture, el: fixture.nativeElement as HTMLElement, tick };
|
||||
}
|
||||
|
||||
it('solo las líneas a las que aplica el detalle muestran su botón', async () => {
|
||||
const { el } = await make([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true }, { ItemCode: 'A1', Quantity: 1 }]);
|
||||
const buttons = [...el.querySelectorAll<HTMLButtonElement>('.detbtn')];
|
||||
expect(buttons.map((b) => b.textContent?.trim())).toEqual(['Indicar lotes']);
|
||||
expect(el.querySelector('th.det')?.textContent).toContain('Lotes / Series');
|
||||
});
|
||||
|
||||
it('el botón abre la ventana, se añaden filas y al aceptar quedan en la línea', async () => {
|
||||
const { fixture, el, tick } = await make([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true }]);
|
||||
(el.querySelector('.detbtn') as HTMLElement).click();
|
||||
await tick();
|
||||
expect(el.querySelector('sbo-line-detail-dialog [role=dialog]')).toBeTruthy();
|
||||
(el.querySelector('sbo-line-detail-dialog footer .btn') as HTMLElement).click(); // + Añadir fila
|
||||
await tick();
|
||||
const inputs = el.querySelectorAll<HTMLInputElement>('sbo-line-detail-dialog tbody input');
|
||||
inputs[0].value = 'LT-9';
|
||||
inputs[0].dispatchEvent(new Event('change'));
|
||||
inputs[1].value = '5';
|
||||
inputs[1].dispatchEvent(new Event('change'));
|
||||
await tick();
|
||||
(el.querySelector('sbo-line-detail-dialog footer .btn.main') as HTMLElement).click(); // Aceptar
|
||||
await tick();
|
||||
expect(el.querySelector('sbo-line-detail-dialog')).toBeNull();
|
||||
expect(fixture.componentInstance.lines()[0]['BatchNumbers']).toEqual([{ BatchNumber: 'LT-9', Quantity: 5 }]);
|
||||
expect(el.querySelector('.detbtn')?.textContent?.trim()).toBe('1 lote(s)');
|
||||
});
|
||||
|
||||
it('cancelar descarta los cambios', async () => {
|
||||
const { fixture, el, tick } = await make([{ ItemCode: 'SU01', Quantity: 5, _manageBatches: true }]);
|
||||
(el.querySelector('.detbtn') as HTMLElement).click();
|
||||
await tick();
|
||||
(el.querySelector('sbo-line-detail-dialog footer .btn') as HTMLElement).click();
|
||||
await tick();
|
||||
const cancel = [...el.querySelectorAll<HTMLElement>('sbo-line-detail-dialog footer .btn')].find((b) => b.textContent?.trim() === 'Cancelar')!;
|
||||
cancel.click();
|
||||
await tick();
|
||||
expect(fixture.componentInstance.lines()[0]['BatchNumbers']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('en solo lectura se ven los lotes pero no se editan', async () => {
|
||||
const { el, tick } = await make([{ ItemCode: 'SU01', Quantity: 3, BatchNumbers: [{ BatchNumber: 'LT001', Quantity: 3 }] }], true);
|
||||
expect(el.querySelector('.detbtn')?.textContent?.trim()).toBe('1 lote(s)');
|
||||
(el.querySelector('.detbtn') as HTMLElement).click();
|
||||
await tick();
|
||||
const input = el.querySelector<HTMLInputElement>('sbo-line-detail-dialog tbody input')!;
|
||||
expect(input.value).toBe('LT001');
|
||||
expect(input.readOnly).toBe(true);
|
||||
expect(el.querySelector('sbo-line-detail-dialog footer .btn.main')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user