Skip to content

feat: consolidate monthly GL for loan interest accrual and demand#1328

Open
Nihantra-Patel wants to merge 8 commits into
frappe:developfrom
Nihantra-Patel:feat/consolidated-loan-gl
Open

feat: consolidate monthly GL for loan interest accrual and demand#1328
Nihantra-Patel wants to merge 8 commits into
frappe:developfrom
Nihantra-Patel:feat/consolidated-loan-gl

Conversation

@Nihantra-Patel

@Nihantra-Patel Nihantra-Patel commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

With loan_accrual_frequency = Daily, every Loan Interest Accrual and Loan Demand posts its own GL entries every day. Daily accrual is still needed for NPA handling, penal interest, backdated transactions and accurate interest — but the GL footprint doesn't need to be daily.

Solution

Decouple GL posting frequency from accrual frequency. Accruals and demands are still created and submitted daily, but their GL is deferred and rolled up into one consolidated voucher per loan per month.

On a real production-style test loan: GL rows went from ~4,076 to 72 (~98% reduction).


1. Company Configuration

Two custom fields on Company (via install.py):

  • loan_gl_consolidation (Check) — enables monthly GL consolidation
  • loan_gl_consolidation_start_date (Date) — only accruals/demands on or after this date are consolidated

Disabled by default, no behavior change for existing companies. Validation in overrides/company.py requires the start date to be the first day of a future month and after the last posted loan GL, so every consolidated month starts empty and no data migration is needed.

2. Deferral of Per-Document GL

Modified Loan Interest Accrual.make_gl_entries and Loan Demand.make_gl_entries. GL dict generation moved into a reusable build_gl_map(). When consolidation is enabled, no GL is posted on submit or cancel, and gl_posted = 0. Cancelled documents are netted out later by the monthly job using reversing deltas.

New fields on both doctypes: gl_posted (whether GL has been posted) and consolidated_gl_voucher (link to the consolidated voucher).

3. New DocType: Process Consolidated Loan GL

One submittable voucher per loan per month, named PCLGL-<loan>-<month_end>-#####.

image image

It aggregates deferred accruals and demands for the month, produces net GL rows grouped by account, and posts on the run date. It's idempotent (re-running posts only the delta), handles reposts and backdated changes, skips empty months, and never rewrites a month's GL — cancellations post reversing deltas instead. Fully compatible with immutable ledger. Standard Ledger button provided.

Child table Consolidated Loan GL Detail stores the breakdown per row: Source Type (Accrual/Demand), Source Document (only when exactly one doc contributes), Loan Disbursement (LOC dimension), Account, Debit, Credit, and # Docs.

4. NPA Suspense JE Consolidation

Per-accrual NPA suspense JEs are deferred and consolidated to one JE per loan per month. Each run cancels the month's existing suspense JE, recomputes from live accruals, and posts one fresh JE equal to that month's live NPA interest — self-correcting across cancels and reposts, no duplicates. The one-time transition JE when a loan first becomes NPA is unchanged.

5. Wiring

A month-end scheduler job (hooks.py) self-gates and only runs on the last day of the month. After a repost, every affected month from the repost date (or consolidation start date) through today is automatically reconsolidated using deltas.

Date Semantics

No existing posting semantics changed:

Document Economic Month Posting Date
Loan Interest Accrual posting_date Today
Loan Demand demand_date Today
Consolidated GL Same economic month Today

Edge Cases Verified

