Skip to content

Commit 194e66c

Browse files
feat(platform-objects): sys_exchange_rate — an organization's dated exchange rates, admin-written and audited, with a Setup list under Localization (#27246)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_ Fixes #27185 ⛔ not released alone — pairs with #27192 (R2); merges in the R2 window Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 新系统对象 `sys_exchange_rate`(币种 ISO 4217、汇率精确小数(小数点后 12 位、总共 24 位,裁决 6469382727)、生效日期、来源 manual/import/app),同组织内 (币种, 生效日期) 唯一;Setup → Configuration 在 Localization 下面加「Exchange Rates」列表页,页面上写明一次汇率方向;文档一行讲财务怎么录一个月的汇率。 - **为什么改:** 多币种 C′ 形状里写入时折算的唯一数据源(裁决 6465431966,参数 2/3 按默认;参数 4 由裁决 6469382727 定为 scale 12 / precision 24)。 - **风险与回滚:** 只新增一张表和一个菜单项,不改任何已有对象(另有三处 CI 强制的登记:spec 平台对象名单、CLI 升级仪式的组织归属清单、CLI 的 Setup 菜单条数断言);它唯一的读者是 #27192,⛔ 不单独发版,和 R2 同一次 Version Packages 合并。回滚 = revert 本 PR(新表无数据依赖)。 - **先看:** `packages/platform-objects/src/system/sys-exchange-rate.object.ts`(唯一键的 `'organization'` 拼法、`rate` 的 `scale`/`min` 与方向句);`src/plugin.ts`(注册进第一个 manifest,走项目库);`src/apps/setup-nav.contributions.ts`(菜单位置)。 - **两分钟验证:** `pnpm --filter @objectstack/platform-objects exec vitest run src/system/sys-exchange-rate.object.test.ts`,预期 15 个测试全绿。 - **需要你做的:** 无,席位落地(R2 窗口里与 #27192 一起合)。 ## What changes - **`sys_exchange_rate`** (`packages/platform-objects/src/system/sys-exchange-rate.object.ts`), `managedBy: 'system-data'`: - `currency` — text, `valueDomain: 'iso_4217_currency'`, required. - `rate` — `number` (the platform's exact-decimal column, never `float`), `scale: 12`, `precision: 24`, `min` = 10⁻¹² (strictly positive), required (ruling 6469382727). - `effective_date` — date, required. `source` — select `manual` / `import` / `app`, default `manual`. - Unique `(currency, effective_date)` spelled `unique: 'organization'` (ADR-0120 D1), so two organizations each hold their own EUR rate for a day. - No tenancy opt-out: `organization_id` is injected and the Layer 0 wall scopes it. - `enable.trackHistory: true` plus per-field `trackHistory` on the four ruled columns; the object is not on plugin-audit's skip list, so every write lands in `sys_audit_log`. - Who writes: `organization_admin`'s `'*'` grant reaches it (not `private`, no `requiredPermissions`); `member_default` names no business object, so members neither read nor write it (ruling parameter 2). - **Rate direction stated once**: `EXCHANGE_RATE_DIRECTION` (exported) is the `rate` field's description and the `all_rates` list view's description, which the console renders on the list page. `EXCHANGE_RATE_SCALE` (12) and `EXCHANGE_RATE_PRECISION` (24) exported beside it. - **Registration**: `PlatformObjectsPlugin` registers it in its first manifest (project database, no routing triple), beside `sys_migration` / `sys_migration_journal` / `sys_secret`: its reader is the ObjectQL engine's write path, present in every composition. - **Registry** (`@objectstack/spec`): `PLATFORM_OBJECTS_BY_PACKAGE['platform-objects']` lists `sys_exchange_rate`, so lint, the engine and the CLI treat it as a real platform object; the spec ratchet `platform-object-names.test.ts` requires it. - **Upgrade ceremony** (`@objectstack/cli`): the ADR-0131 D10 organization-ownership inventory gives `sys_exchange_rate` fate 3 (attribution, no anchors: the Default Organization under `single`, otherwise reported), like the other organization-authored tables added since the first census; without the row `os migrate organization-ownership` refuses every plan as carrying an uninventoried platform table. - **Setup**: `nav_exchange_rates` in `group_configuration` directly under `nav_settings_localization`, opening `all_rates`. - **Translations**: object, fields, options, view, empty state and the nav label in `en` / `zh-CN` / `ja-JP` / `es-ES` (the three translated locales hand-written; bundles regenerated with `check-i18n-bundles.mjs --write`, extract config gains the object). - **Docs**: `content/docs/ui/apps.mdx` (where `main` moved the Setup anchors when it retired `ui/setup-app.mdx`) — the `group_configuration` row and one paragraph saying how finance records a month's rates. - **Changeset**: `.changeset/27185-exchange-rate-table.md`, `@objectstack/platform-objects`, `@objectstack/spec` and `@objectstack/cli` patch. ## Why Multi-currency shape C′ (ruling 6465431966 on #27119; epic #8345, M1-1): a record-mode amount is converted to the home currency at write from the rate in force on its document date. This is that rate table. Parameters 2 (admin-only) and 3 (1 home = `rate` foreign, so home = amount ÷ `rate`) are applied as ruled; parameter 4 as ruled in 6469382727 (scale 12, precision 24). ## Tests - New `src/system/sys-exchange-rate.object.test.ts` (15 cases) pins the declaration each enforcing mechanism reads, on the built value: one unique key on `(currency, effective_date)` equal to `'organization'`; `resolveInjectedSystemColumns` plans `organization_id`; not `private` and no `requiredPermissions` (the org-admin wildcard reaches it); `resolveCrudAffordances` gives New/Edit/Delete and no import; `trackHistory` on object and columns; `iso_4217_currency` with the write predicate's member / non-member / lowercase answers; `rate` is `number` with `scale` 12, `precision` 24 and `min` 10⁻¹²; the direction sentence on field and view; the nav entry sits right after Localization and names a declared view. - Updated: `plugin.test.ts` (registration roster), `platform-objects.test.ts` (nav target classified as owned and listable), `bundle-ownership.test.ts` (bundle roster); `packages/cli/test/connect-agent-both-halves-wire.pin.test.ts` (the Setup tree the platform admin is served: 31 → 32 entries, 22 → 23 contributions). The spec ratchet `platform-object-names.test.ts` and the CLI `organization-ownership-inventory.test.ts` pass unchanged with the two registry rows. ## Acceptance notes - **Behavioural layer not exercised here.** The card's four facts are enforced by mechanisms pinned in their own packages (driver-sql's tenant-scoped declared-unique suite; Layer 0; plugin-security `default-permission-sets.ts`; plugin-audit `AUDIT_EXCLUDED_OBJECTS`). `platform-objects` cannot import plugin-security or plugin-audit (they depend on it), and the claimed file surface stops at this package, so no live admin-201 / member-403 / duplicate-409 / cross-org-201 run is recorded: NOT MEASURED, carrier #27197 (M3-3 dogfood seeds rates and drives the browser). - **CSV import stays off** (`system-data`'s default) until #27190 verifies `sys_import_job` serves this object; turning it on is `userActions: { import: true }` there. - **Parameter 4 is ruled (6469382727 on #27185):** scale 12 (twelve digits after the decimal point), total precision 24, decimal arithmetic, never float. The write seam refuses a value beyond either bound rather than rounding it. The column is the platform's `DECIMAL(65,30)` (`NUMERIC_COLUMN_REPRESENTATION`), which holds both. - **Direction as arithmetic.** With parameter 3 as ruled, a home amount is the record amount **divided by** `rate` (ADR-0144). Card #27192's body writes the projection as amount × rate; that card should follow the ruling. - **SQLite stores `number` as REAL** (`table.decimal` compiles to `float` on SQLite), and every dialect reads a `number` back as a JS double (about 15–17 significant digits). A rate using all 24 digits is beyond that and is not round-tripped exactly; #27192 already owns pinning SQLite rounding. - **Paths outside the claimed file surface**, each forced by a CI gate on the new object: - `scripts/platform-object-tenancy-census.json`, regenerated with `node scripts/platform-object-tenancy-census.mjs --write` because the new tenant-scoped object joins the census population (87 → 88 registered, 44 → 45 in reach). Derived artefact, no hand edit. - `packages/spec/src/system/constants/platform-object-names.ts` (one name in the registry list; the spec ratchet reds without it). - `packages/cli/src/utils/organization-ownership-inventory.ts` (one inventory row; the CLI inventory test and about 15 `organization-ownership` integration tests red without it). - `packages/cli/test/connect-agent-both-halves-wire.pin.test.ts` (the Setup nav count). - Not here by design: a rate row whose currency equals the home currency (no refusal yet — the projection treats same-currency as rate 1 without a lookup, #27192), and the home-currency lock (#27198). - The ADR draft for #27183 (PR #27239, ADR-0144, not yet approved) was read: the direction sentence follows it; the scale follows ruling 6469382727. - **Owner package (ADR-0144 Open item 11, against ADR-0029 D1/D3/D6):** `platform-objects`, because the table's one reader is the ObjectQL engine's write path, present in every composition that can write a record; it registers with the engine-facing infrastructure (`sys_migration`, `sys_secret`) and has no on/off capability boundary of its own (ADR-0029 D2). ADR-0144's M1-1 row names the same package. ## Verification <details><summary>Final head 7ad4554 (after merging main, fresh install and full build)</summary> ``` pnpm --filter @objectstack/platform-objects test Test Files 98 passed · Tests 1381 passed pnpm --filter @objectstack/spec exec vitest run src/system/constants/platform-object-names.test.ts 12 passed pnpm --filter @objectstack/spec check:generated all 14 generated artifacts up to date pnpm --filter @objectstack/cli exec vitest run <inventory test, Setup-nav pin, 8 organization-ownership integration suites> 10 files · 164 passed pnpm --filter @objectstack/{cli,platform-objects} typecheck OK 48 scoped re-checks after the merge (docs, i18n, census, changeset, citation gates and the above) :: all exit 0 Before the merge, on 3e26252: pnpm --filter @objectstack/cli test 521 files · 6493 passed dispatch-gates --commands: 115 families; the 3 added by the CLI paths (i18n-coverage, i18n-walk-parity, cli-test-child-env) :: exit 0; the list did not change across the merge. ``` </details> <details><summary>Earlier heads: package test + typecheck (lock VERDICT)</summary> ``` head 86e39fa (final): pnpm --filter @objectstack/platform-objects test Test Files 97 passed (97) Tests 1377 passed (1377) os-verify-lock: VERDICT command-exit 0 pnpm --filter @objectstack/platform-objects typecheck check:test-typecheck: OK; os-verify-lock: VERDICT command-exit 0 node scripts/check-i18n-bundles.mjs :: exit 0 (platform-objects in sync, 11 bundles) ``` </details> <details><summary>Derived gates (dispatch-gates --commands, --ran reconciliation)</summary> ``` dispatch-gates --commands: 98 families (90 derived first, +8 after the census artefact joined the diff). 90 run on head faf1b6e (before the scale/census commit): 86 exit 0 at first run; 4 refused/red, then resolved: - check-plugin-teardown-shape --self-test :: exit 3 (shallow clone) -> fetched pinned commit -> exit 0 - spec check:skill-examples :: exit 3 (client-react unbuilt) -> built -> exit 0 - check:dual-build-cjs-loads :: exit 3 (unbuilt packages) -> exit 0 after the build - check:platform-object-tenancy-census :: exit 1 (census drift, new object) -> --write -> exit 0 On head 86e39fa: the 8 new families :: exit 0; re-run on the final tree, all exit 0: check:platform-object-tenancy-census, check:i18n (bundles), check:keyed-text-bounds, check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:org-identifier, check:system-context-census, check:issue-citations, check:doc-authoring, check:i18n-stale-fill dispatch-gates --ran: 98 derived famil(ies) accounted for - 98 run, 0 NOT-MEASURED (a DERIVED zero - all 98 recorded an exit code and none of them is 3). Not run locally (CI's): pnpm lint, test:repo, the full farm. ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (The spec change is one name in a constant list.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched only to list the new object in `PLATFORM_OBJECTS_BY_PACKAGE`, with its `patch` bump in the changeset. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e00374a commit 194e66c

