feat(web): el selector de artículo de una línea admite varios (el primero completa la línea, los demás se insertan debajo)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 14:27:36 -05:00
1 parent 1528f7cb96
commit 45c0ca536e
5 files changed
+37 -1

No files matched your search

@@ -49,6 +49,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [
{ key: 'ItemName', title: 'Descripción' },
],
},
multiple: true,
apply: (item, line) => ({ ...line, ItemCode: item['ItemCode'], ItemDescription: item['ItemName'] ?? line['ItemDescription'] }),
},
},
@@ -8,6 +8,8 @@ export type GridLine = Record<string, unknown>;
export interface GridLookup {
config: VhLookup;
apply: (picked: ListRow, line: GridLine) => GridLine;
/** Permite marcar varias filas: la primera completa la línea actual y cada una de las demás crea una línea nueva. */
multiple?: boolean;
}
export interface GridColumn {
@@ -92,6 +92,7 @@
[baseFilter]="cfg.baseFilter"
[orderby]="cfg.orderby"
[rowKey]="cfg.valueKey"
[selection]="lookupMultiple() ? 'multiple' : 'single'"
(selected)="lookupPicked($event)"
(closed)="lookupClosed()"
/>
@@ -218,11 +218,34 @@ describe('LinesGrid', () => {
await tick();
(q('.dlg tbody tr') as HTMLElement).click();
await tick();
(q('.dlg footer .btn.main') as HTMLElement).click();
await tick();
expect(lines()[0]).toMatchObject({ ItemCode: 'I00002', ItemDescription: 'DVD+R Disc 50-Pack', Quantity: 2, Price: 50, LineTotal: 100 });
expect(el.querySelector('.dlg')).toBeNull();
expect(document.activeElement).toBe(cell(0, 0));
});
it('varios artículos: el primero completa la línea y los demás se insertan debajo', async () => {
fixture.componentInstance.lines.set([line('A', 2, 50), line('Z', 1, 10)]);
await tick();
(qa('.pick button')[0] as HTMLButtonElement).click();
await tick();
ctrl.expectOne((r) => r.url === '/api/sl/Items').flush({
value: [
{ ItemCode: 'I1', ItemName: 'Uno' },
{ ItemCode: 'I2', ItemName: 'Dos' },
],
'odata.count': '2',
});
await tick();
qa('.dlg tbody tr').forEach((r) => (r as HTMLElement).click());
await tick();
(q('.dlg footer .btn.main') as HTMLElement).click();
await tick();
expect(lines().map((l) => l['ItemCode'])).toEqual(['I1', 'I2', 'Z']);
expect(lines()[0]).toMatchObject({ Quantity: 2, Price: 50 });
});
it('F4 abre la lista de la celda; una celda sin lista no', async () => {
await key(0, 3, 'F4'); // almacén
ctrl.expectOne((r) => r.url === '/api/sl/Warehouses').flush({ value: [], 'odata.count': '0' });
+10 -1
View File
@@ -55,6 +55,7 @@ export class LinesGrid {
protected readonly lookupTarget = signal<LookupTarget | null>(null);
protected readonly multiOpen = signal(false);
protected readonly lookup = computed(() => this.lookupTarget()?.column.lookup?.config);
protected readonly lookupMultiple = computed(() => !!this.lookupTarget()?.column.lookup?.multiple);
protected cellText(line: GridLine, column: GridColumn): string {
if (column.format) return column.format(line);
@@ -216,7 +217,15 @@ export class LinesGrid {
const picked = rows[0];
const line = target ? this.lines()[target.row] : undefined;
if (!target || !picked || !line || !target.column.lookup) return;
this.replace(target.row, target.column.lookup.apply(picked, line), target.column.key);
const lookup = target.column.lookup;
this.replace(target.row, lookup.apply(picked, line), target.column.key);
const rest = rows.slice(1).map((r) => {
const added = lookup.apply(r, this.newLine()());
return this.recalc()?.(added, target.column.key) ?? added;
});
if (rest.length) {
this.lines.update((ls) => [...ls.slice(0, target.row + 1), ...rest, ...ls.slice(target.row + 1)]);
}
}
protected lookupClosed(): void {