Scenario Result
Daily accruals defer + consolidate ✅ One voucher/month, ~98% fewer GL rows
Idempotent re-runs ✅ No double posting
Mid-month accrual cancel ✅ Exact reversing delta
Repost (including crossing start-date boundary) ✅ Reconsolidates correctly, no orphan entries
Immutable ledger ON ✅ Correct reversal semantics
Immutable ledger OFF ✅ Correct reversal semantics
NPA suspense JE (normal / penal / additional) ✅ One JE/month equals live NPA interest
NPA + repost + mid-month cancel ✅ Monthly JE always correct
Written-off loan ✅ No GL posted after write-off date
Co-lending / partner share ✅ Demand GL uses full amount; partner split unchanged
async_gl_reversal + consolidation ✅ Consolidation supersedes it; balanced exact reversal
Non-NPA loan ✅ No suspense JE created
Ledger balance ✅ Net Debit − Credit = 0
LOC loan, multiple disbursements ✅ GL posts loan-wise; breakdown table traces each disbursement
LOC Loan Demand ✅ Defers and consolidates correctly
Demand deferral gated on demand_date, not posting_date ✅ Fixed a gap where a backdated demand could defer with no month ever consolidating it

Tests (test_process_consolidated_loan_gl.py)

  1. Deferral + consolidation
  2. Idempotency
  3. Mid-month cancellation reversal
  4. NPA suspense consolidation
  5. Disabled company (daily GL unchanged)
  6. Non-NPA loan (no suspense JE)
  7. Written-off loan (no GL after write-off date)
  8. async_gl_reversal superseded by consolidation
  9. Immutable ledger ON (reversal via forward entry)
  10. NPA + repost + mid-month cancel together
  11. LOC loan, multiple disbursements (accrual)
  12. LOC Loan Demand
  13. Demand deferral gated on demand_date, not posting_date

Known Limitation

Only accruals and demands on or after the configured start date participate in consolidation. Existing historical daily GL is intentionally not retroactively consolidated — enforced by the start-date validation, so consolidation always begins at a clean future boundary.

Nihantra-Patel and others added 4 commits July 12, 2026 21:01
Add optional month-end GL consolidation to collapse the per-day GL of
Loan Interest Accrual and Loan Demand into one voucher per loan per month,
gated by company fields loan_gl_consolidation + start date.

- Defer per-doc GL (on submit and cancel) when consolidation is enabled;
  cancelled-after-consolidation docs are netted out via reversing deltas.
- New Process Consolidated Loan GL doctype (one voucher per loan/month,
  posted on run date) with a Consolidated Loan GL Detail breakdown table
  showing source type, disbursement, account, amount and source doc count.
- Consolidation is idempotent per month; reposts and backdated changes
  settle via deltas without rewriting closed months. Skip empty months.
- Ledger view button; scheduler hook; repost reconsolidation trigger.
- Validate loan_gl_consolidation_start_date: must be first of a future
  month, after the last posted loan GL, so enabling the feature never
  leaves existing daily GL half-consolidated (no data migration needed).
- Fix reconsolidate_gl: gate on whether the start date is set rather than
  on repost_date. A repost from before the start date still regenerates
  deferred docs in later months; those were being skipped and left with
  no GL. Now reconsolidate every month from max(repost_date, start) to today.
- Defer the per-accrual NPA suspense JE (income -> suspense) when
  consolidation is on, and post one consolidated suspense JE per loan
  per month. Uses a recompute-from-live approach (cancel the month's JE,
  repost the sum of live NPA accruals) so it stays correct and easy to
  debug after any cancel or repost.
- Consolidation Details table: add Source Document link (populated when a
  line comes from a single accrual/demand), remove Party Type/Party, and
  count distinct source documents in # Docs instead of GL lines.
@codecov-commenter

codecov-commenter commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.12522% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.25%. Comparing base (8d7e84e) to head (9ce345b).
⚠️ Report is 36 commits behind head on develop.

Files with missing lines Patch % Lines
...nsolidated_loan_gl/process_consolidated_loan_gl.py 78.37% 48 Missing ⚠️
lending/overrides/company.py 31.81% 15 Missing ⚠️
...ype/loan_repayment_repost/loan_repayment_repost.py 56.25% 7 Missing ⚠️
...dated_loan_gl/test_process_consolidated_loan_gl.py 99.25% 2 Missing ⚠️
lending/loan_management/utils.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1328      +/-   ##
===========================================
+ Coverage    83.04%   83.25%   +0.21%     
===========================================
  Files          157      161       +4     
  Lines        11268    11842     +574     