26 files changed

Lines changed: 588 additions & 15 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/spec': patch
4+
'@objectstack/cli': patch
5+
---
6+
7+
feat(platform-objects): `sys_exchange_rate` — an organization's dated exchange rates, with a Setup list under Localization
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
A new system object holds the rates multi-currency amounts are converted with, one row per currency and effective date:
13+
14+
- **Columns.** `currency` (an ISO 4217 code, checked on write), `rate` (exact decimal, 12 decimal places and 24 digits in all, strictly positive), `effective_date` (date) and `source` (`manual` / `import` / `app`, default `manual`).
15+
- **Direction.** One row means 1 unit of the organization's home currency (Localization → Default currency) = `rate` units of the row's currency, so a home amount is the foreign amount divided by `rate`. The sentence is exported as `EXCHANGE_RATE_DIRECTION` and shown on the Setup list page; the scale and precision are exported as `EXCHANGE_RATE_SCALE` and `EXCHANGE_RATE_PRECISION`.
16+
- **One rate per currency per day.** `(currency, effective_date)` is unique within an organization; two organizations may each hold their own EUR rate for the same day.
17+
- **Who.** Per organization (its `organization_id` is injected and the tenant wall scopes it). Organization administrators read and write it; rank-and-file members do not see it. Every change is captured in the audit log and shown on the record's History tab.
18+
- **Where.** Setup → Configuration → Exchange Rates, directly under Localization. `PlatformObjectsPlugin` registers the object with the other platform-infrastructure objects. `PLATFORM_OBJECTS_BY_PACKAGE` (`@objectstack/spec`) lists it under `platform-objects`, so a reference to `sys_exchange_rate` resolves as a real platform object.
19+
- **Upgrade ceremony.** The organization-ownership inventory (`@objectstack/cli`) lists `sys_exchange_rate`, so `os migrate organization-ownership` plans it instead of refusing it as an uninventoried platform table.
20+
21+
Nothing reads the table yet: the write-time home-currency conversion that uses it ships in the same release.

