feat(investment,npa): RBI-grounded targets, GL-tracked interest posting, date/search pickers

- Correct Investment portfolio allocation and Loan Origination product-mix targets against
  RBI's UCB SLR/Non-SLR framework and PSL circulars, with a Target Basis column distinguishing
  RBI-mandated vs board-policy rows (Excel export included).
- Expand Investment holdings dataset with more near-term maturities so the Due-Date Diary
  reflects a realistic spread across FD/Govt Security/Non-SLR/Bond.
- Add From/To period-based Interest Calculation & Post (with GL selection for both posting and
  receiving) across the Investment Interest module, FD Entry and Bond Entry screens; fix a bug
  where a posted period kept re-appearing as due since accrual wasn't tracked per-holding.
- Add a reusable DD-MM-YYYY date-picker component and roll it out across all Investment date
  fields.
- NPA Summary Report: drop SMA-0/1/2 from the stage-wise table (kept in Day-wise Overdue only).
- NPA Recovery Report: add Account Type / NPA Category / Target-vs-Actual columns, a shared
  Account Type filter driving both the account picker and the register, and a searchable
  (fixed-position, not clipped by card overflow) account select for long lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 19:26:19 +05:30
co-authored by Claude Sonnet 5
parent d7f5bfe5f5
commit ebe67cc410
20 changed files with 1401 additions and 226 deletions
@@ -192,8 +192,8 @@
><span>Deal No</span><input type="text" [(ngModel)]="purchaseForm.dealNo" readonly
/></label>
<label class="inv-form-field"
><span>* Deal Date</span><input type="text" [(ngModel)]="purchaseForm.dealDate" placeholder="DD-MM-YYYY"
/></label>
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.dealDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Deal Time (24 hr, HH:mm:ss)</span
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
@@ -221,8 +221,8 @@
</label>
<label class="inv-form-field"
><span>Settlement Dt</span
><input type="text" [(ngModel)]="purchaseForm.settlementDt" placeholder="DD-MM-YYYY"
/></label>
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.settlementDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>* Party Code</span>
<select [(ngModel)]="purchaseForm.partyCode">
@@ -333,8 +333,8 @@
</select>
</label>
<label class="inv-form-field"
><span>* Issue Date</span><input type="text" [(ngModel)]="purchaseForm.issueDate" placeholder="DD-MM-YYYY"
/></label>
><span>* Issue Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Period(Year)</span><input type="number" [(ngModel)]="purchaseForm.periodYear" min="0"
/></label>
@@ -354,8 +354,8 @@
><span>Call Option</span><input type="text" [(ngModel)]="purchaseForm.callOption"
/></label>
<label class="inv-form-field"
><span>Meet Dt</span><input type="text" [(ngModel)]="purchaseForm.meetDt" placeholder="DD-MM-YYYY"
/></label>
><span>Meet Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.meetDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Reso No</span><input type="text" [(ngModel)]="purchaseForm.resoNo"
/></label>
@@ -394,11 +394,11 @@
</select>
</label>
<label class="inv-form-field"
><span>Int Date</span><input type="text" [(ngModel)]="purchaseForm.intDate" placeholder="DD-MM-YYYY"
/></label>
><span>Int Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.intDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Last Int.dt</span><input type="text" [(ngModel)]="purchaseForm.lastIntDt" placeholder="DD-MM-YYYY"
/></label>
><span>* Last Int.dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Accrued Interest (computed)</span
><input type="text" [value]="purchaseAccruedInterest | number: '1.2-2'" readonly
@@ -554,8 +554,8 @@
/></label>
}
<label class="inv-form-field"
><span>Redemption Date *</span><input type="text" [(ngModel)]="redeemDate" placeholder="DD-MM-YYYY"
/></label>
><span>Redemption Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="redeemDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Redemption Proceeds (computed)</span
><input type="text" [value]="'₹' + (redeemAmount | number: '1.2-2')" readonly
@@ -575,11 +575,14 @@
<div class="pos-section-label">Investment - Govt./Bond Interest calculation</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Date</span><input type="text" [(ngModel)]="intCalcDate" placeholder="DD-MM-YYYY"
/></label>
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcFromDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcToDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>Type</span>
<select [(ngModel)]="intCalcType">
<select [(ngModel)]="intCalcType" (ngModelChange)="onIntCalcTypeChange()">
<option value="">-- All Types --</option>
@for (o of bondTypeOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
@@ -599,11 +602,25 @@
<label class="inv-form-field"
><span>ALL</span><input type="checkbox" [(ngModel)]="intCalcAll" style="height: 18px; width: 18px"
/></label>
<label class="inv-form-field">
<span>Post To (Interest Income GL)</span>
<select [(ngModel)]="intCalcGlCode">
@for (g of interestIncomeGlOptions; track g.glCode) {
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
}
</select>
</label>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="processInterest()">Process</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
</div>
@if (intCalcExcludedNpi.length > 0) {
<p class="pg-sub" style="padding: 0 16px 12px">
{{ intCalcExcludedNpi.length }} holding(s) excluded — classified Non-Performing, interest is not posted on
accrual basis (collectible on cash basis only, via Interest Receive in the Investment Interest module).
</p>
}
<div class="pos-section-label">Holdings matching the filter ({{ intCalcTargets.length }})</div>
<table class="pg-table">
<thead>
@@ -694,8 +711,8 @@
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="sellForm.tbillGlAc"
/></label>
<label class="inv-form-field"
><span>* Deal Date</span><input type="text" [(ngModel)]="sellForm.dealDate" placeholder="DD-MM-YYYY"
/></label>
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.dealDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Deal Time</span><input type="text" [(ngModel)]="sellForm.dealTime" placeholder="HH:mm:ss"
/></label>
@@ -708,8 +725,8 @@
</select>
</label>
<label class="inv-form-field"
><span>Settlement Dt</span><input type="text" [(ngModel)]="sellForm.settlementDt" placeholder="DD-MM-YYYY"
/></label>
><span>Settlement Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.settlementDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Face Value</span><input type="number" [(ngModel)]="sellForm.faceValue" min="0"
/></label>
@@ -942,8 +959,8 @@
</select>
</label>
<label class="inv-form-field"
><span>Transfer Date *</span><input type="text" [(ngModel)]="goiTransferDate" placeholder="DD-MM-YYYY"
/></label>
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="goiTransferDate"></mifosx-date-ddmmyyyy
></label>
</div>
<div class="inv-form-actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="saveGoiTransfer()">Save</button>
@@ -958,8 +975,8 @@
<div class="pos-section-label">Investment - Investment Depreciation (Mark-to-Market Provisioning)</div>
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Valuation Date</span><input type="text" [(ngModel)]="depreciationDate" placeholder="DD-MM-YYYY"
/></label>
><span>Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field">
<span>Category</span>
<select [(ngModel)]="depreciationCategory">
@@ -1043,8 +1060,8 @@
><span>Units to Transfer *</span><input type="number" [(ngModel)]="mfTransferUnits" min="0" step="0.001"
/></label>
<label class="inv-form-field"
><span>Transfer Date *</span><input type="text" [(ngModel)]="mfTransferDate" placeholder="DD-MM-YYYY"
/></label>
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="mfTransferDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Transfer Value (computed)</span
><input type="text" [value]="'₹' + (mfTransferValue | number: '1.2-2')" readonly
@@ -1069,6 +1086,7 @@
<th>Cert No</th>
<th>Type</th>
<th>Particulars</th>
<th>GL Posted</th>
<th class="num">Amount</th>
</tr>
</thead>
@@ -1082,11 +1100,12 @@
<td>{{ t.certNo }}</td>
<td>{{ t.txnType }}</td>
<td>{{ t.particulars }}</td>
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="pg-empty">No transactions yet.</td>
<td colspan="7" class="pg-empty">No transactions yet.</td>
</tr>
}
</tbody>
@@ -9,6 +9,7 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
GOVT_SECURITY_HOLDINGS,
NON_SLR_HOLDINGS,
@@ -30,12 +31,16 @@ import {
BOND_RATING_OPTIONS,
INT_TYPE_OPTIONS,
AUTHORITY_OPTIONS,
INTEREST_INCOME_GL_MASTER,
INTEREST_INCOME_GL_BY_TYPE,
InvestmentHolding,
InvestmentTxn,
InstrumentType,
InterestFreq,
INSTRUMENT_LABELS,
accruedInterest,
accruedInterestBetween,
canAccrueInterest,
daysToMaturity,
maturityAmount,
addPeriod,
@@ -173,7 +178,8 @@ interface BondSellForm {
standalone: true,
imports: [
CommonModule,
FormsModule
FormsModule,
DateDdmmyyyyPickerComponent
]
})
export class BondInvestmentComponent {
@@ -224,12 +230,17 @@ export class BondInvestmentComponent {
redeemDate = this.asOnDate;
redeemRemarks = '';
// Interest Calculation (legacy: Date / Type / Script / Deal No / ALL + Process)
intCalcDate = this.asOnDate;
// Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) —
// posts interest accrued strictly within [intCalcFromDate, intCalcToDate], same period-based
// basis as the Investment Interest module's Govt./Bond Interest Calculation.
intCalcFromDate = addPeriod(this.asOnDate, 0, -3);
intCalcToDate = this.asOnDate;
intCalcType: BondType | '' = '';
intCalcScript = '';
intCalcDealNo = '';
intCalcAll = false;
intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
// Write-off
writeoffCertNo = '';
@@ -647,13 +658,15 @@ export class BondInvestmentComponent {
}
// -------------------- Interest calculation --------------------
/** Holdings the Interest Calculation "Process" run would cover, given the filters. */
/** Holdings the Interest Calculation "Process" run would cover, given the filters. A
* Non-Performing holding is always excluded — same accrual-basis rule as the Investment
* Interest module (it's still collectible on cash basis via Interest Receive there). */
get intCalcTargets(): InvestmentHolding[] {
const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
if (this.intCalcAll) {
return this.holdings.filter((h) => h.status === 'ACTIVE');
return base;
}
return this.holdings.filter((h) => {
if (h.status !== 'ACTIVE') return false;
return base.filter((h) => {
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false;
if (this.intCalcDealNo && h.certNo !== this.intCalcDealNo.trim()) return false;
@@ -661,31 +674,62 @@ export class BondInvestmentComponent {
});
}
get intCalcExcludedNpi(): InvestmentHolding[] {
return this.holdings.filter((h) => h.status === 'ACTIVE' && !canAccrueInterest(h));
}
/** Re-suggests the Interest Income GL when the Type filter changes — still overridable. */
onIntCalcTypeChange(): void {
if (this.intCalcType) {
this.intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE[this.intCalcType];
}
}
glName(glCode: string): string {
return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
}
/** Interest accrued strictly within [intCalcFromDate, intCalcToDate] — a period-based
* Interest Calculation & Post run, same basis as the Investment Interest module. */
interestAccrued(h: InvestmentHolding): number {
return accruedInterest(h, this.intCalcDate || this.asOnDate);
return accruedInterestBetween(h, this.intCalcFromDate || h.purchaseDate, this.intCalcToDate || this.asOnDate);
}
processInterest(): void {
const targets = this.intCalcTargets;
const targets = this.intCalcTargets.filter((h) => this.interestAccrued(h) > 0);
if (targets.length === 0) {
this.toast('No holdings match the selected filters.');
this.toast('No holdings have interest due to be posted for this period.');
return;
}
const newTxns: InvestmentTxn[] = targets.map((h, i) => ({
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'INTEREST',
voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`,
txnDate: this.intCalcDate || this.asOnDate,
amount: this.interestAccrued(h),
particulars: `Interest calculated - ${h.scriptName}`
}));
const toDate = this.intCalcToDate || this.asOnDate;
const fromDate = this.intCalcFromDate || toDate;
const glCode = this.intCalcGlCode;
const glName = this.glName(glCode);
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
const amt = this.interestAccrued(h);
h.interestPosted = (h.interestPosted ?? 0) + amt;
h.interestPostedTillDate = toDate;
return {
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'INTEREST',
voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`,
txnDate: toDate,
amount: amt,
particulars: `Interest calculated (accrual) for period ${fromDate} to ${toDate} - ${h.scriptName}`,
glCode,
glName
};
});
this.txns = [
...newTxns,
...this.txns
];
this.intCalcFromDate = toDate;
const total = newTxns.reduce((s, t) => s + t.amount, 0);
this.toast(`Interest processed for ${targets.length} holding(s). Total ₹${total.toLocaleString('en-IN')}.`);
this.toast(
`Interest processed for ${targets.length} holding(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.`
);
}
// -------------------- Sell / Redeem Entry --------------------