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 { copyFromDocument } from './document-copy'; import { DocumentFormPage } from './document-form.page'; import { createPayload, linesPayload } from './document-model'; import { DOCUMENT_SCREENS } from './document-screens'; import { SERIES_SUBTYPE, objectLabel } from './document-objects'; import { headerKeysOf } from './document-layouts'; const SERIES = { value: [ { Document: '17', DocumentSubType: '--', Name: 'Primario', Series: 10, Locked: 'tNO' }, { Document: '17', DocumentSubType: '--', Name: 'Web', Series: 11, Locked: 'tNO' }, { Document: '17', DocumentSubType: '--', Name: 'Bloqueada', Series: 12, Locked: 'tYES' }, { Document: '17', DocumentSubType: 'IB', Name: 'Boletas', Series: 20, Locked: 'tNO' }, ], }; describe('series, indicador y proyecto', () => { let fixture: ComponentFixture; 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(); }; const isSeries = (r: { url: string }) => r.url.includes('SeriesService_GetDocumentSeries'); async function open(path: string, doc?: Record) { const screen = DOCUMENT_SCREENS.find((s) => s.path === path)!; const params = doc ? { docEntry: '5' } : {}; const route = { snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap(params) }, paramMap: of(convertToParamMap(params)), }; 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(); return screen; } const answerCatalogs = async (resource: string, doc?: Record) => { ctrl.match((r) => r.url === '/api/sl/SalesTaxCodes').forEach((r) => r.flush({ value: [{ Code: 'IVA', Rate: 19 }] })); ctrl.match((r) => r.url === '/api/sl/AdditionalExpenses').forEach((r) => r.flush({ value: [] })); ctrl.match(isSeries).forEach((r) => r.flush(SERIES)); if (doc) ctrl.expectOne(`/api/sl/${resource}/5`).flush(doc); await tick(); }; beforeEach(() => TestBed.resetTestingModule()); it('pide las series del tipo de documento de la pantalla (órdenes de venta = 17)', async () => { await open('/ventas/ordenes'); const req = ctrl.expectOne(isSeries); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual({ DocumentTypeParams: { Document: '17' } }); await answerCatalogs('Orders'); }); it('ofrece solo las series del subtipo de la pantalla y sin bloquear', async () => { await open('/ventas/ordenes'); await answerCatalogs('Orders'); expect(page().seriesOptions().map((o: { label: string }) => o.label)).toEqual(['Primario', 'Web']); }); it('una pantalla de boletas usa las series de su subtipo', async () => { await open('/ventas/boletas'); await answerCatalogs('Invoices'); expect(page().seriesOptions().map((o: { label: string }) => o.label)).toEqual(['Boletas']); }); it('un documento nuevo envía la serie, el indicador y el proyecto elegidos', async () => { await open('/ventas/ordenes'); await answerCatalogs('Orders'); page().setHeader('CardCode', 'C1'); page().setHeader('Series', 11); page().setHeader('Indicator', '01'); page().setHeader('Project', 'PE001'); page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 50, LineTotal: 50 }]); await tick(); (el().querySelector('.actions .btn.main') as HTMLElement).click(); await tick(); const body = ctrl.expectOne((r) => r.method === 'POST' && !isSeries(r)).request.body as Record; expect(body).toMatchObject({ Series: 11, Indicator: '01', Project: 'PE001' }); }); it('sin elegir serie no la envía (SAP usa la por defecto), y sin indicador ni proyecto tampoco', async () => { await open('/ventas/ordenes'); await answerCatalogs('Orders'); page().setHeader('CardCode', 'C1'); page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 50, LineTotal: 50 }]); await tick(); (el().querySelector('.actions .btn.main') as HTMLElement).click(); await tick(); const body = ctrl.expectOne((r) => r.method === 'POST' && !isSeries(r)).request.body as Record; for (const k of ['Series', 'Indicator', 'Project']) expect(body).not.toHaveProperty(k); }); it('en un documento guardado la serie no se cambia y se muestra aunque esté bloqueada', async () => { await open('/ventas/ordenes', { DocEntry: 5 }); await answerCatalogs('Orders', { 'odata.etag': 'W/"1"', DocEntry: 5, DocNum: 316, Series: 12, CardCode: 'C1', CardName: 'ACME', Indicator: '02', Project: 'SUD001', DocDate: '2026-09-11T00:00:00Z', DocDueDate: '2026-09-20T00:00:00Z', DocumentStatus: 'bost_Open', Cancelled: 'tNO', DocumentLines: [], }); const select = el().querySelector('select[aria-label="Serie de numeración"]')!; expect(select.disabled).toBe(true); expect(select.selectedOptions[0]?.textContent?.trim()).toBe('Bloqueada'); const values = [...el().querySelectorAll('input')].map((i) => i.value); expect(values).toEqual(expect.arrayContaining(['316', '02', 'SUD001'])); expect(page().dirty()).toBe(false); }); it('todos los formularios de documento llevan serie, indicador y proyecto', () => { for (const s of DOCUMENT_SCREENS) { expect(headerKeysOf(s.layout), s.path).toEqual(expect.arrayContaining(['Series', 'Indicator', 'Project'])); } }); it('el subtipo de las series de las boletas y facturas de SAP', () => { expect(SERIES_SUBTYPE['bod_None']).toBe('--'); expect(SERIES_SUBTYPE['bod_Bill']).toBe('IB'); }); }); describe('qué precio se envía a SAP', () => { const lines = DOCUMENT_SCREENS.find((s) => s.path === '/ventas/ordenes')!.layout.lines; it('si el usuario escribió el precio, se envía solo el precio', () => { expect(linesPayload([{ ItemCode: 'A', Price: 100, PriceAfterVAT: 119, _priceBasis: 'net' }], lines)).toEqual([{ ItemCode: 'A', Price: 100 }]); }); it('si escribió el precio con IVA, se envía solo ese', () => { expect(linesPayload([{ ItemCode: 'A', Price: 100, PriceAfterVAT: 119, _priceBasis: 'gross' }], lines)).toEqual([{ ItemCode: 'A', PriceAfterVAT: 119 }]); }); it('una línea cargada de SAP (sin marca) envía el precio, igual que antes', () => { expect(linesPayload([{ ItemCode: 'A', Price: 100, PriceAfterVAT: 119 }], lines)).toEqual([{ ItemCode: 'A', Price: 100 }]); }); it('las dimensiones y el proyecto de la línea se envían; la referencia base solo si viene copiada', () => { const body = createPayload({}, [{ ItemCode: 'A', Quantity: 1, CostingCode: 'CC1', CostingCode3: 'CC3', ProjectCode: 'PE001', BaseType: -1, BaseEntry: null }], lines); expect(body['DocumentLines']).toEqual([{ ItemCode: 'A', Quantity: 1, CostingCode: 'CC1', CostingCode3: 'CC3', ProjectCode: 'PE001' }]); }); }); describe('inventario y solicitudes de compra', () => { const by = (path: string) => DOCUMENT_SCREENS.find((s) => s.path === path)!.layout; const ANALYTIC = ['CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode']; it('llevan las cinco dimensiones y el proyecto en el detalle, y se envían', () => { for (const p of ['/inventario/entradas', '/inventario/salidas', '/inventario/transferencias', '/inventario/solicitudes-traslado', '/compras/solicitudes']) { const l = by(p); expect(l.columns.map((c) => c.key), p).toEqual(expect.arrayContaining(ANALYTIC)); expect(l.lines.fields, p).toEqual(expect.arrayContaining(ANALYTIC)); } }); it('no llevan precio con IVA, cuenta ni referencia base', () => { for (const p of ['/inventario/entradas', '/inventario/transferencias', '/compras/solicitudes']) { const keys = by(p).columns.map((c) => c.key); for (const k of ['PriceAfterVAT', 'AccountCode', 'BaseType', 'BaseEntry', 'BaseLine']) expect(keys, `${p} ${k}`).not.toContain(k); } }); }); describe('copiar conserva dimensiones y proyecto', () => { it('las líneas y la cabecera copiadas llevan las dimensiones, el proyecto y el indicador del origen', () => { const copied = copyFromDocument( { DocEntry: 315, DocNum: 315, CardCode: 'C1', Project: 'PE001', Indicator: '01', DocumentLines: [{ LineNum: 0, ItemCode: 'A', Quantity: 2, RemainingOpenQuantity: 2, CostingCode: 'CC1', CostingCode2: 'CC2', ProjectCode: 'SUD001' }], }, '/ventas/ordenes', 'Órdenes de venta', ); expect(copied.header).toMatchObject({ Project: 'PE001', Indicator: '01' }); expect(copied.lines[0]).toMatchObject({ CostingCode: 'CC1', CostingCode2: 'CC2', ProjectCode: 'SUD001', BaseType: 17, BaseEntry: 315, BaseLine: 0 }); }); it('el tipo base de una línea copiada se muestra con el nombre del documento', () => { expect(objectLabel(17)).toBe('Órdenes de venta'); expect(objectLabel(1470000113)).toBe('Solicitudes de compra'); expect(objectLabel(-1)).toBe(''); }); });