‎content/docs/ui/apps.mdx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -813,11 +813,17 @@ The Setup app's anchors:
813813
| `group_people_org` | Users, organizations, business units, teams, invitations |
814814
| `group_access_control` | Positions, permission sets, sharing rules, API keys |
815815
| `group_approvals` | Approvals inbox, requests, history, delegations |
816-
| `group_configuration` | Settings, localization, authentication, email, storage, AI, feature flags, branding |
816+
| `group_configuration` | Settings, localization, exchange rates, authentication, email, storage, AI, feature flags, branding |
817817
| `group_diagnostics` | Sessions, notification events, audit log |
818818
| `group_integrations` | Webhooks, cloud connection |
819819
| `group_advanced` | OAuth applications |
820820

821+
**Exchange Rates** (`sys_exchange_rate`, under Localization) holds an organization's dated rates, editable by
822+
its administrators. One row means 1 unit of the home currency (Localization → Default currency) = `rate` units
823+
of the row's currency, from its effective date until a later row for that currency. To record a month's rates,
824+
add one row per currency with the first day of the month as the effective date; a correction is an edit to that
825+
row, and every change lands in the audit log.
826+
821827
Setup is for platform administration and needs `setup.access`. For
822828
project-specific administration pages, declare an app of your own.
823829

‎packages/cli/src/utils/organization-ownership-inventory.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,9 @@ export const ORGANIZATION_OWNERSHIP_INVENTORY: readonly InventoryEntry[] = [
781781
{ object: 'sys_queue', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach, added since the first census); ${D10_SINGLE}`, anchors: [] },
782782
{ object: 'sys_queue_member', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach); names its queue by name, not by id; ${D10_SINGLE}`, anchors: [] },
783783
{ object: 'sys_assignment_cursor', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach); names its rule by name, not by id; ${D10_SINGLE}`, anchors: [] },
784+
// Exchange rates (`@objectstack/platform-objects`, #27185): an organization's
785+
// administrators author its own rate table; no parent row anchors a rate.
786+
{ object: 'sys_exchange_rate', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach, added since the first census); ${D10_SINGLE}`, anchors: [] },
784787
{ object: 'sys_import_job', census: P1, fate: 'attribution', citation: `${D3_ORG_ROWS}; ${D10_SINGLE}`, anchors: [] },
785788
{ object: 'sys_user_preference', census: P1, fate: 'attribution', citation: `${D3_ORG_ROWS}; ${D10_SINGLE}`, anchors: [] },
786789

