You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2d3501f
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: content/docs/data-modeling/currency.mdx
+102-3Lines changed: 102 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -201,9 +201,108 @@ The organization's home currency is the existing `localization.currency` setting
201
201
**Metadata never names it**: no field, object or dataset declares the home currency, and a
202
202
`dynamic` amount displays in it because that is what `dynamic` means.
203
203
204
-
Consolidating record-mode amounts into one home-currency total, through dated exchange
205
-
rates and a stored home amount, comes in a later release. Until then, total a record-mode
206
-
amount per currency by grouping by its code field.
204
+
To total record-mode amounts across currencies, store each one converted into the home
205
+
currency when it is written, as the next section shows, and aggregate the stored home
206
+
amount.
207
+
208
+
## Converting into the home currency
209
+
210
+
### Declaring the home amount
211
+
212
+
A record-mode amount declares `currencyConfig.homeAmount` to have the engine store its
213
+
value converted into the home currency, together with the rate it used. The declaration
214
+
names three fields of the same object, all required:
215
+
216
+
| Pointer | Names | Declared as |
217
+
|:---|:---|:---|
218
+
|`field`| where the home amount is stored | a `currency` field in `dynamic` mode (it displays in the home currency), `readonly: true`|
219
+
|`rateDate`| the record's document date, which picks the rate | a `date` or `datetime` field. There is no default: the engine never uses `created_at` in its place. |
220
+
|`rateField`| where the rate used is stored | a `number` field, `readonly: true`|
The names `amount_home` and `amount_rate` are a convention, not a contract: the platform
245
+
never injects these columns and never looks them up by name.
246
+
247
+
### The exchange rates
248
+
249
+
Rates live in the organization's **Exchange Rates** (Setup, the `sys_exchange_rate`
250
+
object): one row per currency and effective date, entered by an organization
251
+
administrator, imported, or written by an app flow that fetches a provider feed. A rate
252
+
states how many units of the foreign currency **one unit of the home currency** buys:
253
+
with home currency USD, an EUR row of `0.92` means 1 USD = 0.92 EUR. A rate keeps up to
254
+
twelve decimals.
255
+
256
+
### What a write stores
257
+
258
+
When a record is inserted, and whenever an update changes its amount, its code or its
259
+
document date, the engine computes the home amount:
260
+
261
+
-**The rate** is the row for the record's currency with the latest effective date **on
262
+
or before** the document date. A row dated after the document never applies, and
263
+
neither does "the most recent rate" when it is dated later. A `datetime` document date
264
+
is read as its calendar day in UTC.
265
+
-**The home amount** is the amount **divided by** the rate, computed in exact decimal
266
+
arithmetic and rounded once, half away from zero, to the home currency's minor units:
267
+
0 decimals for JPY, 2 for USD and CNY, 3 for KWD. USD 10,000 at a rate of 0.1389 into
268
+
CNY stores ¥71,994.24.
269
+
-**The rate field** stores the rate row's own value, unrounded, so the stored home
270
+
amount always reproduces from the stored amount and rate.
271
+
-**An amount in the home currency itself** stores rate `1` and needs no rate row.
272
+
-**A blank amount** stores a blank home amount and a blank rate.
273
+
274
+
The home and rate fields are **the engine's alone**. A value a caller sends for either is
275
+
dropped like any `readonly` value (or refused under `strictReadonlyWrites`); a
276
+
`defaultValue` on either never stands; and a value a system write or a historical import
277
+
supplies is computed over. A batch insert and an import convert every row through the
278
+
same path as a single write.
279
+
280
+
A rate row corrected later does not rewrite the amounts already converted with it: an
281
+
update that does not touch the amount, its code or its document date leaves the stored
282
+
home amount and rate as they are.
283
+
284
+
An update by filter (`multi: true`) writes one patch to every matched row, so it is
285
+
refused, with `VALIDATION_FAILED`, when the patch would convert the rows to different home
286
+
amounts. Update those rows by id instead, and each is converted from its own row.
287
+
288
+
### What a write refuses
289
+
290
+
Each refusal is answered before anything is written, and the row is not stored:
291
+
292
+
| Refusal | When | Fix |
293
+
|:---|:---|:---|
294
+
|**`ERR_EXCHANGE_RATE_MISSING` / 400**| No rate row for the record's currency has an effective date on or before its document date. The message names the currency and the date. | Add a rate for that currency effective on or before that date, then write again. The engine never stores a rate of 1, a later rate or an empty home amount in its place. |
295
+
|**`ERR_HOME_CURRENCY_UNSET` / 400**| The organization has no home currency: `localization.currency` is not set. | Set **Localization → Default currency**, then write again. The platform never assumes a home currency. |
296
+
|`VALIDATION_FAILED` / 400, `required` on the date field | A foreign-currency amount has a blank document date. | Write the document date in the same write. |
297
+
298
+
The `validate()` preview, and an import's dry run, answer the same refusals for the same
299
+
rows.
300
+
301
+
### Totals in the home currency
302
+
303
+
The home field is an ordinary stored `currency` field, so every aggregate reads it as it
304
+
reads any amount: a `sum` over `amount_home` is the consolidated total, with no grouping,
305
+
and a `summary` roll-up, a dataset measure or a grid footer over it needs nothing special.
0 commit comments