From 43cba0cc3e967301ce536f02fae2bcafd13c1b3d Mon Sep 17 00:00:00 2001 From: George Date: Wed, 14 Oct 2026 14:39:16 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20el=20almac=C3=A9n=20y=20el=20impue?= =?UTF-8?q?sto=20elegidos=20en=20la=20primera=20l=C3=ADnea=20se=20copian?= =?UTF-8?q?=20a=20las=20de=20abajo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../app/features/documents/line-columns.ts | 2 + apps/web/src/app/shared/grid/grid.types.ts | 2 + .../src/app/shared/grid/lines-grid.spec.ts | 40 +++++++++++++++++++ apps/web/src/app/shared/grid/lines-grid.ts | 10 ++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/features/documents/line-columns.ts b/apps/web/src/app/features/documents/line-columns.ts index 7b1739b..70c9554 100644 --- a/apps/web/src/app/features/documents/line-columns.ts +++ b/apps/web/src/app/features/documents/line-columns.ts @@ -75,6 +75,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [ title: 'Almacén', width: '90px', mono: true, + propagate: true, lookup: { config: { title: 'Almacenes', @@ -95,6 +96,7 @@ export const DOCUMENT_LINE_COLUMNS: readonly GridColumn[] = [ title: 'Impuesto', width: '90px', mono: true, + propagate: true, lookup: { config: { title: 'Códigos de impuesto', diff --git a/apps/web/src/app/shared/grid/grid.types.ts b/apps/web/src/app/shared/grid/grid.types.ts index 7b6eac6..165e883 100644 --- a/apps/web/src/app/shared/grid/grid.types.ts +++ b/apps/web/src/app/shared/grid/grid.types.ts @@ -24,6 +24,8 @@ export interface GridColumn { /** No editable (siempre, o según la línea). */ readonly?: boolean | ((line: GridLine) => boolean); required?: boolean; + /** Lo elegido en la primera fila se copia a las filas de abajo (almacén, impuesto); desde la 2.ª fila no se replica. */ + propagate?: boolean; /** Celda con botón "…" / F4 que abre una lista de valores. */ lookup?: GridLookup; /** Texto calculado a mostrar (columnas de solo lectura). */ diff --git a/apps/web/src/app/shared/grid/lines-grid.spec.ts b/apps/web/src/app/shared/grid/lines-grid.spec.ts index fbaadce..cdc4ed5 100644 --- a/apps/web/src/app/shared/grid/lines-grid.spec.ts +++ b/apps/web/src/app/shared/grid/lines-grid.spec.ts @@ -246,6 +246,46 @@ describe('LinesGrid', () => { expect(lines()[0]).toMatchObject({ Quantity: 2, Price: 50 }); }); + async function pickFromList(buttonIndex: number, url: string, row: Record) { + (qa('.pick button')[buttonIndex] as HTMLButtonElement).click(); + await tick(); + ctrl.expectOne((r) => r.url === url).flush({ value: [row], 'odata.count': '1' }); + await tick(); + (q('.dlg tbody tr') as HTMLElement).click(); + await tick(); + } + + describe('almacén e impuesto: lo elegido en la 1.ª fila se copia hacia abajo', () => { + beforeEach(async () => { + fixture.componentInstance.lines.set([line('A'), line('B'), line('C')]); + await tick(); + }); + + it('el almacén elegido en la primera fila llega a todas las de abajo', async () => { + await pickFromList(1, '/api/sl/Warehouses', { WarehouseCode: '01', WarehouseName: 'Central' }); + expect(lines().map((l) => l['WarehouseCode'])).toEqual(['01', '01', '01']); + expect(lines()[2]).toMatchObject({ ItemCode: 'C', Quantity: 1 }); + }); + + it('el impuesto elegido en la primera fila llega a todas las de abajo y recalcula', async () => { + await pickFromList(2, '/api/sl/SalesTaxCodes', { Code: 'IVA', Name: 'IVA 19%', Rate: 19 }); + expect(lines().map((l) => l['TaxCode'])).toEqual(['IVA', 'IVA', 'IVA']); + expect(lines()[1]).toMatchObject({ LineTotal: 100 }); + }); + + it('elegir el almacén en la segunda fila no se replica', async () => { + await pickFromList(4, '/api/sl/Warehouses', { WarehouseCode: '02', WarehouseName: 'Norte' }); + expect(lines().map((l) => l['WarehouseCode'])).toEqual(['', '02', '']); + }); + + it('lo que ya tenían las filas de abajo se sustituye al elegir en la primera', async () => { + fixture.componentInstance.lines.set([line('A'), { ...line('B'), WarehouseCode: '09' }]); + await tick(); + await pickFromList(1, '/api/sl/Warehouses', { WarehouseCode: '01', WarehouseName: 'Central' }); + expect(lines().map((l) => l['WarehouseCode'])).toEqual(['01', '01']); + }); + }); + 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' }); diff --git a/apps/web/src/app/shared/grid/lines-grid.ts b/apps/web/src/app/shared/grid/lines-grid.ts index 330d239..3bcb7f3 100644 --- a/apps/web/src/app/shared/grid/lines-grid.ts +++ b/apps/web/src/app/shared/grid/lines-grid.ts @@ -141,7 +141,15 @@ export class LinesGrid { private replace(row: number, line: GridLine, key: string): void { const next = this.recalc()?.(line, key) ?? line; - this.lines.update((ls) => ls.map((l, i) => (i === row ? next : l))); + const spread = row === 0 && this.columns().some((c) => c.key === key && c.propagate) && next[key] !== this.lines()[0]?.[key]; + this.lines.update((ls) => + ls.map((l, i) => { + if (i === row) return next; + if (!spread || i === 0) return l; + const copy = { ...l, [key]: next[key] }; + return this.recalc()?.(copy, key) ?? copy; + }), + ); } // --- filas -----------------------------------------------------------------------------