feat: moneda y tipo de cambio del documento (debajo del Nº de referencia) y ajustes de moneda local de la empresa
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a74c19a139
commit
b85383dda1
11 files changed
+229
-9
No files matched your search
@@ -9,15 +9,15 @@ describe('CompanyService', () => {
|
||||
};
|
||||
|
||||
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 { service, request } = make({ RoundingMethod: 'tYES', AccountSegmentsSeparator: '.', LocalCurrency: 'SOL', DefaultBankAccount: '192-1153365-0-50' });
|
||||
const settings = await service.settings();
|
||||
expect(settings).toEqual({ roundingEnabled: true, accountSegmentsSeparator: '.' });
|
||||
expect(settings).toEqual({ roundingEnabled: true, accountSegmentsSeparator: '.', localCurrency: 'SOL' });
|
||||
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: '-' });
|
||||
expect(await service.settings()).toEqual({ roundingEnabled: false, accountSegmentsSeparator: '-', localCurrency: '' });
|
||||
});
|
||||
|
||||
it('recuerda la respuesta un rato y luego la vuelve a pedir', async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ const TTL_MS = 10 * 60 * 1000;
|
||||
interface AdminInfo {
|
||||
RoundingMethod?: string;
|
||||
AccountSegmentsSeparator?: string | null;
|
||||
LocalCurrency?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,7 @@ export class CompanyService {
|
||||
const value: CompanySettings = {
|
||||
roundingEnabled: res.data?.RoundingMethod === 'tYES',
|
||||
accountSegmentsSeparator: res.data?.AccountSegmentsSeparator?.trim() || '-',
|
||||
localCurrency: res.data?.LocalCurrency?.trim() || '',
|
||||
};
|
||||
this.cached = { at: now, value };
|
||||
return value;
|
||||
|
||||
@@ -77,4 +77,11 @@ describe('recursos del Service Layer', () => {
|
||||
expect(r.actions).toEqual(['Cancel']);
|
||||
expect(r.write).toBe('/finanzas/asientos');
|
||||
});
|
||||
|
||||
it('el tipo de cambio es una acción de solo consulta y las formas de envío y de pago son catálogos', () => {
|
||||
const rate = SL_RESOURCE_MAP.get('SBOBobService_GetCurrencyRate')!;
|
||||
expect(rate.methods).toEqual(['POST']);
|
||||
expect(rate.write).toBeNull();
|
||||
for (const name of ['ShippingTypes', 'WizardPaymentMethods']) expect(SL_RESOURCE_MAP.get(name)!.methods, name).toEqual(['GET']);
|
||||
});
|
||||
});
|
||||
@@ -110,6 +110,11 @@ export const SL_RESOURCES: readonly SlResource[] = [
|
||||
catalog('AccountSegmentations', 'number'),
|
||||
// Series de numeración válidas de un tipo de documento.
|
||||
queryAction('SeriesService_GetDocumentSeries'),
|
||||
// Tipo de cambio de una moneda en una fecha (para documentos en moneda extranjera): solo consulta.
|
||||
queryAction('SBOBobService_GetCurrencyRate'),
|
||||
// Logística y finanzas de los documentos: formas de envío y métodos de pago.
|
||||
catalog('ShippingTypes', 'number'),
|
||||
catalog('WizardPaymentMethods', 'string'),
|
||||
// Gastos adicionales de un documento (el flete): se leen para elegir el código de gasto.
|
||||
catalog('AdditionalExpenses', 'number'),
|
||||
];
|
||||
|
||||
@@ -14,14 +14,18 @@ export class CompanySettingsService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private loading: Promise<void> | null = null;
|
||||
private readonly rounding = signal(false);
|
||||
private readonly local = signal('');
|
||||
|
||||
/** La empresa usa redondeo en los documentos (`OADM.RoundMthd`): solo entonces se ofrece la fila «Redondeo». */
|
||||
readonly roundingEnabled = this.rounding.asReadonly();
|
||||
/** Moneda local de la empresa (vacía mientras no se cargan los ajustes). */
|
||||
readonly localCurrency = this.local.asReadonly();
|
||||
|
||||
ensureLoaded(): Promise<void> {
|
||||
this.loading ??= firstValueFrom(this.http.get<CompanySettings>(`${API_URL}/company/settings`))
|
||||
.then((s) => {
|
||||
this.rounding.set(s.roundingEnabled);
|
||||
this.local.set(s.localCurrency);
|
||||
setAccountSegmentSeparator(s.accountSegmentsSeparator);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -36,6 +36,11 @@ export class SlDocumentApi {
|
||||
return firstValueFrom(this.http.post<unknown>(`${this.url(resource, docEntry)}/${action}`, {}));
|
||||
}
|
||||
|
||||
/** Tipo de cambio de una moneda en una fecha (`SBOBobService_GetCurrencyRate`): cuántas unidades de la moneda local vale una de ella. */
|
||||
currencyRate(currency: string, date: string): Promise<number> {
|
||||
return firstValueFrom(this.http.post<number>(`${API_URL}/sl/SBOBobService_GetCurrencyRate`, { Currency: currency, Date: date }));
|
||||
}
|
||||
|
||||
/** Crea el documento; SAP devuelve el documento creado (con `DocEntry` y `DocNum`). */
|
||||
create(resource: string, body: DocRecord): Promise<DocRecord> {
|
||||
return firstValueFrom(this.http.post<DocRecord>(this.url(resource), body));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
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('moneda y tipo de cambio del documento', () => {
|
||||
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() {
|
||||
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 loaded = TestBed.inject(CompanySettingsService).ensureLoaded();
|
||||
ctrl.expectOne('/api/company/settings').flush({ roundingEnabled: false, accountSegmentsSeparator: '-', localCurrency: 'SOL' });
|
||||
await loaded;
|
||||
fixture = TestBed.createComponent(DocumentFormPage);
|
||||
await tick();
|
||||
ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [] });
|
||||
ctrl.expectOne((r) => r.url === '/api/sl/AdditionalExpenses').flush({ value: [] });
|
||||
ctrl.expectOne((r) => r.url === '/api/sl/Currencies').flush({ value: [{ Code: 'SOL', Name: 'Sol' }, { Code: 'USD', Name: 'Dolar' }] });
|
||||
await tick();
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||
await tick();
|
||||
return ctrl.expectOne((r) => r.method === 'POST' && r.url === '/api/sl/Orders');
|
||||
};
|
||||
|
||||
beforeEach(() => TestBed.resetTestingModule());
|
||||
|
||||
it('la fila va debajo del Nº de referencia, con la moneda local primero', async () => {
|
||||
await open();
|
||||
const left = page().layout.header.left as { type: string; key?: string }[];
|
||||
const at = left.findIndex((f) => f.key === 'NumAtCard');
|
||||
expect(left[at + 1].type).toBe('currency');
|
||||
expect(page().currencyOptions().map((o: { value: string; label: string }) => o.label)).toEqual(['Moneda local', 'USD · Dolar']);
|
||||
expect(page().currencyValue()).toBe('SOL');
|
||||
expect(page().isLocalCurrency()).toBe(true);
|
||||
});
|
||||
|
||||
it('elegir otra moneda pide su tipo de cambio de la fecha del documento y se puede corregir', async () => {
|
||||
await open();
|
||||
page().onCurrency('USD');
|
||||
await tick();
|
||||
const req = ctrl.expectOne('/api/sl/SBOBobService_GetCurrencyRate');
|
||||
expect(req.request.body).toEqual({ Currency: 'USD', Date: page().header().DocDate });
|
||||
req.flush(3.431);
|
||||
await tick();
|
||||
expect(page().header().DocRate).toBe(3.431);
|
||||
expect(page().isLocalCurrency()).toBe(false);
|
||||
page().setHeader('DocRate', 3.5);
|
||||
expect(page().header().DocRate).toBe(3.5);
|
||||
});
|
||||
|
||||
it('volver a la moneda local limpia el tipo de cambio', async () => {
|
||||
await open();
|
||||
page().onCurrency('USD');
|
||||
ctrl.expectOne('/api/sl/SBOBobService_GetCurrencyRate').flush(3.4);
|
||||
await tick();
|
||||
page().onCurrency('SOL');
|
||||
expect(page().header().DocRate).toBe('');
|
||||
expect(page().isLocalCurrency()).toBe(true);
|
||||
});
|
||||
|
||||
it('crea en moneda extranjera enviando moneda y tipo de cambio, y en la local no envía ninguno', async () => {
|
||||
await open();
|
||||
page().setHeader('CardCode', 'C1');
|
||||
page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 10, LineTotal: 10 }]);
|
||||
page().onCurrency('USD');
|
||||
ctrl.expectOne('/api/sl/SBOBobService_GetCurrencyRate').flush(3.431);
|
||||
await tick();
|
||||
let req = await save();
|
||||
expect(req.request.body).toMatchObject({ DocCurrency: 'USD', DocRate: 3.431 });
|
||||
req.flush({ DocEntry: 1, DocNum: 1 });
|
||||
await tick();
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
await open();
|
||||
page().setHeader('CardCode', 'C1');
|
||||
page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 10, LineTotal: 10 }]);
|
||||
req = await save();
|
||||
expect(req.request.body).not.toHaveProperty('DocCurrency');
|
||||
expect(req.request.body).not.toHaveProperty('DocRate');
|
||||
});
|
||||
|
||||
it('el socio con otra moneda hace que el documento la tome', async () => {
|
||||
await open();
|
||||
page().onPartner({ CardCode: 'C2', CardName: 'EXPORT', Currency: 'USD' });
|
||||
ctrl.expectOne('/api/sl/SBOBobService_GetCurrencyRate').flush(3.4);
|
||||
await tick();
|
||||
expect(page().currencyValue()).toBe('USD');
|
||||
page().onPartner({ CardCode: 'C3', CardName: 'TODAS', Currency: '##' });
|
||||
expect(page().currencyValue()).toBe('USD'); // «todas las monedas» no cambia nada
|
||||
});
|
||||
|
||||
it('en un documento guardado la moneda no se cambia', async () => {
|
||||
await open();
|
||||
const f = page().layout.header.left.find((x: { type: string }) => x.type === 'currency');
|
||||
expect(page().locked(f)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -248,6 +248,25 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@case ('currency') {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-select
|
||||
width="170px"
|
||||
ariaLabel="Moneda del documento"
|
||||
[options]="currencyOptions()"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="currencyValue()"
|
||||
(valueChange)="onCurrency('' + ($event ?? ''))"
|
||||
/>
|
||||
<sbo-number
|
||||
ariaLabel="Tipo de cambio"
|
||||
[decimals]="6"
|
||||
[readonly]="readOnly() || locked(f) || isLocalCurrency()"
|
||||
[value]="isLocalCurrency() ? null : +str(header()['DocRate'])"
|
||||
(valueChange)="setHeader('DocRate', $event ?? '')"
|
||||
/>
|
||||
</sbo-field>
|
||||
}
|
||||
@case ('rounding') {
|
||||
@if (roundingEnabled()) {
|
||||
<sbo-field [label]="f.label">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
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 { formatNumber, todayIso } from '../../core/format';
|
||||
import { CompanySettingsService } from '../../core/company/company-settings';
|
||||
import { SlDocumentApi } from '../../core/sl/sl-document';
|
||||
import { CheckboxField, CheckboxInput, DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField } from '../../shared/form';
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
validateLayout,
|
||||
} from './document-model';
|
||||
import { SERVICE_DOC_TYPE } from './document-layouts';
|
||||
import { type ComputedSource, type GroupCell, type HeaderField, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasExpenses, hasFolio, hasSeries, headerKeysOf, keysOfFields, requiredHeaderOf } from './document-layouts';
|
||||
import { type ComputedSource, type GroupCell, type HeaderField, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasCurrency, hasExpenses, hasFolio, hasSeries, headerKeysOf, keysOfFields, requiredHeaderOf } from './document-layouts';
|
||||
import { type CopyOrigin, copyFromDocument, copySources, copyTargets } from './document-copy';
|
||||
import { DOCUMENT_SCREENS, type DocumentScreen, docNum, documentStatus, menuLabel } from './document-screens';
|
||||
import { OBJECT_TYPES } from './document-objects';
|
||||
@@ -43,7 +43,7 @@ import { ITEMS_MULTI_ADD, LINE_COLUMNS } from './line-columns';
|
||||
import { RowsTable } from '../../shared/grid/rows-table';
|
||||
import type { DetailColumn } from '../../shared/grid/grid.types';
|
||||
|
||||
const today = (): string => new Date().toISOString().slice(0, 10);
|
||||
const today = (): string => todayIso();
|
||||
const DATE_KEYS = new Set(['DocDate', 'DocDueDate', 'RequriedDate', 'TaxDate', 'DueDate']);
|
||||
/** SAP devuelve las fechas como `2026-01-05T00:00:00Z`; el campo de fecha y el guardado usan `2026-01-05`. */
|
||||
const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) : '');
|
||||
@@ -89,7 +89,8 @@ export class DocumentFormPage {
|
||||
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;
|
||||
private readonly settings = inject(CompanySettingsService);
|
||||
protected readonly roundingEnabled = this.settings.roundingEnabled;
|
||||
|
||||
protected readonly title = this.route.snapshot.data['title'] as string;
|
||||
protected readonly screen = this.route.snapshot.data['screen'] as DocumentScreen;
|
||||
@@ -104,6 +105,7 @@ export class DocumentFormPage {
|
||||
private readonly hasFolio = hasFolio(this.layout);
|
||||
private readonly blankHeader: DocRecord = this.layout.newHeader();
|
||||
private readonly hasExpenses = hasExpenses(this.layout);
|
||||
private readonly hasCurrency = hasCurrency(this.layout);
|
||||
private pick(doc: DocRecord | undefined): DocRecord {
|
||||
return pickHeader(doc, this.headerKeys, this.blankHeader);
|
||||
}
|
||||
@@ -136,6 +138,39 @@ export class DocumentFormPage {
|
||||
() => new Map((this.expensesResource.hasValue() ? this.expensesResource.value().value : []).map((e) => [String(e['ExpensCode']), text(e['Name'])])),
|
||||
);
|
||||
|
||||
// --- moneda y tipo de cambio ---------------------------------------------------------------
|
||||
|
||||
private readonly currenciesResource = httpResource<SlListResponse<DocRecord>>(() =>
|
||||
this.hasCurrency ? { url: `${API_URL}/sl/Currencies`, params: { $select: 'Code,Name', $top: 100 } } : undefined,
|
||||
);
|
||||
/** Moneda local primero («Moneda local») y después las demás. */
|
||||
protected readonly currencyOptions = computed(() => {
|
||||
const local = this.settings.localCurrency();
|
||||
const others = (this.currenciesResource.hasValue() ? this.currenciesResource.value().value : [])
|
||||
.filter((c) => text(c['Code']) !== local)
|
||||
.map((c) => ({ value: text(c['Code']), label: `${text(c['Code'])} · ${text(c['Name'])}` }));
|
||||
return [{ value: local, label: 'Moneda local' }, ...others];
|
||||
});
|
||||
protected readonly currencyValue = computed(() => text(this.header()['DocCurrency']) || this.settings.localCurrency());
|
||||
/** El documento está en la moneda local (o la empresa aún no informa cuál es): no lleva tipo de cambio. */
|
||||
protected readonly isLocalCurrency = computed(() => !text(this.header()['DocCurrency']) || this.currencyValue() === this.settings.localCurrency());
|
||||
|
||||
protected onCurrency(code: string): void {
|
||||
this.header.update((h) => ({ ...h, DocCurrency: code, DocRate: '' }));
|
||||
this.message.set(null);
|
||||
if (code !== this.settings.localCurrency()) void this.fetchRate(code);
|
||||
}
|
||||
|
||||
/** El tipo de cambio de la moneda en la fecha del documento; se puede corregir a mano. */
|
||||
private async fetchRate(currency: string): Promise<void> {
|
||||
try {
|
||||
const rate = await this.api.currencyRate(currency, text(this.header()['DocDate']) || today());
|
||||
if (text(this.header()['DocCurrency']) === currency) this.header.update((h) => ({ ...h, DocRate: rate }));
|
||||
} catch (e) {
|
||||
this.message.set({ kind: 'warn', text: `No se pudo obtener el tipo de cambio de ${currency}: ${apiErrorMessage(e)}` });
|
||||
}
|
||||
}
|
||||
|
||||
/** Filas de gastos adicionales; se reinician al cargar o recargar el documento. */
|
||||
protected readonly expenses = linkedSignal<ExpenseRow[]>(() => expenseRowsOf(this.doc()));
|
||||
protected readonly expenseColumns: readonly DetailColumn[] = [
|
||||
@@ -393,7 +428,7 @@ export class DocumentFormPage {
|
||||
baseFilter: `CardType eq '${this.partner?.type ?? 'cCustomer'}'`,
|
||||
orderby: 'CardCode',
|
||||
columns: this.layout.lookups?.partner ?? PARTNER_COLUMNS,
|
||||
extraFields: ['FederalTaxID'],
|
||||
extraFields: ['FederalTaxID', 'Currency'],
|
||||
};
|
||||
|
||||
protected str = text;
|
||||
@@ -401,6 +436,8 @@ export class DocumentFormPage {
|
||||
private readonly lockedKeys = new Set(this.layout.lockedKeys ?? []);
|
||||
/** El campo se ve pero no se edita (una extensión de cliente lo dejó de solo lectura). */
|
||||
protected locked(f: HeaderField): boolean {
|
||||
// la moneda de un documento ya creado no se cambia
|
||||
if (f.type === 'currency' && !this.isNew()) return true;
|
||||
// el tipo (artículo / servicio) no se cambia en un documento ya creado
|
||||
if (f.type === 'select' && f.key === 'DocType' && !this.isNew()) return true;
|
||||
return this.lockedKeys.size > 0 && (keysOfFields([f]).some((k) => this.lockedKeys.has(k)) || (f.type === 'computed' && this.lockedKeys.has(f.source)));
|
||||
@@ -442,6 +479,8 @@ export class DocumentFormPage {
|
||||
if (key === 'DocType' && this.layout.service) return this.onDocType(String(value));
|
||||
this.header.update((h) => ({ ...h, [key]: value }));
|
||||
this.message.set(null);
|
||||
// otra fecha del documento, otro tipo de cambio
|
||||
if (key === 'DocDate' && this.isNew() && !this.isLocalCurrency()) void this.fetchRate(this.currencyValue());
|
||||
}
|
||||
|
||||
/** Tipo al que se quiere cambiar y que espera confirmación porque hay líneas escritas que se descartarían. */
|
||||
@@ -494,6 +533,9 @@ export class DocumentFormPage {
|
||||
protected onPartner(row: Record<string, unknown>): void {
|
||||
this.partnerName.set(text(row['CardName']));
|
||||
this.partnerTaxId.set(text(row['FederalTaxID']));
|
||||
// el documento toma la moneda del socio (si es una sola y no es la local), como en SAP
|
||||
const currency = text(row['Currency']);
|
||||
if (this.isNew() && this.hasCurrency && currency && currency !== '##' && currency !== this.currencyValue() && this.isLocalCurrency()) this.onCurrency(currency);
|
||||
// elegido el socio, el documento nuevo arranca con una línea lista para escribir
|
||||
if (this.isNew() && !this.readOnly() && this.lines().length === 0) this.lines.set([this.newLine()]);
|
||||
}
|
||||
@@ -535,6 +577,11 @@ export class DocumentFormPage {
|
||||
const d = this.doc();
|
||||
if (!d) {
|
||||
const header = folioAsNumber(this.layout.beforeCreate?.(this.header()) ?? this.header());
|
||||
// en la moneda local no se envía moneda ni tipo de cambio: SAP usa los suyos
|
||||
if (this.isLocalCurrency()) {
|
||||
delete header['DocCurrency'];
|
||||
delete header['DocRate'];
|
||||
}
|
||||
// sin redondeo marcado no se envía nada: SAP calcula el suyo
|
||||
if (header['Rounding'] !== 'tYES') {
|
||||
delete header['Rounding'];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { todayIso } from '../../core/format';
|
||||
import type { GridColumn, GridDetail, GridLine } from '../../shared/grid/grid.types';
|
||||
import type { ListColumn } from '../../shared/list/list.types';
|
||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
||||
@@ -59,6 +60,8 @@ export type HeaderField =
|
||||
| { type: 'expenses'; label: string }
|
||||
/** Redondeo del documento (`Rounding` y `RoundingDiffAmount`): solo se ofrece si la empresa usa redondeo (`OADM.RoundMthd`). */
|
||||
| { type: 'rounding'; label: string }
|
||||
/** Moneda del documento (`DocCurrency`) y su tipo de cambio (`DocRate`): el de la fecha del documento si no es la local. */
|
||||
| { type: 'currency'; label: string }
|
||||
| { type: 'computed'; label: string; source: ComputedSource };
|
||||
|
||||
/** Las listas de valores (ventanas modales) de un formulario. */
|
||||
@@ -114,7 +117,7 @@ export interface DocumentLayout {
|
||||
export const SERVICE_DOC_TYPE = 'dDocument_Service';
|
||||
export const ITEMS_DOC_TYPE = 'dDocument_Items';
|
||||
|
||||
export const today = (): string => new Date().toISOString().slice(0, 10);
|
||||
export const today = (): string => todayIso();
|
||||
|
||||
// --- catálogo de campos ---------------------------------------------------------------------
|
||||
|
||||
@@ -279,6 +282,7 @@ export function marketingLayout(opts: {
|
||||
{ type: 'computed', label: 'Nombre', source: 'partnerName' },
|
||||
{ type: 'computed', label: 'RUT', source: 'partnerTaxId' },
|
||||
FIELDS.numAtCard,
|
||||
{ type: 'currency', label: 'Moneda / Tipo de cambio' },
|
||||
...(opts.identity ? [] : [FIELDS.indicator]),
|
||||
FIELDS.project,
|
||||
...(opts.left ?? []),
|
||||
@@ -341,6 +345,8 @@ export const keysOfFields = (fields: readonly HeaderField[]): string[] =>
|
||||
return [];
|
||||
case 'rounding':
|
||||
return ['Rounding', 'RoundingDiffAmount'];
|
||||
case 'currency':
|
||||
return ['DocCurrency', 'DocRate'];
|
||||
default:
|
||||
return [f.key];
|
||||
}
|
||||
@@ -365,4 +371,6 @@ export const hasSeries = (layout: DocumentLayout): boolean => allFields(layout).
|
||||
|
||||
export const hasExpenses = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'expenses');
|
||||
|
||||
export const hasCurrency = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'currency');
|
||||
|
||||
export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio' || (f.type === 'group' && f.cells.some((c) => c.key === 'FolioNumber')));
|
||||
@@ -5,6 +5,8 @@
|
||||
export interface CompanySettings {
|
||||
/** La empresa usa redondeo en los documentos (`OADM.RoundMthd`, `RoundingMethod` en el Service Layer). */
|
||||
roundingEnabled: boolean;
|
||||
/** Moneda local de la empresa (`LocalCurrency`): la de los documentos que no llevan otra. */
|
||||
localCurrency: string;
|
||||
/** Separador de los segmentos de un código de cuenta (`AccountSegmentsSeparator`). */
|
||||
accountSegmentsSeparator: string;
|
||||
}
|
||||
Reference in new issue
Block a user