Originally created by: grynn-in
Configure FX/currency handling properly. The repo ingests FX rates from D365 but has no ISO 4217 currency reference, no way to surface rates in the app, and no place to enter a manual rate (e.g. a hand-keyed USD→CHF). This issue defines the design and the work, split by system-of-record.
D365 OData ExchangeRates + ExchangeRateTypes
→ Airbyte → epm_raw
→ stg_d365_fo__exchange_rates.sql (×100 when ConversionFactor='One')
→ stg_exchange_rates.sql (canonical UNION, var: erp_sources)
→ bronze_exchange_rate_currency_pairs.sql
→ silver_exchange_rates.sql (÷100)
→ gold_consolidated_trial_balance.sql (rate_lookup)
Gaps:
from_currency/to_currency are unvalidated free strings with no names.SELECT from CH (api.py:111,615,842) but nothing exposes rates.D365 F&O — General ledger ▸ Currencies — uses three tables:
| D365 table | Purpose | Entity |
|---|---|---|
| Currency | ISO 4217 code, name, symbol | Currency / CurrencyISOCodeEntity |
| Exchange rate type | Default / Average / Budget | ExchangeRateType (already ingested) |
| Exchange rate | rate per pair + start date + display factor (One/Hundred) | ExchangeRate keyed by ExchangeRateCurrencyPair |
The display factor is exactly what our adapter's ×100 when ConversionFactor='One' already handles. D365 can also auto-import rates from providers (OANDA, ECB).
Direction of data flow is already split on where data originates (D365-origin → app reads from CH; app-origin → doctype writes to CH via sync_doctype). Creating a doctype to re-hold D365 rates would duplicate the source of truth and sync_doctype's TRUNCATE+INSERT would clobber the D365 rates. So we split by layer:
| Data | System of record | Decision |
|---|---|---|
| Actual / closing / average FX rates | D365 (silver_exchange_rates) |
Surface from CH — read-only view/report in Frappe. No doctype. |
| ISO 4217 currency reference | Static reference | No new doctype. Reuse Frappe core Currency (full ISO 4217). Add currencies.csv dbt seed for the CH-side dimension. |
| Manual rates not in D365 (hand-keyed, budget/plan, overrides) | The app | New Exchange Rate doctype → epm_staging.manual_exchange_rates, UNION'd into stg_exchange_rates as a new erp_source. |
dbt_project/seeds/currencies.csv — currency_code, currency_name, symbol, minor_unit (full ISO 4217 list)._seeds.yml with a unique/not_null test on currency_code.currencies dimension; add a dbt relationships test so from_currency/to_currency in silver_exchange_rates validate against the seed.silver_exchange_rates (or epm_gold equivalent) via the existing clickhouse.execute read pattern (api.py).Exchange Rate doctype: from_currency (Link→Currency), to_currency (Link→Currency), rate_date, exchange_rate, rate_type, display_factor.epm_staging.manual_exchange_rates in clickhouse/init-db.sql.docstatus=1 filter (see related bug below; do not repeat the leak).stg_exchange_rates.sql erp_sources to UNION a 'manual' adapter (stg_manual__exchange_rates.sql) so manual + D365 rates coexist; define precedence vs D365 on pair/date collisions.Do we need to hand-enter rates D365 doesn't supply?
docstatus; dbt lookup ignores period_date; free-text keys; no validation; narrow permissions; autoname length). Tracked separately — the docstatus fix in particular must be applied to sync_doctype so task C doesn't inherit the same leak.open_epm (this repo): A (dbt seed/tests), C dbt adapter.konsol (Frappe app at docker/frappe/konsol): B (read view), C (doctype + DDL + sync).
Originally posted by: grynn-in
Decision (next round). Part A done (#98/#108). Do Part B next — a read-only ClickHouse view of FX rates surfaced via
api.py/konsol.clickhouse(cheap, high value: lets users see the rates that drive translation). Part C (manual Exchange Rate doctype →epm_staging.manual_exchange_ratesUNION'd as a 'manual' source) is more work — after B. Keeping open.Originally posted by: grynn-in
📋 Decision brief (options + trade-offs + recommendation):
docs/developer-guide/decisions/konsolidat-91-surface-fx.md— merged in [#133].Related
Tickets:
#133Originally posted by: grynn-in
Status check, 11 Sep 2026 — A and B are done; only C remains, and it is still gated on the open question.
A. ISO 4217 currency reference — done, differently. Not as
dbt_project/seeds/currencies.csv: [#146] showed that seeds materialise intoepm_gold, which is where konsol also writes, so a seed is the wrong home for anything the app owns. The list is now konsol's ISO Currency doctype (grynn-in/konsol#119), published toepm_gold.currenciesas a declared source, with theunique/not_nulltests oncurrency_codemoved onto the source. 66 codes including the six Frappe itself does not ship (ANG, AZN, GEL, TJS, TMT, XDR), and the correct ISO exponents — JPY 0, KWD 3.assert_exchange_rate_currencies_are_isovalidates FX codes against it and passes.B. Surface D365 rates in the app — done.
konsol/api.py:1684readsepm_silver.silver_exchange_ratesthrough theclickhouse.executepattern, withorchestrator/fx.pyand 24 tests behind it.C. Manual Exchange Rate doctype — not started, and still waiting on the scoping question in this issue: do accountants need to hand-enter rates D365 does not supply? If D365 is authoritative and we only need to see rates, C should be dropped and this issue closed.
Note for whoever picks C up: the
docstatusleak this issue warned about inheriting is fixed centrally inresolve_sync_filters, so a new submittable doctype gets the filter automatically.Related
Tickets:
#146Originally posted by: grynn-in
Parts A and B are done: the ISO Currency doctype is published to
epm_gold.currenciesand the D365 rates are surfaced. Part C, a manual Exchange Rate doctype, is superseded by grynn-in/konsol#103: one governed group rate table, with ERP rates demoted to a pre-fill. Closing in favour of that issue.Ticket changed by: grynn-in