‎packages/cli/test/connect-agent-both-halves-wire.pin.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,14 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
254254
.toContain('nav_connect_agent');
255255
// The Setup tree the platform admin is served BY THIS COMPOSITION: 9
256256
// authored groups less `group_approvals`, which nothing contributes
257-
// into and the filter collapses, plus the 22 entries
257+
// into and the filter collapses, plus the 23 entries
258258
// `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one.
259259
// `nav_settings_branding` is not among them, and not because of a
260260
// tenancy wall (this harness boots none): since #24766 it is
261261
// contributed by `@objectstack/service-settings` together with the
262262
// `branding` namespace, offered only where no organization wall stands,
263263
// and this harness composes platform-objects and the MCP bundle alone.
264-
expect(allowedIds).toHaveLength(31);
264+
expect(allowedIds).toHaveLength(32);
265265
expect(allowedIds).not.toContain('nav_settings_branding');
266266
// The two per-user stores are reached from the User record page, not
267267
// from Setup's menu (#23803).
@@ -284,8 +284,8 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
284284
expect(withAccountHalf.statusCode).toBe(200);
285285
expect(withoutAccountHalf.statusCode).toBe(200);
286286
expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item));
287-
// 31: the composition the count in the test above states.
288-
expect(navIds(withAccountHalf.body?.item)).toHaveLength(31);
287+
// 32: the composition the count in the test above states.
288+
expect(navIds(withAccountHalf.body?.item)).toHaveLength(32);
289289

