Originally created by: grynn-in
It's a narrow IAS-21 override used during currency translation in consolidation. When a foreign subsidiary's trial balance is translated into the group reporting currency, equity accounts (share capital, premium, pre-acquisition reserves) must not move with FX — they're frozen at the historical rate on the date the equity was established/acquired. One row = "for equity account X in entity Y of group Z, translate at frozen rate R." It is not a general FX table — it has no currency-pair fields.
Consumed by gold_consolidated_trial_balance.sql:214–220:
when accounting_currency = reporting_currency → 1.0
when is_equity = 1 and historical_rate exists → historical_equity_rate ← this doctype
when is_balance_sheet → closing_rate
when is_pnl → average_rate
This issue tracks 6 correctness/design defects found in that doctype and its dbt consumption.
| # | Severity | Issue | Where | What's wrong | Fix direction |
|---|---|---|---|---|---|
| 1 | Critical | Cancel doesn't remove rate; drafts leak to ClickHouse | historical_equity_rate.py:22-29 → clickhouse.py:237-255 |
sync_doctype runs frappe.get_all with no docstatus filter, so the full TRUNCATE+INSERT re-syncs draft (0) and cancelled (2) docs. on_cancel re-inserts the just-cancelled row; drafts leak in on the next submit of any doc. Systemic — same pattern in Ownership Period, IC Balance, Consolidation Adjustment. |
Add filters={"docstatus": 1} in sync_doctype for submittable doctypes |
| 2 | Critical | dbt lookup ignores period_date (wrong as-of) |
gold_consolidated_trial_balance.sql:233-250 |
Comment says "latest rate_date ≤ period_date" but row_number() ... order by rate_date desc + rn = 1 just takes the most recent rate_date ever — no period correlation. Earlier periods get a future rate. Contrast the correct asof join used for ownership. The autoname includes rate_date precisely to allow multiple tranches, but only the latest is ever used. |
Replace row_number/rn=1 with an ASOF join keyed on period_date |
| 3 | High | Free-text keys → silent join misses | historical_equity_rate.json:14-34 |
consolidation_group, data_area_id, main_account are Data, not Link. Any typo silently fails the exact-string dbt join → equity quietly falls back to closing rate (line 217), no error. Conflicts with the app's Link/referential-integrity convention; consolidation_group is a Link elsewhere. |
Convert to Link fields |
| 4 | Medium | No validation | historical_equity_rate.py (no validate) |
No check that main_account is an equity account — yet dbt applies the rate only when is_equity = 1 (line 216), so a misplaced rate is silently dropped. No duplicate or positive-rate guard. (Ownership Period has a validate(); this doesn't.) |
Add validate(): equity-account check + duplicate/positive guards |
| 5 | Low | Permissions too narrow | historical_equity_rate.json:57-66 |
Only System Manager. Rest of the consolidation app grants EPM Admin / System Manager / Administrator; an EPM Admin can't manage historic rates. |
Add EPM Admin + Administrator roles |
| 6 | Low | autoname length risk | historical_equity_rate.json:11 |
HER-{consolidation_group}-{data_area_id}-{main_account}-{rate_date} packs four free-text fields into Frappe's 140-char name → truncation/collision. |
Hash or shorten the naming series |
assert_equity_uses_historical_rate.sql does not catch [#2] — it only checks that a historical rate was used, not the correct-period one. Add a period-specific assertion when fixing [#2].sync_doctype itself so the planned manual Exchange Rate doctype (#91) doesn't inherit the same leak.konsol (Frappe app at docker/frappe/konsol): [#1] (sync_doctype, the doctype hooks), [#3], [#4], [#5], [#6].open_epm (this repo): [#2] (gold_consolidated_trial_balance.sql), the new dbt test.Related: [#91] (FX configuration).
Tickets: #1
Tickets: #130
Tickets: #2
Tickets: #3
Tickets: #4
Tickets: #5
Tickets: #6
Tickets: #91
Tickets: #92
Tickets: #93
Tickets: #97
Originally posted by: grynn-in
Status audit against current
main— 5 of 6 findings resolved, scoping down.clickhouse.py::sync_doctypenow applies{"docstatus": 1}for submittable doctypes (keyed onis_submittable, so it fixes Ownership Period / IC Balance / Consolidation Adjustment / HER at once).row_number()/rn=1with an ASOF join onperiod_dateingold_consolidated_trial_balance.sql.validate()rejects non-positive rates; duplicates blocked via collision-safeautoname(). Remaining: the is-equity-account check (a rate on a non-equity account is still silently dropped by the dbtis_equity=1gate).format:name withdigest_name()(readable head + hash), no truncation/collision.Still open: #3 —
consolidation_group/data_area_id/main_accountare stillData, notLink, so a typo silently misses the dbt join → equity quietly falls back to closing rate with no error. Fixing this needs the field-type change plus a migration patch to validate existing values resolve to real Links. Leaving this issue open scoped to #3 (+ the [#4] equity-account check).Related
Tickets:
#4Originally posted by: grynn-in
Finding [#3] addressed (konsol [#81], merged). A literal
Data→Linkconversion is infeasible — there's no Link target (Consolidation Group is namedCG-{group}-{entity}, the dbt join keys on the bare code, and there's no Legal Entity / Main Account doctype). So referential integrity is now enforced invalidate(): the group must be a known group and the entity a known member entity in the Consolidation Group registry (a typo previously missed the gold join silently → closing-rate fallback).Checked independently, not as a
(group, entity)pair — the Consolidation Group doctype currently diverges from the dbt seed gold joins on (#130), so a pair check would reject seed-correct keys. Pair enforcement lands once [#130] is resolved.Remaining on this issue: finding [#4]'s is-equity-account check (a rate on a non-equity account is still silently dropped by the dbt
is_equity=1gate — needs a ClickHouse/account lookup). Keeping [#92] open scoped to that + pair-enforcement (pending [#130]).Related
Tickets:
#130Tickets:
#3Tickets:
#4Tickets:
#81Tickets:
#92Originally posted by: grynn-in
📋 Decision brief (options + trade-offs + recommendation):
docs/developer-guide/decisions/konsolidat-92-historical-equity-rate.md— merged in [#133].Related
Tickets:
#133Originally posted by: grynn-in
Finding [#4] — reassessed after [#130]. The (group, entity) referential integrity is already enforced by
assert_equity_rate_coverage(error-level), and #130 made it green (historical_equity_rates now matches the seed: GROUP_CORP×{USMF,DEMF,GBMF,JPMF} + AMG×{AMHQ,AMUS,AMDE}).The one remaining bit — the is-equity-account guard — turns out to be low value as an interim dbt test: on the real D365 chart the HER demo accounts 3010/3100 are BalanceSheet/ProfitAndLoss (not Equity) and carry no DEMF activity, so the equity join is inert on real data (as
assert_equity_rate_coveragealready notes). A warn test would just re-state that known demo mismatch on every build.Decision: defer the is-equity guard to its proper form — a doctype
validate()against a synced equity-account registry — and fold it into the single-source-of-truth work in konsol [#82] (which also fixes the HER dual-writer). Keeping [#92] open, scoped to that one guard. Findings #1/#2/#3/#5/#6 are done; [#4]'s coverage half is done.Related
Tickets:
#130Tickets:
#4Tickets:
#82Tickets:
#92Originally posted by: grynn-in
Status check, 11 Sep 2026 — four of the six are fixed by later work. Narrowing this issue to what remains.
clickhouse.resolve_sync_filtersreturns{"docstatus": 1}for any submittable doctype, on both write paths (document hooks andreconcile_all) since grynn-in/konsol#112gold_consolidated_trial_balance.sql:279is anasof left joinkeyed onperiod_date, with aNullable(Float64)cast so a miss falls back to the closing rate instead of 0data_area_idis a Link to Entity (F1).consolidation_groupandmain_accountare stillDatavalidate()now calls_validate_positive_rateand_validate_referencesautonameis now empty rather than the four-field format string — worth a glance to confirm that is deliberateRemaining: [#3] (two fields) and a glance at [#6].
One thing this issue did not anticipate, and which matters more than either: grynn-in/konsol#82.
epm_staging.historical_equity_ratesstill has two writers, and on the live stack the table is at 0 rows — the doctype holds nothing, so every reconcile truncates whatever demo-data inserted. That is what makesassert_equity_rate_coverageone of the two remaining baseline build failures.Related
Tickets:
#3Tickets:
#6Originally posted by: grynn-in
Rescoped on 12 Sep 2026 after an issue review against main.
Done:
data_area_idis a Link to Entity.digest_name.Remaining:
consolidation_groupandmain_accountare still Data fields._validate_referencesdocstring still refers to the deleted seed.The dual-writer problem (grynn-in/konsol#82) went away with the demo removal. grynn-in/konsol#103 (one governed rate table) may later absorb this doctype.
Originally posted by: grynn-in
Verdict: PARTLY DONE — recommend narrowing the scope rather than closing.
The integrity half appears built.
historical_equity_rate.pynow has_validate_references, which refuses an unknown consolidation group ("Unknown consolidation group '…'") and an unknown entity ("Unknown entity '…'. It must be a member entity"), plus_validate_positive_rate.What I could not find is the equity-account guard — nothing appears to refuse a
main_accountthat is not an equity account.Worth re-scoping rather than just finishing: since konsol#182 the chart declares
fx_methodper account, andgold_consolidated_trial_balanceselects the historical rate onfx_method = 'historical', not on account type. So the correct guard is probably "the account must be declaredfx_method = historical", not "the account must be Equity". On the current chart those differ sharply: 21 accounts are declared historical, of which only 6 are equity leaves — 14 assets and 1 liability are too.Live state worth knowing: zero Historical Equity Rate records exist, while 5,481 rows in the consolidated trial balance sit on historical-method accounts translated at a rate other than 1 — every one of them falling back to the closing rate.
🤖 Triage against
main— Claude Code · https://claude.ai/code/session_01P3Pf9835FeLeXjRrYTTZ1MOriginally posted by: grynn-in
Re-scoped — the integrity half is done, and the remaining half needs a different rule
Done:
historical_equity_rate.pynow carries_validate_references, refusing an unknown consolidation group ("Unknown consolidation group '…'") and an unknown entity ("Unknown entity '…'. It must be a member entity"), plus_validate_positive_rate.What remains: the account guard. Nothing refuses a
main_accountthat should not carry a historical rate.But the rule this issue proposes has been overtaken. It asks for an equity-account guard. Since konsol#182 the chart declares
fx_methodper account, andgold_consolidated_trial_balanceselects the historical rate onfx_method = 'historical'— not on account type. On the current chart the two differ sharply:historical3400 Noncontrolling interestis the one equity leaf not declared historicalSo an equity-only guard would refuse 15 accounts the warehouse will happily translate at a historical rate, and permit one it will not. The guard should test the declaration: refuse a
main_accountwhose chart row is notfx_method = 'historical'.Live state worth knowing before this is built: zero Historical Equity Rate records exist, while 5,481 rows in the consolidated trial balance sit on historical-method accounts translated at a rate other than 1 — every one falling back to the closing rate, across 194 distinct rates. There is also an open question about whether declaring 14 assets historical is intended (that is the temporal method, not IAS 21.39 current-rate) — worth settling first, since it changes which accounts this guard should accept.
🤖 Re-scoped 15 Sep 2026 against
main— Claude Code · https://claude.ai/code/session_01P3Pf9835FeLeXjRrYTTZ1MTicket changed by: grynn-in
Ticket changed by: grynn-in
Originally posted by: grynn-in
Reopening: I closed this on merge, and that was wrong. This issue was scoped to finding 3 (the free-text keys) plus the equity-account half of finding 4. Only finding 4 shipped.
Shipped (grynn-in/konsolidat#217, with grynn-in/konsol#241): the warehouse now names a historical declaration on a non-equity account, and a Historical Equity Rate keyed to an account the chart does not type as equity, instead of discarding it in silence.
Still open, and the reason this issue stays open:
consolidation_group,data_area_idandmain_accounton Historical Equity Rate areData, notLink. A typo therefore misses the dbt join and the balance falls back to the closing rate with nothing said. That matters more now than it did: the user decided on 16 Sep that the retained-earnings account may declare historical or closing, so customers will start entering these rates, and a silent miss becomes a wrong number rather than a theoretical one. Fixing it needs the field-type change plus a migration that proves existing values resolve.Originally posted by: grynn-in
Closing: finding 3 is resolved as far as it can be, and the reasoning now lives in the code rather than here.
main_accountis a Link to Main Account (grynn-in/konsol#244). That doctype is named by its own code, so the stored value is unchanged and the dbt join is untouched byte for byte.consolidation_groupanddata_area_idstay as they are, deliberately. Consolidation Group is namedCG-{group}-{entity}while the warehouse joins on the bare code, so a Link there would store a string the join cannot use — trading a silent miss for a certain one. Both keys are instead checked for existence in_validate_references, which also fires on the write paths Frappe's own link validation skips (a patch, an import,ignore_links), and the docstring explains the asymmetry.A migrate patch rewrites existing account values to the stored spelling, or stops the migrate naming every value that does not resolve and the rates carrying it. Nothing is guessed.
The silent-drop class this issue was really about is now covered from both sides: konsol refuses the bad key at entry, and grynn-in/konsolidat#219 names any account and period that declares historical with no rate covering it.
Originally posted by: grynn-in
Fixed by grynn-in/konsol#244 (the account key and its migration) and grynn-in/konsolidat#217 (the warehouse naming its silent drops).
Ticket changed by: grynn-in