feat: redondeo en el pie de los documentos de ventas y compras (visible según OADM.RoundMthd) y endpoint /api/company/settings
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6c2cb30d63
commit
e9e3ba03e5
19 files changed
+270
-17
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { CompanyService } from './company.service.js';
|
||||
|
||||
/** Ajustes de la empresa de SAP para la interfaz: cualquier sesión puede consultarlos. */
|
||||
@Controller('company')
|
||||
export class CompanyController {
|
||||
constructor(private readonly company: CompanyService) {}
|
||||
|
||||
@Get('settings')
|
||||
settings() {
|
||||
return this.company.settings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { vi } from 'vitest';
|
||||
import type { SapClientService } from '../sap/sap-client.service.js';
|
||||
import { CompanyService } from './company.service.js';
|
||||
|
||||
describe('CompanyService', () => {
|
||||
const make = (data: unknown) => {
|
||||
const request = vi.fn(async () => ({ data }));
|
||||
return { service: new CompanyService({ request } as unknown as SapClientService), request };
|
||||
};
|
||||
|
||||
it('entrega solo el redondeo y el separador, sin el resto de la información de la empresa', async () => {
|
||||
const { service, request } = make({ RoundingMethod: 'tYES', AccountSegmentsSeparator: '.', DefaultBankAccount: '192-1153365-0-50' });
|
||||
const settings = await service.settings();
|
||||
expect(settings).toEqual({ roundingEnabled: true, accountSegmentsSeparator: '.' });
|
||||
expect(request).toHaveBeenCalledWith({ method: 'POST', path: 'CompanyService_GetAdminInfo', body: {} });
|
||||
});
|
||||
|
||||
it('sin redondeo ni separador usa los valores por defecto', async () => {
|
||||
const { service } = make({ RoundingMethod: 'tNO', AccountSegmentsSeparator: null });
|
||||
expect(await service.settings()).toEqual({ roundingEnabled: false, accountSegmentsSeparator: '-' });
|
||||
});
|
||||
|
||||
it('recuerda la respuesta un rato y luego la vuelve a pedir', async () => {
|
||||
const { service, request } = make({ RoundingMethod: 'tYES' });
|
||||
await service.settings(1000);
|
||||
await service.settings(1000 + 60_000);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
await service.settings(1000 + 11 * 60_000);
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { CompanySettings } from '@basic-erp/sap-types';
|
||||
import { SapClientService } from '../sap/sap-client.service.js';
|
||||
|
||||
/** Cuánto se recuerdan los ajustes de la empresa: cambian casi nunca. */
|
||||
const TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
interface AdminInfo {
|
||||
RoundingMethod?: string;
|
||||
AccountSegmentsSeparator?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajustes de la empresa que necesita la interfaz. Se leen de `CompanyService_GetAdminInfo` y solo se entregan los
|
||||
* campos de `CompanySettings`: esa respuesta trae mucho más (cuentas bancarias, carpetas) que no debe salir.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CompanyService {
|
||||
private cached: { at: number; value: CompanySettings } | null = null;
|
||||
|
||||
constructor(private readonly sap: SapClientService) {}
|
||||
|
||||
async settings(now = Date.now()): Promise<CompanySettings> {
|
||||
if (this.cached && now - this.cached.at < TTL_MS) return this.cached.value;
|
||||
const res = await this.sap.request<AdminInfo>({ method: 'POST', path: 'CompanyService_GetAdminInfo', body: {} });
|
||||
const value: CompanySettings = {
|
||||
roundingEnabled: res.data?.RoundingMethod === 'tYES',
|
||||
accountSegmentsSeparator: res.data?.AccountSegmentsSeparator?.trim() || '-',
|
||||
};
|
||||
this.cached = { at: now, value };
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuditModule } from '../audit/audit.module.js';
|
||||
import { CompanyController } from '../company/company.controller.js';
|
||||
import { CompanyService } from '../company/company.service.js';
|
||||
import { PermissionsModule } from '../permissions/permissions.module.js';
|
||||
import { SapModule } from '../sap/sap.module.js';
|
||||
import { SlProxyController } from './sl-proxy.controller.js';
|
||||
@@ -7,7 +9,7 @@ import { SlProxyService } from './sl-proxy.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [SapModule, PermissionsModule, AuditModule],
|
||||
controllers: [SlProxyController],
|
||||
providers: [SlProxyService],
|
||||
controllers: [SlProxyController, CompanyController],
|
||||
providers: [SlProxyService, CompanyService],
|
||||
})
|
||||
export class SlModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import type { CompanySettings } from '@basic-erp/sap-types';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { API_URL } from '../api';
|
||||
import { setAccountSegmentSeparator } from '../sl/account-segments';
|
||||
|
||||
/**
|
||||
* Ajustes de la empresa de SAP (`GET /api/company/settings`): si usa redondeo en los documentos y cómo se separan
|
||||
* los segmentos de una cuenta. Se piden una vez por sesión; mientras no llegan, sin redondeo y con el separador `-`.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CompanySettingsService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private loading: Promise<void> | null = null;
|
||||
private readonly rounding = signal(false);
|
||||
|
||||
/** La empresa usa redondeo en los documentos (`OADM.RoundMthd`): solo entonces se ofrece la fila «Redondeo». */
|
||||
readonly roundingEnabled = this.rounding.asReadonly();
|
||||
|
||||
ensureLoaded(): Promise<void> {
|
||||
this.loading ??= firstValueFrom(this.http.get<CompanySettings>(`${API_URL}/company/settings`))
|
||||
.then((s) => {
|
||||
this.rounding.set(s.roundingEnabled);
|
||||
setAccountSegmentSeparator(s.accountSegmentsSeparator);
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = null; // se reintenta en la próxima pantalla
|
||||
});
|
||||
return this.loading;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,14 @@ import type { SlListResponse } from '@basic-erp/sap-types';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { API_URL } from '../api';
|
||||
|
||||
/** Separador de los segmentos del código de cuenta (`AccountSegmentsSeparator` de SAP; el Service Layer no lo ofrece a una sesión común). */
|
||||
/** Separador de los segmentos del código de cuenta por defecto; el de la empresa (`AccountSegmentsSeparator`) lo trae `/api/company/settings`. */
|
||||
export const ACCOUNT_SEGMENT_SEPARATOR = '-';
|
||||
|
||||
/** Separador de los segmentos que usa la empresa (hasta que se cargan sus ajustes, el de por defecto). */
|
||||
const separator = signal(ACCOUNT_SEGMENT_SEPARATOR);
|
||||
export const accountSegmentSeparator = (): string => separator();
|
||||
export const setAccountSegmentSeparator = (value: string): void => separator.set(value || ACCOUNT_SEGMENT_SEPARATOR);
|
||||
|
||||
/**
|
||||
* Parte un código con formato en sus segmentos y los une con el separador (`8911111010` con tamaños 6, 2 y 2 →
|
||||
* `891111-10-10`). Si no hay segmentos, o sus tamaños no suman el largo del código, se devuelve tal cual: así un
|
||||
@@ -27,7 +32,7 @@ export function formatSegments(code: string, sizes: readonly number[], separator
|
||||
const segmentSizes = signal<readonly number[]>([]);
|
||||
|
||||
/** Código con formato de una cuenta segmentada, con sus separadores. Lee una señal: las vistas se actualizan al cargar los segmentos. */
|
||||
export const formatAccountCode = (code: string): string => formatSegments(code, segmentSizes());
|
||||
export const formatAccountCode = (code: string): string => formatSegments(code, segmentSizes(), separator());
|
||||
|
||||
/** Carga una vez los tamaños de los segmentos del código de cuenta (`AccountSegmentations`). */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
||||
@@ -5,7 +5,7 @@ import { firstValueFrom } from 'rxjs';
|
||||
import type { ListRow } from '../../shared/list/list.types';
|
||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
||||
import { API_URL } from '../api';
|
||||
import { ACCOUNT_SEGMENT_SEPARATOR } from './account-segments';
|
||||
import { accountSegmentSeparator } from './account-segments';
|
||||
import { combineFilters, escapeODataString } from './odata';
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ export class SlLookupApi {
|
||||
const typed = code.trim();
|
||||
const quote = (f: string, v: string): string => `${f} eq '${escapeODataString(v)}'`;
|
||||
// el código con formato se puede escribir con o sin los separadores de segmento
|
||||
const variants = [...new Set([typed, typed.split(ACCOUNT_SEGMENT_SEPARATOR).join('')])];
|
||||
const variants = [...new Set([typed, typed.split(accountSegmentSeparator()).join('')])];
|
||||
const match = numeric
|
||||
? `${keyField} eq ${typed}`
|
||||
: [quote(keyField, typed), ...(lookup.alsoMatch ?? []).flatMap((f) => variants.map((v) => quote(f, v)))].join(' or ');
|
||||
|
||||
@@ -214,6 +214,25 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@case ('rounding') {
|
||||
@if (roundingEnabled()) {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-checkbox
|
||||
ariaLabel="Redondear el total del documento"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="header()['Rounding'] === 'tYES'"
|
||||
(valueChange)="setHeader('Rounding', $event ? 'tYES' : 'tNO')"
|
||||
/>
|
||||
<sbo-number
|
||||
ariaLabel="Importe del redondeo"
|
||||
[decimals]="2"
|
||||
[readonly]="readOnly() || locked(f) || header()['Rounding'] !== 'tYES'"
|
||||
[value]="+str(header()['RoundingDiffAmount'])"
|
||||
(valueChange)="setHeader('RoundingDiffAmount', $event ?? 0)"
|
||||
/>
|
||||
</sbo-field>
|
||||
}
|
||||
}
|
||||
@case ('computed') {
|
||||
<sbo-text-field [label]="f.label" readonly [value]="computedValue(f.source)" />
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('DocumentFormPage', () => {
|
||||
expect(el().querySelector('.badge.tone-ok')?.textContent?.trim()).toBe('Abierto');
|
||||
expect(el().querySelector('h1')?.textContent).toContain('1041');
|
||||
expect(el().querySelectorAll('tbody tr').length).toBeGreaterThanOrEqual(1);
|
||||
expect(page().totals()).toEqual({ subtotal: 200, discount: 0, net: 200, tax: 38, expenses: 0, total: 238 });
|
||||
expect(page().totals()).toEqual({ subtotal: 200, discount: 0, net: 200, tax: 38, expenses: 0, rounding: 0, total: 238 });
|
||||
expect(buttonText()).toContain('Actualizar');
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ import type { SlListResponse } from '@basic-erp/sap-types';
|
||||
import { map } from 'rxjs';
|
||||
import { API_URL, apiErrorMessage } from '../../core/api';
|
||||
import { formatNumber } from '../../core/format';
|
||||
import { CompanySettingsService } from '../../core/company/company-settings';
|
||||
import { SlDocumentApi } from '../../core/sl/sl-document';
|
||||
import { CheckboxField, DateField, LookupField, NumberField, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField } from '../../shared/form';
|
||||
import { CheckboxField, CheckboxInput, DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField } from '../../shared/form';
|
||||
import type { GridLine, GridMultiAdd } from '../../shared/grid/grid.types';
|
||||
import { LinesGrid } from '../../shared/grid/lines-grid';
|
||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
||||
@@ -71,7 +72,7 @@ const folioAsNumber = (h: DocRecord): DocRecord => {
|
||||
@Component({
|
||||
selector: 'sbo-document-form-page',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CheckboxField, DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, RouterLink, RowsTable, SboField, SelectField, SelectInput, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
|
||||
imports: [CheckboxField, CheckboxInput, DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, NumberInput, RouterLink, RowsTable, SboField, SelectField, SelectInput, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
|
||||
templateUrl: './document-form.page.html',
|
||||
styles: `
|
||||
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
||||
@@ -87,6 +88,8 @@ export class DocumentFormPage {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly api = inject(SlDocumentApi);
|
||||
/** La empresa usa redondeo (`OADM.RoundMthd`): solo entonces se ofrece la fila «Redondeo». */
|
||||
protected readonly roundingEnabled = inject(CompanySettingsService).roundingEnabled;
|
||||
|
||||
protected readonly title = this.route.snapshot.data['title'] as string;
|
||||
protected readonly screen = this.route.snapshot.data['screen'] as DocumentScreen;
|
||||
@@ -269,7 +272,7 @@ export class DocumentFormPage {
|
||||
taxRateMap(this.taxResource.hasValue() ? this.taxResource.value().value : []),
|
||||
);
|
||||
protected readonly totals = computed(() =>
|
||||
computeTotals(this.lines(), this.rates(), Number(this.header()['DiscountPercent'] ?? 0), this.expenses()),
|
||||
computeTotals(this.lines(), this.rates(), Number(this.header()['DiscountPercent'] ?? 0), this.expenses(), this.header()['Rounding'] === 'tYES' ? Number(this.header()['RoundingDiffAmount'] ?? 0) : 0),
|
||||
);
|
||||
protected readonly fmt = (n: number): string => formatNumber(n, 2);
|
||||
|
||||
@@ -512,6 +515,11 @@ export class DocumentFormPage {
|
||||
const d = this.doc();
|
||||
if (!d) {
|
||||
const header = folioAsNumber(this.layout.beforeCreate?.(this.header()) ?? this.header());
|
||||
// sin redondeo marcado no se envía nada: SAP calcula el suyo
|
||||
if (header['Rounding'] !== 'tYES') {
|
||||
delete header['Rounding'];
|
||||
delete header['RoundingDiffAmount'];
|
||||
}
|
||||
const body = createPayload(header, this.lines(), this.spec());
|
||||
if (this.hasExpenses && expensesPayload(this.expenses()).length) body[EXPENSES_COLLECTION] = expensesPayload(this.expenses());
|
||||
const created = await this.api.create(this.screen.resource, body);
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('columnas de línea por documento', () => {
|
||||
|
||||
it('un documento puede traer su cabecera completa en lugar de la base', () => {
|
||||
const l = marketingLayout({ ...base, header: { left: [], right: [] } });
|
||||
expect(headerKeysOf(l)).toEqual(['Comments', 'DiscountPercent']);
|
||||
expect(headerKeysOf(l)).toEqual(['Comments', 'DiscountPercent', 'Rounding', 'RoundingDiffAmount']);
|
||||
});
|
||||
|
||||
it('los demás documentos no cambian cuando uno elige columnas', () => {
|
||||
|
||||
@@ -44,6 +44,8 @@ export type HeaderField =
|
||||
/** Flete: código del gasto adicional y su importe (`DocumentAdditionalExpenses`). */
|
||||
/** Tabla de gastos adicionales del documento (flete, seguro…): una fila por gasto. */
|
||||
| { type: 'expenses'; label: string }
|
||||
/** Redondeo del documento (`Rounding` y `RoundingDiffAmount`): solo se ofrece si la empresa usa redondeo (`OADM.RoundMthd`). */
|
||||
| { type: 'rounding'; label: string }
|
||||
| { type: 'computed'; label: string; source: ComputedSource };
|
||||
|
||||
/** Las listas de valores (ventanas modales) de un formulario. */
|
||||
@@ -263,11 +265,12 @@ export function marketingLayout(opts: {
|
||||
{ type: 'number', key: 'DiscountPercent', label: 'Descuento %', decimals: 2 },
|
||||
{ type: 'computed', label: 'Gastos adicionales', source: 'expenses' },
|
||||
{ type: 'computed', label: 'Impuesto', source: 'tax' },
|
||||
{ type: 'rounding', label: 'Redondeo' },
|
||||
{ type: 'computed', label: 'Total del documento', source: 'total' },
|
||||
],
|
||||
},
|
||||
columns,
|
||||
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), DiscountPercent: 0 }),
|
||||
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
|
||||
newLine: newDocumentLine,
|
||||
recalc: recalcLine,
|
||||
service: {
|
||||
@@ -301,6 +304,8 @@ export const keysOfFields = (fields: readonly HeaderField[]): string[] =>
|
||||
return [f.prefixKey, f.numberKey];
|
||||
case 'expenses':
|
||||
return [];
|
||||
case 'rounding':
|
||||
return ['Rounding', 'RoundingDiffAmount'];
|
||||
default:
|
||||
return [f.key];
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ describe('computeTotals', () => {
|
||||
|
||||
it('suma líneas e impuesto por tasa', () => {
|
||||
const t = computeTotals([line(), line({ TaxCode: 'EXE', LineTotal: 100 })], rates, 0);
|
||||
expect(t).toEqual({ subtotal: 300, discount: 0, net: 300, tax: 38, expenses: 0, total: 338 });
|
||||
expect(t).toEqual({ subtotal: 300, discount: 0, net: 300, tax: 38, expenses: 0, rounding: 0, total: 338 });
|
||||
});
|
||||
|
||||
it('aplica el descuento de cabecera antes del impuesto', () => {
|
||||
const t = computeTotals([line()], rates, 10);
|
||||
expect(t).toEqual({ subtotal: 200, discount: 20, net: 180, tax: 34.2, expenses: 0, total: 214.2 });
|
||||
expect(t).toEqual({ subtotal: 200, discount: 20, net: 180, tax: 34.2, expenses: 0, rounding: 0, total: 214.2 });
|
||||
});
|
||||
|
||||
it('un código de impuesto desconocido no suma impuesto', () => {
|
||||
@@ -52,7 +52,7 @@ describe('computeTotals', () => {
|
||||
});
|
||||
|
||||
it('sin líneas, todo en cero', () => {
|
||||
expect(computeTotals([], rates, 0)).toEqual({ subtotal: 0, discount: 0, net: 0, tax: 0, expenses: 0, total: 0 });
|
||||
expect(computeTotals([], rates, 0)).toEqual({ subtotal: 0, discount: 0, net: 0, tax: 0, expenses: 0, rounding: 0, total: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface DocTotals {
|
||||
/** Total antes de impuestos. */
|
||||
net: number;
|
||||
tax: number;
|
||||
/** Redondeo del documento, si está marcado. */
|
||||
rounding: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -52,6 +54,7 @@ export function computeTotals(
|
||||
rates: ReadonlyMap<string, number>,
|
||||
discountPercent: number,
|
||||
expenseRows: readonly ExpenseRow[] = [],
|
||||
rounding = 0,
|
||||
): DocTotals {
|
||||
const factor = 1 - discountPercent / 100;
|
||||
let subtotal = 0;
|
||||
@@ -74,7 +77,8 @@ export function computeTotals(
|
||||
net: round2(net),
|
||||
tax: round2(tax),
|
||||
expenses: round2(expenses),
|
||||
total: round2(net + tax + expenses),
|
||||
rounding: round2(rounding),
|
||||
total: round2(net + tax + expenses + rounding),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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 { CompanySettingsService } from '../../core/company/company-settings';
|
||||
import { DocumentFormPage } from './document-form.page';
|
||||
import { DOCUMENT_SCREENS } from './document-screens';
|
||||
|
||||
describe('redondeo en el formulario', () => {
|
||||
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();
|
||||
};
|
||||
|
||||
/** Abre un documento nuevo; `roundingEnabled` es lo que la empresa responde en sus ajustes (`OADM.RoundMthd`). */
|
||||
async function open(roundingEnabled: boolean) {
|
||||
const screen = DOCUMENT_SCREENS.find((s) => s.resource === 'Orders')!;
|
||||
const route = { snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({}) }, paramMap: of(convertToParamMap({})) };
|
||||
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);
|
||||
const settings = TestBed.inject(CompanySettingsService);
|
||||
const loaded = settings.ensureLoaded();
|
||||
ctrl.expectOne('/api/company/settings').flush({ roundingEnabled, accountSegmentsSeparator: '-' });
|
||||
await loaded;
|
||||
fixture = TestBed.createComponent(DocumentFormPage);
|
||||
await tick();
|
||||
ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [{ Code: 'IVA', Rate: 19 }] });
|
||||
ctrl.expectOne((r) => r.url === '/api/sl/AdditionalExpenses').flush({ value: [] });
|
||||
await tick();
|
||||
}
|
||||
|
||||
const labels = () => [...el().querySelectorAll('label')].map((l) => l.textContent?.trim());
|
||||
|
||||
beforeEach(() => TestBed.resetTestingModule());
|
||||
|
||||
it('si la empresa redondea por moneda aparece la fila Redondeo', async () => {
|
||||
await open(true);
|
||||
expect(labels()).toContain('Redondeo');
|
||||
});
|
||||
|
||||
it('si el redondeo es por documento la fila no se ve, pero el campo sigue en el documento', async () => {
|
||||
await open(false);
|
||||
expect(labels()).not.toContain('Redondeo');
|
||||
expect(page().header()).toMatchObject({ Rounding: 'tNO', RoundingDiffAmount: 0 });
|
||||
});
|
||||
|
||||
it('el redondeo marcado se suma al total y se envía al crear', async () => {
|
||||
await open(true);
|
||||
page().setHeader('CardCode', 'C1');
|
||||
page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 100, TaxCode: 'IVA', LineTotal: 100 }]);
|
||||
page().setHeader('Rounding', 'tYES');
|
||||
page().setHeader('RoundingDiffAmount', 0.4);
|
||||
await tick();
|
||||
expect(page().totals()).toMatchObject({ rounding: 0.4, total: 119.4 });
|
||||
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||
await tick();
|
||||
const req = ctrl.expectOne((r) => r.method === 'POST' && !r.url.includes('SeriesService'));
|
||||
expect(req.request.body).toMatchObject({ Rounding: 'tYES', RoundingDiffAmount: 0.4 });
|
||||
req.flush({ DocEntry: 10, DocNum: 10 });
|
||||
await tick();
|
||||
});
|
||||
|
||||
it('sin marcar no se suma ni se envía', async () => {
|
||||
await open(true);
|
||||
page().setHeader('CardCode', 'C1');
|
||||
page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 100, TaxCode: 'IVA', LineTotal: 100 }]);
|
||||
page().setHeader('RoundingDiffAmount', 0.4); // el importe solo cuenta con el redondeo marcado
|
||||
await tick();
|
||||
expect(page().totals()).toMatchObject({ rounding: 0, total: 119 });
|
||||
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||
await tick();
|
||||
const body = ctrl.expectOne((r) => r.method === 'POST' && !r.url.includes('SeriesService')).request.body as Record<string, unknown>;
|
||||
expect(body).not.toHaveProperty('Rounding');
|
||||
expect(body).not.toHaveProperty('RoundingDiffAmount');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import { CompanySettingsService } from '../core/company/company-settings';
|
||||
import { AccountCodesService } from '../core/sl/account-codes';
|
||||
import { AccountSegmentsService } from '../core/sl/account-segments';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
@@ -27,6 +28,7 @@ export class MainLayout {
|
||||
|
||||
constructor() {
|
||||
void this.segments.ensureLoaded();
|
||||
void inject(CompanySettingsService).ensureLoaded();
|
||||
}
|
||||
private readonly collapsed = signal<ReadonlySet<string>>(new Set());
|
||||
|
||||
|
||||
Reference in new issue
Block a user