290290
// The mechanism behind the invariance, asserted rather than assumed:
291291
// the bundle aims exactly one contribution at `setup` and one at

‎packages/platform-objects/scripts/i18n-extract.config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ import {
187187
} from '../src/metadata/index.js';
188188

189189
// ── System ────────────────────────────────────────────────────────────────
190-
import { SysSetting, SysSecret, SysSettingAudit, SysMigration } from '../src/system/index.js';
190+
import { SysSetting, SysSecret, SysSettingAudit, SysMigration, SysExchangeRate } from '../src/system/index.js';
191191

192192
// ── Existing Setup app + dashboards + translations ────────────────────────
193193
import { SETUP_APP } from '../src/apps/setup.app.js';
@@ -317,6 +317,7 @@ const config: ObjectStackDefinition = defineStack({
317317
SysSecret,
318318
SysSettingAudit,
319319
SysMigration,
320+
SysExchangeRate,
320321
] as any,
321322

322323
apps: [SETUP_APP, STUDIO_APP, ACCOUNT_APP] as any,

‎packages/platform-objects/src/apps/setup-nav.contributions.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
126126
// both directly. No `requiredPermissions` — matches Branding (read perm is
127127
// the app's base `setup.access`).
128128
{ id: 'nav_settings_localization', type: 'url', label: 'Localization', url: '/apps/setup/system/settings/localization', icon: 'globe' },
129+
// Multi-currency M1-1 (epic #8345): the dated rates from the home
130+
// currency Localization sets, so the entry sits directly under it. The
131+
// list page states the rate direction (`EXCHANGE_RATE_DIRECTION`). No
132+
// `requiredPermissions`: the object grant decides who reads and writes
133+
// (organization admins), and Setup itself already needs `setup.access`.
134+
{ id: 'nav_exchange_rates', type: 'object', label: 'Exchange Rates', objectName: 'sys_exchange_rate', viewName: 'all_rates', icon: 'arrow-right-left' },
129135
{ id: 'nav_settings_company', type: 'url', label: 'Company', url: '/apps/setup/system/settings/company', icon: 'building-2' },
130136
// `nav_settings_branding` is contributed by `@objectstack/service-settings`
131137
// with the `branding` namespace, which is offered only where no

‎packages/platform-objects/src/apps/translations/bundle-ownership.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const OWNED_OBJECTS = new Set([
3838
// metadata (sys_view_definition retired as inert, ADR-0131 D13)
3939
'sys_metadata', 'sys_metadata_history', 'sys_metadata_audit',
4040
// system
41-
'sys_setting', 'sys_secret', 'sys_setting_audit', 'sys_migration',
41+
'sys_setting', 'sys_secret', 'sys_setting_audit', 'sys_migration', 'sys_exchange_rate',
4242
]);
4343

4444
describe('objects translation bundle ownership (ADR-0029 D8)', () => {

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3530,5 +3530,46 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
35303530
label: "Updated At"
35313531
}
35323532
}
3533+
},
3534+
sys_exchange_rate: {
3535+
label: "Exchange Rate",
3536+
pluralLabel: "Exchange Rates",
3537+
description: "Exchange rates from the organization's home currency to a foreign currency, by effective date — the rates foreign-currency amounts are converted to the home currency with.",
3538+
fields: {
3539+
id: {
3540+
label: "ID"
3541+
},
3542+
currency: {
3543+
label: "Currency",
3544+
help: "ISO 4217 code of the foreign currency, uppercase (EUR, JPY, CHF)."
3545+
},
3546+
rate: {
3547+
label: "Rate",
3548+
help: "1 unit of the organization's home currency (Localization → Default currency) = Rate units of this currency. Example: home currency USD, currency EUR, rate 0.92 means 1 USD = 0.92 EUR."
3549+
},
3550+
effective_date: {
3551+
label: "Effective Date",
3552+
help: "First date this rate applies to. It stays in force until a later row for the same currency takes over."
3553+
},
3554+
source: {
3555+
label: "Source",
3556+
help: "How the row arrived: typed in Setup (manual), loaded from a file (import), or written by an app flow (app).",
3557+
options: {
3558+
manual: "Manual",
3559+
import: "Import",
3560+
app: "App"
3561+
}
3562+
}
3563+
},
3564+
_views: {
3565+
all_rates: {
3566+
label: "All",
3567+
description: "1 unit of the organization's home currency (Localization → Default currency) = Rate units of this currency. Example: home currency USD, currency EUR, rate 0.92 means 1 USD = 0.92 EUR.",
3568+
emptyState: {
3569+
title: "No exchange rates yet",
3570+
message: "Add one row per currency for each date its rate changes, for example one row per currency at the start of each month."
3571+
}
3572+
}
3573+
}
35333574
}
35343575
};

