From b85383dda1eb1a55d2e7a0d256866d7baff46609 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 19 Oct 2026 14:40:14 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20moneda=20y=20tipo=20de=20cambio=20del?= =?UTF-8?q?=20documento=20(debajo=20del=20N=C2=BA=20de=20referencia)=20y?= =?UTF-8?q?=20ajustes=20de=20moneda=20local=20de=20la=20empresa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- apps/api/src/company/company.service.spec.ts | 6 +- apps/api/src/company/company.service.ts | 2 + apps/api/src/sl/sl-resources.spec.ts | 7 + apps/api/src/sl/sl-resources.ts | 5 + .../src/app/core/company/company-settings.ts | 4 + apps/web/src/app/core/sl/sl-document.ts | 5 + .../documents/document-currency.spec.ts | 121 ++++++++++++++++++ .../documents/document-form.page.html | 19 +++ .../features/documents/document-form.page.ts | 57 ++++++++- .../features/documents/document-layouts.ts | 10 +- libs/sap-types/src/company.ts | 2 + 11 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/features/documents/document-currency.spec.ts diff --git a/apps/api/src/company/company.service.spec.ts b/apps/api/src/company/company.service.spec.ts index 378b332..ae91c21 100644 --- a/apps/api/src/company/company.service.spec.ts +++ b/apps/api/src/company/company.service.spec.ts @@ -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 () => { diff --git a/apps/api/src/company/company.service.ts b/apps/api/src/company/company.service.ts index 5181360..09a290f 100644 --- a/apps/api/src/company/company.service.ts +++ b/apps/api/src/company/company.service.ts @@ -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; diff --git a/apps/api/src/sl/sl-resources.spec.ts b/apps/api/src/sl/sl-resources.spec.ts index ecf704c..58e52ab 100644 --- a/apps/api/src/sl/sl-resources.spec.ts +++ b/apps/api/src/sl/sl-resources.spec.ts @@ -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']); + }); }); diff --git a/apps/api/src/sl/sl-resources.ts b/apps/api/src/sl/sl-resources.ts index 54153d1..505424b 100644 --- a/apps/api/src/sl/sl-resources.ts +++ b/apps/api/src/sl/sl-resources.ts @@ -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'), ]; diff --git a/apps/web/src/app/core/company/company-settings.ts b/apps/web/src/app/core/company/company-settings.ts index e0ae51e..2a18458 100644 --- a/apps/web/src/app/core/company/company-settings.ts +++ b/apps/web/src/app/core/company/company-settings.ts @@ -14,14 +14,18 @@ export class CompanySettingsService { private readonly http = inject(HttpClient); private loading: Promise | 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 { this.loading ??= firstValueFrom(this.http.get(`${API_URL}/company/settings`)) .then((s) => { this.rounding.set(s.roundingEnabled); + this.local.set(s.localCurrency); setAccountSegmentSeparator(s.accountSegmentsSeparator); }) .catch(() => { diff --git a/apps/web/src/app/core/sl/sl-document.ts b/apps/web/src/app/core/sl/sl-document.ts index 83e0437..8c6d2f5 100644 --- a/apps/web/src/app/core/sl/sl-document.ts +++ b/apps/web/src/app/core/sl/sl-document.ts @@ -36,6 +36,11 @@ export class SlDocumentApi { return firstValueFrom(this.http.post(`${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 { + return firstValueFrom(this.http.post(`${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 { return firstValueFrom(this.http.post(this.url(resource), body)); diff --git a/apps/web/src/app/features/documents/document-currency.spec.ts b/apps/web/src/app/features/documents/document-currency.spec.ts new file mode 100644 index 0000000..51430f1 --- /dev/null +++ b/apps/web/src/app/features/documents/document-currency.spec.ts @@ -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; + 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); + }); +}); diff --git a/apps/web/src/app/features/documents/document-form.page.html b/apps/web/src/app/features/documents/document-form.page.html index 1aadcf0..1f99709 100644 --- a/apps/web/src/app/features/documents/document-form.page.html +++ b/apps/web/src/app/features/documents/document-form.page.html @@ -248,6 +248,25 @@ } } + @case ('currency') { + + + + + } @case ('rounding') { @if (roundingEnabled()) { diff --git a/apps/web/src/app/features/documents/document-form.page.ts b/apps/web/src/app/features/documents/document-form.page.ts index 118ce22..be328c2 100644 --- a/apps/web/src/app/features/documents/document-form.page.ts +++ b/apps/web/src/app/features/documents/document-form.page.ts @@ -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>(() => + 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 { + 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(() => 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): 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']; diff --git a/apps/web/src/app/features/documents/document-layouts.ts b/apps/web/src/app/features/documents/document-layouts.ts index 8cff85e..84a8b27 100644 --- a/apps/web/src/app/features/documents/document-layouts.ts +++ b/apps/web/src/app/features/documents/document-layouts.ts @@ -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'))); diff --git a/libs/sap-types/src/company.ts b/libs/sap-types/src/company.ts index 5a69704..71e3039 100644 --- a/libs/sap-types/src/company.ts +++ b/libs/sap-types/src/company.ts @@ -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; }