===========================================
+ Hits          9357     9859     +502     
- Misses        1911     1983      +72     
Files with missing lines Coverage Δ
lending/hooks.py 100.00% <ø> (ø)
lending/install.py 0.00% <ø> (ø)
...ated_loan_gl_detail/consolidated_loan_gl_detail.py 100.00% <100.00%> (ø)
lending/loan_management/doctype/loan/test_loan.py 99.22% <ø> (ø)
...loan_management/doctype/loan_demand/loan_demand.py 87.12% <100.00%> (+0.40%) ⬆️
...ype/loan_interest_accrual/loan_interest_accrual.py 88.08% <100.00%> (+0.23%) ⬆️
lending/loan_management/utils.py 27.67% <87.50%> (+5.15%) ⬆️
...dated_loan_gl/test_process_consolidated_loan_gl.py 99.25% <99.25%> (ø)
...ype/loan_repayment_repost/loan_repayment_repost.py 71.57% <56.25%> (-1.51%) ⬇️
lending/overrides/company.py 45.94% <31.81%> (-20.73%) ⬇️
... and 1 more

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@deepeshgarg007
deepeshgarg007 marked this pull request as ready for review July 17, 2026 07:43
@deepeshgarg007
deepeshgarg007 self-requested a review as a code owner July 17, 2026 07:43
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Both prior defects are fixed; the remaining risk is the complexity of a new month-end GL consolidation path touching core accounting records.

Both prior review findings are addressed: the synchronous repost loop is now enqueued and LoanDemand deferral correctly gates on demand_date. No new defects found. Score reflects the inherent risk of a large, novel accounting pathway that is difficult to fully validate through review alone.

process_consolidated_loan_gl.py — build_consolidated_gl, flag_covered_docs, and sync_consolidated_suspense_je handle all accounting state transitions and warrant close QA attention.

Reviews (4): Last reviewed commit: "fix: gate Loan Demand GL deferral on dem..." | Re-trigger Greptile

Comment thread lending/loan_management/doctype/loan_repayment_repost/loan_repayment_repost.py Outdated
Split company-wide GL consolidation into batches of 3000 loans, enqueued
on the long queue, so it does not run thousands of loan submits in one
job for companies with a large number of loans.

Move the repost's month-by-month reconsolidation loop to a background
job too, since it was creating one voucher per month synchronously
inside the repost's on_submit.

Also trim long comments down to short one-liners, and remove a test
that had no assertions.
Per-iteration commits in the batch/month loops are intentional, so a
later failure does not roll back earlier loans or months already
processed in the same job.
Loan Interest Accrual's build_gl_map returns None (not an empty list)
for accruals on or after a loan's write-off date. The consolidation
job looped over that return value directly, so a written-off loan
with deferred accruals under consolidation would crash with a
TypeError. Guard with 'or []', matching Loan Demand's build_gl_map.

Add tests for edge cases that had no automated coverage: non-NPA loan
(no suspense JE), written-off loan (no GL after write-off), async GL
reversal being superseded by consolidation, immutable ledger on
(reversal via a forward entry, not in-place cancellation), NPA plus
repost plus mid-month cancel together, and LOC loans with multiple
disbursements for both accrual and demand.
Comment thread lending/loan_management/doctype/loan_demand/loan_demand.py Outdated
posting_date is always set to today in validate(), while demand_date is
the demand's own economic date and can be backdated. Deferral was gated
on posting_date, but the consolidation queries (get_deferred_docs,
loan_has_deferred_gl) scope by demand_date. A demand with demand_date
before the consolidation start_date but created (posting_date) on or
after it would defer with gl_posted=0, yet fall outside every
consolidation query's date window -- stuck with no GL forever.

Gate on demand_date instead, matching Loan Interest Accrual which
already gates and queries on the same field. Add a regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants