Originally created by: grynn-in
On real D365 data the GL didn't balance — debit ≠ credit → trial balance off → cash flow off, with every fiscal year imbalanced (millions to trillions). The Close Run assertion suite was Red on the whole warehouse.
The raw D365 source is a clean, balanced double-entry set (all 91,984 vouchers net to 0; AccountingCurrencyAmount sums to 0). Two compounding bugs in our transformation broke it:
stg_d365_fo__gl_entries — IsCredit arrives JSON-quoted ("Yes"/"No") from Airbyte, but the parser matched unquoted yes/true/1, so is_credit was ~always 0 → almost every line booked as a debit (512,697 debit / 922 credit).silver_gl_entries — derived debit/credit via abs(amount) keyed on is_credit, which inherited the bad flag and discarded the real sign on contra/reversal lines.IsCredit.accounting_currency_amount (positive = debit, negative = credit); net = signed amount. The sign is authoritative (raw provably balances), so this also fixes the contra/reversal lines and no longer depends on the flag.dbt test --select test_type:singular via Close Run)| Assertion | Before | After |
|---|---|---|
| assert_silver_gl_debit_credit_balance | 188 | 0 |
| assert_trial_balance_balances | 1,165 | 0 |
| assert_cf_categories_equal_net_change | 1,165 | 0 |
| assert_cta_zero_for_same_currency | 144 | 0 |
Every fiscal year's TB now nets to 0. Close Run went 67/3/2 → 70/2/0 (remaining: fiscal-calendar [#111], a dynamic-step-count check).
bronze_general_journal_account_entries is incremental — a deployment with pre-fix rows needs a one-time dbt run --full-refresh to backfill is_credit and clear stale rows.
🤖 Generated with Claude Code
Originally posted by: grynn-in
Review — fix(dbt): balance GL — derive debit/credit from signed amount + strip IsCredit quotes
Reviewed adversarially in two passes (correctness/data-integrity, then nits/edge-cases) in an isolated worktree off
origin/main. The core change is sound and actually bringssilver_gl_entriesinto line with the documented canonical contract.Why this is correct
amount= debit − credit (signed)", spelled out inmodels/staging/erpnext/stg_erpnext__gl_entries.sql:14-16, and both adapters honour it: D365 passes raw signedAccountingCurrencyAmount; ERPNext emitsdebit - credit.debit_amount = max(amount,0),credit_amount = max(-amount,0). Thereforedebit_amount - credit_amount = amountexactly. The TB balances iffsum(amount) = 0per voucher/entity — which is the same invariant the old code relied on, just sourced from the sign instead of a flag.is_credit-based split wheneveris_creditagreed with the sign, and strictly more robust when it doesn't (the very bug being fixed). Mutually exclusive (one leg is 0 unless amount=0), so no double-counting. Decimal128 comparisons/negation are well-defined in ClickHouse.Blocking issues
None.
Non-blocking nits
models/staging/d365_fo/stg_d365_fo__gl_entries.sql:7-9still says "bronze/silver handle debit/credit splitting using is_credit." That is now false; silver splits on the sign. Please update.models/staging/erpnext/stg_erpnext__gl_entries.sql:14-16says "silver takes abs(amount) and splits debit/credit by the is_credit flag." Also no longer true (and theabs()it references is gone).is_creditis now dead for computation — after this PR no model consumesis_creditfor debit/credit splitting (only carried throughstg_gl_entries→bronze_general_journal_account_entries). So the staging quote-strip fix (stg_d365_fo__gl_entries.sql:48) no longer affects the trial balance at all — it only corrects the storedis_creditcolumn for ad-hoc/analytics use. The two halves of this PR are independent: only the silver change moves the TB. Worth a one-line note in the PR/commit so nobody assumes the staging fix is load-bearing.tests/assert_silver_gl_debit_credit_balance.sql:3isseverity='warn'. With the new derivationsum(debit)-sum(credit) ≡ sum(amount), so this test now is the verification of the PR's central claim ("every voucher nets to 0"). If the sign assumption is ever violated (e.g. an entity that storesAccountingCurrencyAmountunsigned), everything books as a debit and you get only a warning, not a failure. Consider escalating toerrorfor D365 real data, or documenting why warn is intentional.trim(both '"' from trim(toString(...)))(stg_d365_fo__gl_entries.sql:48) strips outer whitespace then quotes, but not whitespace inside quotes (e.g."Yes "). Harmless given nit [#3], but if you ever rely onis_creditagain, normalize inside the quotes too.Verification
dbt parse --no-partial-parse --profiles-dir .→ exit 0 (clean). Only pre-existing, unrelatedMissingArgumentsPropertyInGenericTestDeprecationwarnings ongold_bs_movement.dbt run/build(would mutate live ClickHouse).MERGE RECOMMENDATION: MERGE-WITH-NITS
Related
Tickets:
#3Ticket changed by: grynn-in