Operations
Multi-Currency Operations Guide
How foreign-currency positions, exchange rates, and period-end revaluation
work, and what operations staff are expected to run. Applies to tenants with
foreign-currency journals (any posting whose currency differs from the
position's functional base).
Requires system.config for rate and revaluation endpoints; report.read for
conversion previews.
The model
- Every foreign-currency account/currency pair carries an FX position
booking (
gl_fx_position_bookings): the functional-currency base value the position has been folded to, the last applied rate, and the valuation watermark. - Movement journals are folded into the booking when they're consumed. A revaluation recomputes the position at a new rate and posts only the incremental gain/loss — never re-folds consumed entries.
- Bookings are claimed under CAS so concurrent revaluation attempts cannot double-post.
Exchange rates
POST /api/v1/accounting/cycle/currency/exchange-rates
{ "from_currency": "EUR", "to_currency": "USD", "rate": "1.08",
"effective_date": "2026-09-30" }
- Rates are stored per
(from, to)pair with an effective date. The rate used for a given journal is the most recent rate effective on or before that journal's date — backdating a rate correctly re-folds historical entries. - A revaluation at date
Drequires a rate effective on or beforeD. If none exists, the operation fails closed with "no approved translation" — it never fabricates a rate. - Maintain rates per functional base (the position's base, not any shared global). A tenant's functional currency is defined once in business settings.
Conversion preview
POST /api/v1/accounting/cycle/currency/convert?currency=<BASE>
{ "amount": "1000", "from_currency": "EUR" }
Read-only arithmetic — resolves the rate and returns the converted amount. Requires no journal, posts nothing, safe to call ad hoc.
Period-end revaluation
POST /api/v1/accounting/cycle/currency/fx-revaluation?currency=<BASE>
{ "as_of_date": "2026-09-30" }
For every foreign-currency position under the reporting base:
- Loads the booking and enumerates unconsumed entries.
- Recomputes the position at the current rate effective on
as_of_date. - Posts one
FX_REVALUATIONjournal only if the delta is non-zero — re-running at the same rate posts nothing (idempotent; the proof exists inTestFXRevaluationPostgres*). - Advances the valuation watermark.
Run it at period end, after rates for that date exist. The journal lands in the
normal ledger with reference_type='FX_REVALUATION' — visible in trial
balance, GL drill-down, and the shared audit trail.
What you must not do
- Do not edit
gl_fx_position_bookingsorgl_fx_position_consumed_entriesdirectly — those tables are managed by the service; hand-edits bypass the CAS/transaction guards that keep corrections consistent. - Do not post "manual FX adjustments" as ordinary journals against a foreign position — a subsequent revaluation will recompute the position and the manual journal's effect persists unreconciled. Use the repair-flag path (see below) for corrections.
- Do not run a revaluation with a date before rates for that date exist; the call fails closed rather than using a mismatched rate.
When things go wrong — repair flags
Migrations 000301–000303 retroactively flag entries the upgrade's backfill consumed incorrectly (deferred postings folded at the wrong time, then a phantom revaluation posted). The flags surface at Admin → FX Repair Flags.
See the FX Repair Flag Review Guide for the claim → inspect → resolve/dismiss lifecycle, ownership rules, and the reconciliation drill script that proves the path end-to-end.
Reporting
- Trial balance, balance sheet and income statement accept
?currency=to render a position in any reporting base (middleware resolves the rate chain). - The
FX_GAIN/FX_LOSSGL accounts accumulate all revaluation movement; the flag-resolution corrections post here too, flagged asJOURNAL_REVERSALof the revaluation they correct.