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:
@@ -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 --------------------
|
||||
|
||||
Reference in New Issue
Block a user