feat(web): el almacén y el impuesto elegidos en la primera línea se copian a las de abajo

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 14:39:16 -05:00
1 parent 155d18fce4
commit 43cba0cc3e
4 files changed
+53 -1

No files matched your search

@@ -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',
@@ -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). */
@@ -246,6 +246,46 @@ describe('LinesGrid', () => {
expect(lines()[0]).toMatchObject({ Quantity: 2, Price: 50 });
});
async function pickFromList(buttonIndex: number, url: string, row: Record<string, unknown>) {
(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' });
+9 -1
View File
@@ -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 -----------------------------------------------------------------------------