‎packages/platform-objects/src/apps/translations/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export const en: TranslationData = {
124124
// Configuration
125125
nav_settings_hub: { label: 'All Settings' },
126126
nav_settings_localization: { label: 'Localization' },
127+
nav_exchange_rates: { label: 'Exchange Rates' },
127128
nav_settings_company: { label: 'Company' },
128129
nav_settings_mail: { label: 'Email' },
129130
nav_settings_branding: { label: 'Branding' },

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3530,5 +3530,46 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
35303530
label: "Actualizado el"
35313531
}
35323532
}
3533+
},
3534+
sys_exchange_rate: {
3535+
label: "Tipo de cambio",
3536+
pluralLabel: "Tipos de cambio",
3537+
description: "Tipos de cambio de la moneda local de la organización a una moneda extranjera, por fecha de entrada en vigor: los tipos con los que los importes en moneda extranjera se convierten a la moneda local.",
3538+
fields: {
3539+
id: {
3540+
label: "ID"
3541+
},
3542+
currency: {
3543+
label: "Moneda",
3544+
help: "Código ISO 4217 de la moneda extranjera, en mayúsculas (EUR, JPY, CHF)."
3545+
},
3546+
rate: {
3547+
label: "Tipo",
3548+
help: "1 unidad de la moneda local de la organización (Localización → Moneda predeterminada) = Tipo unidades de esta moneda. Ejemplo: moneda local USD, moneda EUR, tipo 0,92 significa 1 USD = 0,92 EUR."
3549+
},
3550+
effective_date: {
3551+
label: "Fecha de entrada en vigor",
3552+
help: "Primera fecha a la que se aplica este tipo. Sigue vigente hasta que una fila posterior de la misma moneda lo sustituya."
3553+
},
3554+
source: {
3555+
label: "Origen",
3556+
help: "Cómo llegó la fila: introducida en Setup (manual), cargada desde un archivo (importación) o escrita por un flujo de una aplicación (aplicación).",
3557+
options: {
3558+
manual: "Manual",
3559+
import: "Importación",
3560+
app: "Aplicación"
3561+
}
3562+
}
3563+
},
3564+
_views: {
3565+
all_rates: {
3566+
label: "Todos",
3567+
description: "1 unidad de la moneda local de la organización (Localización → Moneda predeterminada) = Tipo unidades de esta moneda. Ejemplo: moneda local USD, moneda EUR, tipo 0,92 significa 1 USD = 0,92 EUR.",
3568+
emptyState: {
3569+
title: "Aún no hay tipos de cambio",
3570+
message: "Añade una fila por moneda para cada fecha en que cambie su tipo, por ejemplo una fila por moneda al inicio de cada mes."
3571+
}
3572+
}
3573+
}
35333574
}
35343575
};

0 commit comments

Comments
 (0)