Originally created by: grynn-in
I measured the local stack's data directly (12 Sep 2026) instead of trusting the known-baseline framing. epm_gold.gold_trial_balance has 0 rows with a credit across all 7 entities and all 12 periods of 2024. For each entity, total debits equal the whole trial balance: AMDE 54,212,086 debits and 0 credits, AMHQ 48,690,354 and 0, JPMF 7,600,888,746 and 0, and so on.
So every revenue, liability and equity balance is booked as a debit. Every trial balance is one-sided, and every consolidated number built on them is meaningless. assert_silver_gl_debit_credit_balance fails 7 (one row per entity-year) and has been carried as a "pre-existing demo-data tension".
The raw data is correct. The credit sign lives only in IsCredit.
On epm_raw.general_journal_account_entry_bi_entities, the table d365_raw points at:
AccountingCurrencyAmount; IsCredit = 'Yes' on 962GeneralJournalEntry): 0 net to zero read as signed; 927/927 net to zero with IsCredit applied (residual 0)GeneralJournalAccountEntryBiEntities behaves identically: 0 negatives, 419/419 vouchers balance only with the flagThe demo generator writes magnitudes plus a flag, and has since 6523605 (26 Jun). scripts/generate_demo_data.py:511-512:
is_credit = "Yes" if bal < 0 else "No"
amt = abs(bal)
The staging adapter passes the amount through unsigned. dbt_project/models/staging/d365_fo/stg_d365_fo__gl_entries.sql:38:
entries.AccountingCurrencyAmount as amount,
and silver creates a credit only from a negative amount (silver_gl_entries.sql:44-47), so credit_amount is always 0.
4caf9aa (29 Jun, [#118] "retire dead is_credit") removed the only code that applied the flag:
- when lower(trim(trim(both '"' from trim(toString(coalesce(entries.IsCredit, '')))))) in ('yes', 'true', '1') then 1
- else 0
- end as is_credit,
What the pipeline does NOT do: apply IsCredit to the amount anywhere.
4caf9aa states "VERIFIED PASS against live data 2026-06-29". That can't have held for this data, because the abs-plus-flag generator was already in place from 26 Jun. The likeliest explanation is that the build was silently producing no models until 6e7413e / [#139] (10 Sep), so the test passed on empty tables. That is inferred, not proven.stg_d365_fo__gl_entries.sql, derive the sign from the flag, idempotently:if(<quote-stripped IsCredit> = 'yes', -abs(AccountingCurrencyAmount), abs(AccountingCurrencyAmount)) as amount, using the exact parser 4caf9aa removed. It is correct for this data and for real D365, where the amount is already signed. Apply the same to ReportingCurrencyAmount and TransactionCurrencyAmount.GeneralJournalEntry voucher nets to 0 in stg_gl_entries, so a sign regression fails where it's introduced instead of three layers down.dbt run --select bronze_general_journal_account_entries+ --full-refresh.stg_d365_fo__budget_entries.sql:22 takes AccountingCurrencyAmount the same way, and the generator writes every budget line as abs(amount), 'No' (generate_demo_data.py:918, :1453). A revenue budget (-int(...), line 896) loses its sign at the source, so the generator needs fixing for budgets as well.SELECT count() AS vouchers,
countIf(abs(signed) < 0.01) AS balanced_as_signed,
countIf(abs(flagged) < 0.01) AS balanced_with_flag
FROM (SELECT GeneralJournalEntry,
sum(AccountingCurrencyAmount) AS signed,
sum(if(lower(trim(both '"' from trim(coalesce(IsCredit,'')))) = 'yes',
-abs(AccountingCurrencyAmount), abs(AccountingCurrencyAmount))) AS flagged
FROM epm_raw.general_journal_account_entry_bi_entities GROUP BY 1);
-- 927 | 0 | 927
SELECT countIf(period_credit > 0) FROM epm_gold.gold_trial_balance; -- 0
🤖 Generated with Claude Code
Tickets: #118
Tickets: #139
Tickets: #157
Tickets: #158
Tickets: #160
Tickets: #168
Originally posted by: grynn-in
Correction to an earlier finding: the unsigned source is the demo generator, not necessarily D365
The fix proposed above, deriving the sign from
IsCreditinstg_d365_fo__gl_entries, could make things worse for real D365.AccountingCurrencyAmount, andIsCreditcan disagree with the sign on storno/reversal lines. That's why [#118] retiredis_credit, andtests/assert_is_credit_retired.sqlpins that down.if(IsCredit, -abs(x), abs(x))would flip real storno debits. I have no real D365 data on this stack to confirm the storno semantics either way.scripts/generate_demo_data.py. For the GL it writesamt = abs(bal)with the sign only inIsCredit(:515-516,:1229-1230). For budgets it writesabs(amount)with no sign information at all: its'No'column isIncludeInCashFlowForecast, and all 672 raw budget rows are non-negative.Revised fix: detect, don't guess
general_journal_entry_recid) nets to zero. An unsigned feed from any source then fails at staging, naming the voucher, instead of silently booking every credit as a debit three layers down. It holds whatever real D365 does with storno lines, because a correctly signed voucher always nets to zero.with_demo_buCTE in the same model. It hard-codes AMUS business-unit splits "so MGMT_DEMO rollups are testable", which is demo code in a production adapter.On the current local stack, which still holds the demo raw rows until the planned wipe, (1) is expected to fail. That failure is the proof that it detects the problem.
🤖 Generated with Claude Code
Related
Tickets:
#112Tickets:
#118Ticket changed by: grynn-in