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
@@ -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'];
|
||||
|
||||
Reference in new issue
Block a user