Accounting Placeholder Audit — features that exist in the model but aren't 100% functional¶
Date: 2026-06-15
Scope: accounting-related GraphQL domains only
Trigger: TaxRate.account_payable_id / account_receivable_id / withholding_account_id turned out to be stored-and-echoed placeholders that no posting logic ever reads. This document sweeps the rest of the accounting surface for the same pattern.
Re-verification & remediation — 2026-06-27¶
All 42 findings were re-checked against HEAD and the still-pending ones triaged into cleanup now / fix now / feature (deferred). Original finding text below is preserved as the 2026-06-15 record.
Resolved since the audit¶
- #1 — TaxRate GL-account links: now fully functional and tested. The tax-rate GL wiring feature renamed the columns (
output_tax_account_id/input_tax_account_id/withholding_asset_account_id) and they are consumed at posting time byinvoice_ledger_serviceandsupplier_invoice_ledger_service(covered bytests/graphql/ledger/test_tax_account_sourcing.py).
Verified NOT a bug (no change needed)¶
- #12 (adjacent double-post risk): false alarm.
OpeningInvoiceService/OpeningSupplierInvoiceServicecreate the AR/AP invoice as an aging-only subledger record viasession.add()— they post no ledger entry of their own (confirmed by the class docstring and the absence of anyledger_servicecall). The singleOPENING_BALANCESentry is the only GL posting, so AR/AP are posted exactly once. - #34 (recurring-journal idempotency): the scheduler is already idempotent —
materialize_dueadvancesnext_run_datepastas_of, solist_duenever re-selects a journal in the same period.last_run_atis an informational write-only field, not a missing guard.
Fixed now (with tests)¶
- #27 — depreciation double-post:
FixedAssetService.run_periodnow refuses a period that already has aPOSTEDrun (tests/graphql/fixed_assets/test_depreciation_idempotency.py). - #11 — account landing-page balance:
AccountQueryRepository.paginated_stmtnow computes each account's real posted GL balance (debit-normal vs credit-normal) instead ofliteral(0)(tests/graphql/accounts/test_account_balance.py).
Dead code removed now¶
- #7 stale
pac_transactions/bytecode tree; #8SupplierDefault+client_defaults.py; #9AgeInvoiceenum; #10 orphanvalidate_supplier_defaults_accounts; #22last_closed()stub; #32 unusedLedgerEntry.reversal_of/reversed_byORM relationships (scalar FK columns kept); #41 deadRecurringInvoiceRunStatusenum.
Shipped as feature builds — 2026-06-27¶
- #16 bank-reconciliation settlement → feature (2.9.0).
- #36–#39 supplier-credit application → feature (2.9.1).
- #24 depreciation-run reversal, #25 units-of-production rejected at registration, #26 DRAFT runs retired → feature (2.10.0).
- #31
is_postedmade real via unpost/repost of manual journal entries → feature (2.11.0). (The audit's "headline" claim thatis_postedis neverFalseno longer holds.)
Still deferred — schema change or real feature (need product review)¶
- Dead enum members #14
SPLIT/ #15COMPLETED/ #21IN_PROGRESS/ #33EDITED_MEMO: verified inert. Removing them changes the GraphQL schema; #21 and #33 areauto()members whose removal would reindex later persisted values (data corruption). Retained pending a dedicated enum-cleanup with a data migration. (#24REVERSEDis now produced — no longer dead.) - #35 unused
Account.journal_details/User.journalsback-refs (removal needs matchingback_populatesedits); #42Payment.qb_sync_enabledcolumn (drop needs a migration). - Remaining features: #17/#19 bank matching/detection (embeddings + auto-detect); #28 budget approval gating; #30 budget project/office variance scope (the ledger has no project/office dimension — needs that added first); #40 sales-credit
due_amountsemantics; #2/#3 declaration/certificate PDF persistence; #12 journal-link field (OpeningBalanceCommitResponse.journalis typedJournalResponsebut the entry is aLedgerEntry— needs a response-type change); units-of-production depreciation (build once per-period usage is captured).
Still-
none-resolvedinformational findings (#4, #5, #6, #13, #18, #20, #23, #29) are inert-but-acceptable model fields/labels; left as-is.
What counts as a finding¶
A field, enum value, relationship, mutation, or service method that is declared (and often written) but never consumed by business logic — never read by any ledger-posting, calculation, matching, report, query-filter, or state-transition path. Echoing a value back in a GraphQL *_response serializer does not count as "used."
Three severity tiers:
| Tier | Meaning |
|---|---|
placeholder |
Stored/echoed but never consumed anywhere. Pure dead data. |
partial |
Consumed in some paths, but a specific state/branch/dimension is dead or ignored. |
stub |
A method/mutation that runs but does nothing meaningful (no-op, returns constant, or raises). |
Verification method: each finding was checked by grepping the whole
app/for real reads of the field, then classifying every hit as "real use" vs "serialization echo." Citations arefile:line. The headline claims (is_postedneverFalse, accountbalance=literal(0), bank matches never post to the ledger) were additionally spot-checked by hand.
Summary¶
| # | Domain | Feature | Severity |
|---|---|---|---|
| 1 | tax_filings | TaxRate.account_payable_id / account_receivable_id / withholding_account_id |
placeholder |
| 2 | tax_filings | TaxPeriod.declaration_file_id |
placeholder |
| 3 | tax_filings | RetentionCertificate.pdf_file_id |
placeholder |
| 4 | tax_filings | TaxRate.rate (never used in a tax calc) |
partial |
| 5 | tax_filings | TaxRateRepository.find_effective() + TaxRate.valid_to |
partial |
| 6 | tax_filings | TaxPeriodType (no behavioral branching) |
partial |
| 7 | pac_transactions | Entire domain removed — only stale .pyc remain |
stub |
| 8 | accounts | SupplierDefault model (+ client_defaults.py duplicate) |
placeholder |
| 9 | accounts | AgeInvoice enum + age_invoices_by (account aging) |
placeholder |
| 10 | accounts | AccountValidationService.validate_supplier_defaults_accounts |
placeholder |
| 11 | accounts | Account landing-page balance hardcoded to 0 |
placeholder |
| 12 | opening_balances | OpeningBalanceCommitResponse.journal always None |
placeholder |
| 13 | bank_statements | TransactionKind enum + transaction_kind |
placeholder |
| 14 | bank_statements | TransactionStatus.SPLIT (never set) |
placeholder |
| 15 | bank_statements | BankStatementStatus.COMPLETED (unreachable) |
placeholder |
| 16 | bank_statements | confirm_match / ignore_transaction never settle AR/AP |
partial |
| 17 | bank_statements | normalized_counterparty + reference_number (matching inputs unused) |
partial |
| 18 | bank_statements | balance_after (never reconciled against) |
placeholder |
| 19 | bank_statements | bank_account_id auto-detection (detection path doesn't exist) |
partial |
| 20 | bank_reconciliations | statement_id + statement relationship |
placeholder |
| 21 | bank_reconciliations | BankReconciliationStatus.IN_PROGRESS (never produced) |
partial |
| 22 | bank_reconciliations | BankReconciliationRepository.last_closed() |
stub |
| 23 | bank_accounts | BankAccount.client / supplier / company forward relationships |
partial |
| 24 | fixed_assets | DepreciationRunStatus.REVERSED (never produced) |
placeholder |
| 25 | fixed_assets | DepreciationMethod.UNITS_OF_PRODUCTION (always computes 0) |
stub |
| 26 | fixed_assets | DepreciationRunStatus.DRAFT (vestigial) |
partial |
| 27 | fixed_assets | DepreciationRunRepository.list_for_period() → no duplicate-period guard |
stub |
| 28 | budgets | BudgetStatus.APPROVED / ARCHIVED (inert lifecycle) |
placeholder |
| 29 | budgets | Budget.approved_at / approved_by_id (write-only) |
placeholder |
| 30 | budgets | Budget.project_id / office_id (ignored in variance) |
partial |
| 31 | ledger | LedgerEntry.is_posted (never False) |
placeholder |
| 32 | ledger | LedgerEntry.reversal_of / reversed_by relationships |
partial |
| 33 | ledger_audit | LedgerAuditOperation.EDITED_MEMO (no producer) |
placeholder |
| 34 | recurring_journals | last_ledger_entry_id / last_run_at (write-only) |
partial |
| 35 | journals | Account.journal_details / User.journals back-refs |
partial |
| 36 | credits | SupplierCredit.supplier_credit_status (never transitioned) |
placeholder |
| 37 | credits | SupplierCreditRepository.cancel_supplier_credit (no-op) |
stub |
| 38 | credits | SupplierCreditBalance.due_amount (hardcoded 0) |
placeholder |
| 39 | credits | SupplierCreditDetail.supplier_invoice_detail_id (line link unused) |
placeholder |
| 40 | credits | CreditBalance.due_amount — sales (hardcoded 0) |
placeholder |
| 41 | recurring_invoices | RecurringInvoiceRunStatus enum (entirely dead) |
placeholder |
| 42 | payments | Payment.qb_sync_enabled (never read for payments) |
partial |
Headline themes
- Tax-rate → GL automation was never built. The
TaxRateGL-account links andrateexist so the system could derive tax journal lines automatically, butfileTaxPeriodtakes payment lines from the frontend and worksheets sum pre-computedtax_amount. (#1, #4) - PDF persistence was never wired. Tax declarations and retention certificates generate ephemeral presigned URLs; the
*_file_idcolumns meant to persist them are never written. (#2, #3) - Bank reconciliation records matches but posts nothing. Confirming a bank-statement match links IDs but never settles the invoice/expense or posts a ledger entry — the accounting consequence is missing. (#16, #20)
- Supplier credit application is a shell. A supplier credit can be created but never applied, never has its status/balance maintained, and cannot be meaningfully canceled — unlike the fully-wired sales-credit flow. (#36–#39)
- Several lifecycle states are decorative. Budget approval, ledger
is_posted, depreciationREVERSED/DRAFT, bankIN_PROGRESS/COMPLETED/SPLITare stored or queryable but drive no behavior. (#28, #31, #24, #26, #21, #15, #14)
tax_filings¶
1. TaxRate GL-account links — account_payable_id / account_receivable_id / withholding_account_id — placeholder¶
- Where: app/graphql/tax_filings/models/tax_rate.py:30-47
- Intent: Tie each fiscal rate code to the GL accounts where output-tax liability, input-tax credit, and withholding payable accumulate — so tax journal entries could be posted automatically.
- Why it's not functional: The three FKs are written by
create/update(tax_rate_service.py:48-50, 75-77) and echoed intax_rate_response. No posting code reads them.fileTaxPeriodposts aLedgerEntrybuilt frompaymentLinessupplied by the frontend, not from the rate's accounts. Compare withInvoice.account_receivable_idandSupplierInvoice.account_payable_id, which ledger services do consume (invoice_ledger_service.py:77, payment_ledger_service.py:75).
2. TaxPeriod.declaration_file_id — placeholder¶
- Where: app/graphql/tax_filings/models/tax_period.py:46-51
- Intent: Link a filed period to the persisted declaration PDF
File. - Why: No code assigns it.
file_period()sets status/filed_at/filed_by_id/payment_ledger_entry_idbut not this.TaxDeclarationPdfService.generate()uploads to ephemeraltmp/and returns a presigned URL — it never creates aFilerow. Thedeclaration_fileresolver therefore always returnsNone.
3. RetentionCertificate.pdf_file_id — placeholder¶
- Where: app/graphql/tax_filings/models/retention_certificate.py:60-65
- Intent: Link a certificate to its persisted PDF
File. - Why: Same shape as #2 —
generate_certificate()never sets it,RetentionCertificatePdfService.generate()dumps totmp/and returns a presigned URL.pdf_filealways resolvesNone.
4. TaxRate.rate — partial¶
- Where: app/graphql/tax_filings/models/tax_rate.py:23-27
- Intent: The decimal multiplier (e.g.
0.0700) used to compute ITBMS. - Why: The only consumer of
tax_filings.TaxRateisbuild_itbms_worksheet(), which readscodeonly and sums already-storedInvoiceDetail.tax_amount/sub_total_amount— it never multiplies byrate. Actual invoice tax is computed from a different model,items.ITBMSTaxRate. Hererateis write-and-echo only.
5. TaxRateRepository.find_effective() + TaxRate.valid_to — partial¶
- Where: app/graphql/tax_filings/repositories/tax_rate_repository.py:23-38
- Intent: Resolve the rate effective on a given date via
valid_from/valid_towindows. - Why: Zero callers. The one place needing date-effective resolution — the backfill script — re-implements the logic inline (
scripts/backfill_tax_rate_id_invoice_details.py:85, comment "Inline replication of TaxRateRepository.find_effective for script use"). Sovalid_toand the windowing logic are never consumed by a live read path.
6. TaxPeriodType (ITBMS_MONTHLY / RETENTION_MONTHLY / ISR_ANNUAL) — partial¶
- Where: app/graphql/tax_filings/models/enums.py:7-11
- Intent: Distinguish the kind of period and drive the right worksheet/declaration.
- Why: Never branched on.
build_itbms_worksheet,build_retention_summary,build_isr_annual_worksheet, and both PDF services run against anyperiod_idregardless of its type — nothing stops you building an ITBMS worksheet for anISR_ANNUALperiod. The enum is only a display label.
Verified functional (not flagged):
TaxPeriodStatus.AMENDED+amendTaxPeriod,filed_at/filed_by_id/payment_ledger_entry_id,ITBMS_UNCLASSIFIEDbucketing, the retentionTaxRateCodes, andInvoiceDetail.tax_rate_id/SupplierInvoiceDetail.tax_rate_idworksheet joins.
pac_transactions¶
7. Entire domain removed — only orphaned bytecode remains — stub¶
- Where:
app/graphql/pac_transactions/(only__pycache__/*.pyc; no.pyfiles) - Intent: A
PacTransactionmodel logging each PAC call (transaction_type,source_id,document_number,pac_document_id,success,error_message) for billing/audit. - Why: Committed historically (
a448a29d,62f35685) but does not exist in HEAD.grep -rn "PacTransaction" app --include=*.py→ zero hits. The working tree contains only stale.pyc. The PAC-call audit/billing logging it promised is absent at runtime. - Cleanup: delete the stray
__pycache__directory.
Note: the live PAC/DGI e-invoicing integration (
wrappers/pac,dgi/,pac_service.py,common/mutations/pac_mutations.py) is a real, non-stubbed HTTP integration — not a placeholder. Thedgi/domain has no findings.
accounts¶
8. SupplierDefault model (and client_defaults.py duplicate) — placeholder¶
- Where: app/graphql/accounts/models/supplier_defaults.py:21-56, app/graphql/accounts/models/client_defaults.py:21-56
- Intent: Per-supplier posting defaults (aging basis + default
expense_account_id/discount_account_id). - Why: The class is never imported, so SQLAlchemy never registers it; no migration creates a
supplier_defaultstable; it's not exported from the models__init__. Real defaults live onSupplier/ClientInvoiceDefault.client_defaults.pyis a copy-paste that also definesclass SupplierDefaultwith__tablename__ = "supplier_defaults"— there is noClientDefaultclass anywhere, and itsback_populates="supplier_default"has no matching relationship onAccount, so it would fail to map if ever imported.
9. AgeInvoice enum + age_invoices_by (account aging) — placeholder¶
- Where: app/graphql/accounts/models/aging_invoice.py:8-10
- Intent: Configure whether AR/AP aging is computed by invoice date vs. due date.
- Why: Referenced only by the two dead default models (#8); never imported into any service/schema/report. A
## TODO: Account Aging Aging Categoriescomment atsupplier_defaults.py:44confirms it was never finished.
10. AccountValidationService.validate_supplier_defaults_accounts — placeholder¶
- Where: app/graphql/accounts/services/account_validation_service.py:273-279
- Intent: Validate GL account types on supplier defaults before save.
- Why: Validates the dead
SupplierDefaultfields and has zero callers. Every othervalidate_*in this service is wired into a mutation; this one is orphaned.
11. Account landing-page balance hardcoded to 0 — placeholder¶
- Where: app/graphql/accounts/repositories/account_query_repository.py:60 (
literal(0).label("balance")), exposed via account_landing_page_response.py:12 - Intent: Show each account's running balance in the accounts list.
- Why: The column is always
literal(0)— never computed fromLedgerEntryLine. The GraphQLAccountLandingPage.balanceis a constant zero. Verified by hand.
opening_balances¶
12. OpeningBalanceCommitResponse.journal always None — placeholder¶
- Where: app/graphql/opening_balances/strawberry/opening_balance_commit_response.py:11
- Intent: Return the journal/GL entry created by a successful opening-balance commit so the UI can link to it.
- Why:
commit()returnsjournal=Noneon both the failure and success paths; the ledger entry it creates atopening_balance_import_service.py:112is discarded (_ = await self.ledger_service.create_entry(...)). The field is a permanentNone.
⚠️ Adjacent correctness risk (not a placeholder): in
commit(), AR/AP rows appear to post twice — once via_post_ar_row/_post_ap_row(opening invoices) and again as raw GL lines in thecreate_entrycomprehension atopening_balance_import_service.py:117-126. Worth a closer look as a possible double-posting bug.
bank_statements¶
13. TransactionKind enum + transaction_kind — placeholder¶
- Where: app/graphql/bank_statements/models/bank_statement_transaction.py:66-70, enum at
models/enums.py:31-42 - Intent: Classify each transaction (YAPPY / TRANSFER / ACH / CARD / WIRE / FEE / INTEREST / CASH) for downstream handling.
- Why: Produced by regex and written/echoed, but never branched on or filtered by. No
TransactionKind.<VALUE>comparison exists in business logic. Pure label.
14. TransactionStatus.SPLIT — placeholder¶
- Where: app/graphql/bank_statements/models/enums.py:28
- Intent: A transaction split across multiple matches.
- Why: No code path ever sets
SPLIT; no split mutation/service exists. Dead enum state.
15. BankStatementStatus.COMPLETED — placeholder¶
- Where: app/graphql/bank_statements/models/enums.py:12
- Intent: Terminal "statement fully reconciled" state.
- Why: Nothing transitions to it. Parsing ends at
READY_FOR_REVIEW. Unreachable terminal state.
16. confirm_match / ignore_transaction never settle the matched invoice/expense — partial¶
- Where: app/graphql/bank_statements/services/bank_statement_service.py:157-211
- Intent: Confirming a match should reconcile/settle the linked Invoice/Expense — that's the point of matching.
- Why:
confirm_matchwritesmatch_source_type/match_source_id/match_confidence/status and flipssuggestion.accepted = True, but posts no ledger entry and touches no invoice/expense balance. The wholebank_statements/package contains zero ledger-posting references (verified). Matching is recorded but has no accounting effect.
17. normalized_counterparty + reference_number — partial¶
- Where: app/graphql/bank_statements/models/bank_statement_transaction.py:51-60
- Intent: Column comment: "Counterparty name extracted/normalized for matching and embeddings."
- Why: The matcher keys solely on amount-within-tolerance + date-window ("No LLM, no fuzzy counterparty"). Neither field is read by the matcher — the "for matching" promise (Phase 2 / Qdrant) never landed.
18. balance_after — placeholder¶
- Where: app/graphql/bank_statements/models/bank_statement_transaction.py:62
- Intent: Running balance after the transaction (could validate reconciliation against the statement).
- Why: Written at parse and echoed; never read in any balance/reconciliation check.
19. BankStatement.bank_account_id auto-detection — partial¶
- Where: app/graphql/bank_statements/models/bank_statement.py:27-32
- Intent: Column comment: "Detected/selected bank account; null until detection completes."
- Why: Only ever set from the upload mutation argument; there is no detection logic that resolves the parsed
bank_detected/account_number_maskedto aBankAccount. The "detection completes" path doesn't exist.
bank_reconciliations¶
20. statement_id + statement relationship — placeholder¶
- Where: app/graphql/bank_reconciliations/models/bank_reconciliation.py:54-60
- Intent: Link a reconciliation to the imported PDF statement it closes against.
- Why: Set from the
openmutation arg and echoed, but never read.compute_summary/close/_cleared_bucketsrely entirely on GL ledger lines and the manually-enteredclosing_balance_statement; the linked statement's transactions are never pulled in. Thestatementrelationship is never traversed.
21. BankReconciliationStatus.IN_PROGRESS (vs DRAFT) — partial¶
- Where: app/graphql/bank_reconciliations/models/enums.py:9-10
- Intent: Distinct draft → in-progress lifecycle states.
- Why:
open()always creates with defaultDRAFT; nothing ever assignsIN_PROGRESS. It only appears inside thefind_in_progressfilter list.DRAFTandIN_PROGRESSare functionally identical and the latter is unreachable.
22. BankReconciliationRepository.last_closed() — stub¶
- Where: app/graphql/bank_reconciliations/repositories/bank_reconciliation_repository.py:48-62
- Intent: Return the prior closed reconciliation for opening-balance roll-forward / continuity validation.
- Why: Never called.
open()takesopening_balanceas a raw argument and never validates it against the previous period's closing balance — the roll-forward this enables is not wired.
bank_accounts¶
23. BankAccount.client / supplier / company forward relationships — partial¶
- Where: app/graphql/bank_accounts/models/bank_account.py:39-58
- Intent: Polymorphic
source_type/source_idlink from a bank account back to its owning Client/Supplier/Company. - Why: The three forward relationships are never traversed. Only the reverse
company_bank_accountshas one consumer (PDF rendering);client/supplierdirections are dead. No code resolvesbank_account.client/.supplier/.company.
fixed_assets¶
24. DepreciationRunStatus.REVERSED — placeholder¶
- Where: app/graphql/fixed_assets/models/enums.py:35
- Intent: Mark a depreciation run as reversed/un-posted.
- Why: No code assigns it — the service only sets
POSTED, and there is no reverse-run mutation.dispose()posts a fresh entry and sets the asset toDISPOSEDbut never touches a run's status.
25. DepreciationMethod.UNITS_OF_PRODUCTION — stub¶
- Where: app/graphql/fixed_assets/strategies/depreciation_strategies.py:27-30
- Intent: Depreciate by units of production.
- Why: The
matcharm hardcodesmonthly = Decimal("0")("needs per-period usage which lives outside this scope"). An asset registered with this selectable method silently never depreciates.
26. DepreciationRunStatus.DRAFT — partial¶
- Where: app/graphql/fixed_assets/models/depreciation_run.py:39
- Intent: An unposted/draft run to be posted later.
- Why:
run_period()creates and immediately flips toPOSTED, or leavesDRAFTonly in the degenerate zero-depreciation case. There is no mutation to post a DRAFT run later, and_current_nbvsumsPOSTEDruns only — so a DRAFT run is invisible to all calculations. Vestigial state.
27. DepreciationRunRepository.list_for_period() — stub¶
- Where: app/graphql/fixed_assets/repositories/depreciation_run_repository.py:17-29
- Intent: Fetch existing runs for a period (e.g. to prevent duplicate depreciation of the same month).
- Why: No caller. Consequently
run_depreciationhas no idempotency guard — running it twice for the same month double-posts depreciation. ⚠️ correctness risk.
Verified functional:
salvage_value,useful_life_months,acquisition_cost, the three GL account FKs,FixedAssetStatus.FULLY_DEPRECIATED, and the disposal posting all drive real GL entries.
budgets¶
28. BudgetStatus.APPROVED / ARCHIVED — inert lifecycle — placeholder¶
- Where: app/graphql/budgets/models/enums.py:10-11; written at
budget_service.py:78(approve),:86(archive) - Intent: Gate budgets through draft → approved → archived so (e.g.) only approved budgets feed variance and archived ones are excluded.
- Why:
statusis never read as a filter or guard.variance()sumsBudgetLines with no status check — DRAFT and ARCHIVED budgets count exactly like APPROVED.approve_budget/archive_budgetonly mutate-and-echo the field.
29. Budget.approved_at / approved_by_id — placeholder¶
- Where: app/graphql/budgets/models/budget.py:46-54
- Intent: Record who approved a budget and when.
- Why: Set in
approve()and echoed, but never read by any guard/report. Tied to the inert approval workflow (#28).
30. Budget.project_id / office_id — ignored in variance — partial¶
- Where: app/graphql/budgets/models/budget.py:56-67
- Intent: Scope a budget to a project/office so variance is computed against that scope's ledger activity.
- Why: Written and echoed, but
variance()joinsLedgerEntryLinepurely byaccount_id+ date with no project/office filter. A project-scoped budget is compared against company-wide actuals — the dimension is ignored.
ledger / ledger_audit¶
31. LedgerEntry.is_posted — never False — placeholder¶
- Where: app/graphql/ledger/models/ledger_entry.py:81 (
default=True) - Intent: Distinguish posted/finalized entries from drafts; ~a dozen report queries filter on
is_posted.is_(True). - Why: No code anywhere sets
is_posted = False(verified — zero hits acrossapp/); no draft/post/unpost mutation exists. Every entry is bornTrue, so all theis_posted.is_(True)filters are vacuous. The draft concept the column models doesn't exist.
32. LedgerEntry.reversal_of / reversed_by relationships — partial¶
- Where: app/graphql/ledger/models/ledger_entry.py:94,
:101 - Intent: Navigate to the entry being reversed / the entry that reversed it.
- Why: Both relationships are referenced only at their own definition. The reversal logic operates entirely on the scalar
reversal_of_id/reversed_by_idcolumns (which are functional). The ORM relationships are never loaded/traversed.
33. LedgerAuditOperation.EDITED_MEMO — placeholder¶
- Where: app/graphql/ledger_audit/models/enums.py:11
- Intent: Record a "memo edited" event in the tamper-evident audit chain.
- Why: Appears only in its own enum. The other four values have producers; there is no edit-memo mutation, so this operation is never recorded.
recurring_journals / journals¶
34. RecurringJournal.last_ledger_entry_id / last_run_at — partial¶
- Where: app/graphql/recurring_journals/models/recurring_journal.py:62-65
- Intent: Track the last materialized ledger entry and last run time.
- Why: Assigned in
_materialize_oneand only echoed. No logic reads them back (no "already ran today" idempotency guard keys offlast_run_at;last_ledger_entry_idis never dereferenced). Write-only pointers. (The rest of the recurring-journal scheduler is functional.)
35. Account.journal_details / User.journals back-references — partial¶
- Where: app/graphql/accounts/models/account.py:274, app/graphql/users/models/user.py:244
- Intent: Reverse navigation from account/user to journal detail rows.
- Why: Each is referenced only at its own definition. The journals domain reads
journal_detailsfrom theJournalside; no code loadsaccount.journal_detailsoruser.journals.
credits¶
36. SupplierCredit.supplier_credit_status — placeholder¶
- Where: app/graphql/credits/models/supplier_credit.py:87-93
- Intent: Track the lifecycle (APPLIED/UNAPPLIED/CANCELED/VOID) of a supplier credit note.
- Why: Defaults to
UNAPPLIEDand is never changed or filtered on. Contrast the salesCredit.credit_status, which is transitioned incredit_repository.pyand read by recompute + client-statement report.
37. SupplierCreditRepository.cancel_supplier_credit — no-op — stub¶
- Where: app/graphql/credits/repositories/supplier_credit_repository.py:90-94
- Intent: Cancel a supplier credit note.
- Why: Fetches the credit, calls
flush()with no mutation, returnsTrue. Sets no status, reverts no balance/ledger effect. The mutation is wired end-to-end but accomplishes nothing. (Compare the real salescancel_credit/void_credit.)
38. SupplierCreditBalance.due_amount — hardcoded 0 — placeholder¶
- Where: app/graphql/credits/models/supplier_credit_balance.py:26; writers
supplier_credit_balance_repository.py:32,50 - Intent: "Valor total pendiente de la nota de crédito" — outstanding/unapplied amount.
- Why: Set to
Decimal(0)in bothaddandrecalculate_balances(the latter carries# TODO: Implement the logic...). No code applies a supplier credit against a supplier-invoice balance. Echo-only.
39. SupplierCreditDetail.supplier_invoice_detail_id — placeholder¶
- Where: app/graphql/credits/models/supplier_credit_detail.py:44-48
- Intent: Link a supplier-credit line to the original supplier-invoice line it credits.
- Why: Written and echoed, never read by any allocation. The sales analog (
CreditDetail.invoice_detail_id) is consumed to adjust canceled quantities; no such logic exists supplier-side.
40. CreditBalance.due_amount (sales) — hardcoded 0 — placeholder¶
- Where: app/graphql/credits/models/credit_balance.py:26; writers
credit_balance_repository.py:28,46(# TODOat 45) - Intent: Outstanding/unapplied amount of the sales credit note.
- Why: Hardcoded to
0; only read is the response echo. Sales credit application itself works (it cancels invoice-detail quantities), but the credit's owndue_amountbalance field is a placeholder.
Combined: findings #36–#39 mean the supplier-credit application/cancellation flow is a placeholder shell — a supplier credit can be created and its subtotal computed, but never applied, never status/balance-maintained, and not meaningfully cancelable.
recurring_invoices / payments¶
41. RecurringInvoiceRunStatus enum — entirely dead — placeholder¶
- Where: app/graphql/recurring_invoices/models/enums.py:16-19
- Intent: Represent the outcome (SUCCESS/FAILED) of a recurring-invoice materialization run.
- Why: Zero references outside its own definition. Run outcome is actually tracked via
last_run_at/last_run_error. The enum is never imported, stored, or returned.
42. Payment.qb_sync_enabled — partial¶
- Where: app/graphql/payments/models/payment.py:102-107
- Intent: Per-payment flag gating whether the payment syncs to QuickBooks.
- Why: Written generically by the base repository, but no Payment path reads it — the QB bill-payment sync gates on
payment.qb_bill_payment_id+ the tenant-levelcontext.qb_sync_enabled, neverpayment.qb_sync_enabled. Not even echoed in a payment response serializer.
Suggested follow-up priority¶
High impact (a workflow the UI suggests doesn't actually happen):
- #16 / #20 — bank reconciliation confirms matches but posts nothing to the ledger or settles AR/AP.
- #36–#39 — supplier-credit application/cancellation is a non-functional shell.
- #11 / #31 — account landing-page balance always 0; ledger is_posted draft concept doesn't exist (so "posted-only" report filters are vacuous).
Correctness risks adjacent to placeholders (investigate as bugs, not cleanup): - #12 note — opening-balance commit may double-post AR/AP. - #27 — no duplicate-period guard on depreciation runs → double-posting if run twice.
Decide intent, then either build or remove: - #1–#3 — tax-rate → GL automation & PDF persistence (build the posting/persist path, or drop the columns). - #28–#30 — budget approval lifecycle and project/office scoping.
Dead code to delete:
- #7 (pac_transactions stale .pyc), #8–#10 (SupplierDefault / client_defaults.py / orphan validator / AgeInvoice), #41 (RecurringInvoiceRunStatus), plus the unreachable enum states (#14, #15, #21, #24, #33) and dead methods/relationships (#5, #22, #23, #32, #34, #35).