213 lines
9.8 KiB
TypeScript
213 lines
9.8 KiB
TypeScript
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<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();
|
|
};
|
|
const isSeries = (r: { url: string }) => r.url.includes('SeriesService_GetDocumentSeries');
|
|
|
|
async function open(path: string, doc?: Record<string, unknown>) {
|
|
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<string, unknown>) => {
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<HTMLSelectElement>('select[aria-label="Serie de numeración"]')!;
|
|
expect(select.disabled).toBe(true);
|
|
expect(select.selectedOptions[0]?.textContent?.trim()).toBe('Bloqueada');
|
|
const values = [...el().querySelectorAll<HTMLInputElement>('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('');
|
|
});
|
|
});
|