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 D requires a rate effective on or before D. 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:

  1. Loads the booking and enumerates unconsumed entries.
  2. Recomputes the position at the current rate effective on as_of_date.
  3. Posts one FX_REVALUATION journal only if the delta is non-zero — re-running at the same rate posts nothing (idempotent; the proof exists in TestFXRevaluationPostgres*).
  4. 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_bookings or gl_fx_position_consumed_entries directly — 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_LOSS GL accounts accumulate all revaluation movement; the flag-resolution corrections post here too, flagged as JOURNAL_REVERSAL of the revaluation they correct.