feat(web): extensiones por cliente (campos estándar adicionales y U_*) por módulo, y campo de selección

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 15:28:03 -05:00
1 parent 1f212e2b30
commit b3b47c34a3
12 files changed
+326 -10

No files matched your search

@@ -0,0 +1,156 @@
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { type ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angular/router';
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CLIENT_EXTENSIONS } from './client-extensions';
import { headerKeysOf } from './document-layouts';
import { DocumentFormPage } from './document-form.page';
import { BASE_SCREENS, DOCUMENT_SCREENS } from './document-screens';
import { type ScreenExtensions, applyExtensions } from './screen-extensions';
const by = (screens: readonly { path: string }[], path: string) => screens.find((s) => s.path === path)! as (typeof BASE_SCREENS)[number];
const EXT: ScreenExtensions = {
'/ventas/facturas': {
headerRight: [{ type: 'text', key: 'U_Obra', label: 'Obra' }],
footerLeft: [{ type: 'text', key: 'U_Nota', label: 'Nota interna' }],
lineColumns: [{ key: 'U_Lote', title: 'Lote', width: '90px' }],
listColumns: [{ key: 'U_Obra', title: 'Obra', searchable: true }],
},
'/inventario/entradas': { headerLeft: [{ type: 'text', key: 'U_Chofer', label: 'Chofer' }], lineColumns: [{ key: 'U_Origen', title: 'Origen' }] },
};
describe('extensiones por cliente', () => {
it('en main no agregan nada: las pantallas son las del base', () => {
expect(CLIENT_EXTENSIONS).toEqual({});
expect(DOCUMENT_SCREENS).toEqual(BASE_SCREENS);
});
it('agregan campos de cabecera y de pie al final y los envían a SAP', () => {
const s = by(applyExtensions(BASE_SCREENS, EXT), '/ventas/facturas');
expect(s.layout.header.right.at(-1)).toMatchObject({ key: 'U_Obra' });
expect(s.layout.footer.left.at(-1)).toMatchObject({ key: 'U_Nota' });
expect(headerKeysOf(s.layout)).toEqual(expect.arrayContaining(['U_Obra', 'U_Nota']));
});
it('agregan columnas de línea antes del total y las envían; y columnas de lista al final', () => {
const s = by(applyExtensions(BASE_SCREENS, EXT), '/ventas/facturas');
const keys = s.layout.columns.map((c) => c.key);
expect(keys.indexOf('U_Lote')).toBe(keys.indexOf('LineTotal') - 1);
expect(s.layout.lines.fields).toContain('U_Lote');
expect(s.columns.at(-1)).toMatchObject({ key: 'U_Obra' });
});
it('en un documento sin total las columnas van al final', () => {
const s = by(applyExtensions(BASE_SCREENS, EXT), '/inventario/entradas');
expect(s.layout.columns.at(-1)).toMatchObject({ key: 'U_Origen' });
});
it('solo cambia la pantalla extendida (aunque comparta layout con otra)', () => {
const out = applyExtensions(BASE_SCREENS, EXT);
expect(by(out, '/inventario/salidas')).toEqual(by(BASE_SCREENS, '/inventario/salidas'));
expect(by(out, '/ventas/ordenes')).toEqual(by(BASE_SCREENS, '/ventas/ordenes'));
expect(by(BASE_SCREENS, '/ventas/facturas').layout.columns.map((c) => c.key)).not.toContain('U_Lote');
});
it('una ruta que no existe es un error', () => {
expect(() => applyExtensions(BASE_SCREENS, { '/ventas/facturaz': { listColumns: [] } })).toThrow(/no existe la pantalla/);
});
});
describe('formulario con campos de cliente', () => {
let fixture: ComponentFixture<DocumentFormPage>;
let ctrl: HttpTestingController;
const el = () => fixture.nativeElement as HTMLElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const page = () => fixture.componentInstance as any;
const tick = async () => {
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
};
async function open(doc: Record<string, unknown>) {
const screen = by(
applyExtensions(BASE_SCREENS, {
'/ventas/ordenes': {
headerLeft: [
{ type: 'select', key: 'U_Canal', label: 'Canal', options: [{ value: 'W', label: 'Web' }, { value: 'T', label: 'Tienda' }] },
],
headerRight: [{ type: 'text', key: 'U_Obra', label: 'Obra' }],
lineColumns: [{ key: 'U_Lote', title: 'Lote' }],
},
}),
'/ventas/ordenes',
);
const route = {
snapshot: { data: { screen, title: 'Órdenes de venta' }, paramMap: convertToParamMap({ docEntry: '5' }) },
paramMap: of(convertToParamMap({ docEntry: '5' })),
};
await TestBed.configureTestingModule({
imports: [DocumentFormPage],
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
}).compileComponents();
ctrl = TestBed.inject(HttpTestingController);
vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
fixture = TestBed.createComponent(DocumentFormPage);
await tick();
ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [] });
ctrl.expectOne('/api/sl/Orders/5').flush(doc);
await tick();
}
const order = {
'odata.etag': 'W/"1"',
DocEntry: 5,
DocNum: 5,
CardCode: 'C1',
CardName: 'ACME',
DocDate: '2026-09-11T00:00:00Z',
DocDueDate: '2026-09-20T00:00:00Z',
DiscountPercent: 0,
DocumentStatus: 'bost_Open',
Cancelled: 'tNO',
U_Canal: 'W',
U_Obra: 'Torre 3',
DocumentLines: [{ LineNum: 0, ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 1, Price: 10, DiscountPercent: 0, TaxCode: '', LineTotal: 10, WarehouseCode: '01', U_Lote: 'L-77' }],
};
beforeEach(() => TestBed.resetTestingModule());
it('muestra el campo de usuario, el desplegable y la columna de línea con sus valores de SAP', async () => {
await open(order);
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).toEqual(expect.arrayContaining(['Canal', 'Obra']));
expect(el().querySelector<HTMLSelectElement>('select')!.value).toBe('W');
expect([...el().querySelectorAll<HTMLInputElement>('input')].map((i) => i.value)).toEqual(expect.arrayContaining(['Torre 3', 'L-77']));
});
it('al modificar envía solo el campo de usuario que cambió', async () => {
await open(order);
page().setHeader('U_Canal', 'T');
page().setHeader('U_Obra', 'Torre 4');
await tick();
(el().querySelector('.actions .btn.main') as HTMLElement).click();
await tick();
const req = ctrl.expectOne((r) => r.method === 'PATCH');
expect(req.request.body).toEqual({ U_Canal: 'T', U_Obra: 'Torre 4' });
req.flush(null);
await tick();
ctrl.expectOne('/api/sl/Orders/5').flush(order);
});
it('al cambiar la columna de línea envía las líneas con el campo de usuario', async () => {
await open(order);
page().setLines([{ ...page().lines()[0], U_Lote: 'L-80' }]);
await tick();
(el().querySelector('.actions .btn.main') as HTMLElement).click();
await tick();
const req = ctrl.expectOne((r) => r.method === 'PATCH');
expect((req.request.body as { DocumentLines: Record<string, unknown>[] }).DocumentLines[0]).toMatchObject({ ItemCode: 'A1', U_Lote: 'L-80' });
req.flush(null);
await tick();
ctrl.expectOne('/api/sl/Orders/5').flush(order);
});
});