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:
@@ -192,8 +192,8 @@
|
|||||||
><span>Deal No</span><input type="text" [(ngModel)]="purchaseForm.dealNo" readonly
|
><span>Deal No</span><input type="text" [(ngModel)]="purchaseForm.dealNo" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Deal Date</span><input type="text" [(ngModel)]="purchaseForm.dealDate" placeholder="DD-MM-YYYY"
|
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.dealDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Deal Time (24 hr, HH:mm:ss)</span
|
><span>* Deal Time (24 hr, HH:mm:ss)</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
||||||
@@ -221,8 +221,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Settlement Dt</span
|
><span>Settlement Dt</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.settlementDt" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.settlementDt"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Party Code</span>
|
<span>* Party Code</span>
|
||||||
<select [(ngModel)]="purchaseForm.partyCode">
|
<select [(ngModel)]="purchaseForm.partyCode">
|
||||||
@@ -333,8 +333,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Issue Date</span><input type="text" [(ngModel)]="purchaseForm.issueDate" placeholder="DD-MM-YYYY"
|
><span>* Issue Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.issueDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Period(Year)</span><input type="number" [(ngModel)]="purchaseForm.periodYear" min="0"
|
><span>Period(Year)</span><input type="number" [(ngModel)]="purchaseForm.periodYear" min="0"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -354,8 +354,8 @@
|
|||||||
><span>Call Option</span><input type="text" [(ngModel)]="purchaseForm.callOption"
|
><span>Call Option</span><input type="text" [(ngModel)]="purchaseForm.callOption"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Meet Dt</span><input type="text" [(ngModel)]="purchaseForm.meetDt" placeholder="DD-MM-YYYY"
|
><span>Meet Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.meetDt"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Reso No</span><input type="text" [(ngModel)]="purchaseForm.resoNo"
|
><span>Reso No</span><input type="text" [(ngModel)]="purchaseForm.resoNo"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -394,11 +394,11 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Int Date</span><input type="text" [(ngModel)]="purchaseForm.intDate" placeholder="DD-MM-YYYY"
|
><span>Int Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.intDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Last Int.dt</span><input type="text" [(ngModel)]="purchaseForm.lastIntDt" placeholder="DD-MM-YYYY"
|
><span>* Last Int.dt</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Accrued Interest (computed)</span
|
><span>Accrued Interest (computed)</span
|
||||||
><input type="text" [value]="purchaseAccruedInterest | number: '1.2-2'" readonly
|
><input type="text" [value]="purchaseAccruedInterest | number: '1.2-2'" readonly
|
||||||
@@ -554,8 +554,8 @@
|
|||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Redemption Date *</span><input type="text" [(ngModel)]="redeemDate" placeholder="DD-MM-YYYY"
|
><span>Redemption Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="redeemDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Redemption Proceeds (computed)</span
|
><span>Redemption Proceeds (computed)</span
|
||||||
><input type="text" [value]="'₹' + (redeemAmount | number: '1.2-2')" readonly
|
><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="pos-section-label">Investment - Govt./Bond Interest calculation</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Date</span><input type="text" [(ngModel)]="intCalcDate" placeholder="DD-MM-YYYY"
|
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcFromDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="intCalcToDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<select [(ngModel)]="intCalcType">
|
<select [(ngModel)]="intCalcType" (ngModelChange)="onIntCalcTypeChange()">
|
||||||
<option value="">-- All Types --</option>
|
<option value="">-- All Types --</option>
|
||||||
@for (o of bondTypeOptions; track o.value) {
|
@for (o of bondTypeOptions; track o.value) {
|
||||||
<option [value]="o.value">{{ o.label }}</option>
|
<option [value]="o.value">{{ o.label }}</option>
|
||||||
@@ -599,11 +602,25 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>ALL</span><input type="checkbox" [(ngModel)]="intCalcAll" style="height: 18px; width: 18px"
|
><span>ALL</span><input type="checkbox" [(ngModel)]="intCalcAll" style="height: 18px; width: 18px"
|
||||||
/></label>
|
/></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>
|
||||||
<div class="inv-form-actions">
|
<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--primary" (click)="processInterest()">Process</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</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>
|
<div class="pos-section-label">Holdings matching the filter ({{ intCalcTargets.length }})</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -694,8 +711,8 @@
|
|||||||
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="sellForm.tbillGlAc"
|
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="sellForm.tbillGlAc"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Deal Date</span><input type="text" [(ngModel)]="sellForm.dealDate" placeholder="DD-MM-YYYY"
|
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.dealDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Deal Time</span><input type="text" [(ngModel)]="sellForm.dealTime" placeholder="HH:mm:ss"
|
><span>* Deal Time</span><input type="text" [(ngModel)]="sellForm.dealTime" placeholder="HH:mm:ss"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -708,8 +725,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Settlement Dt</span><input type="text" [(ngModel)]="sellForm.settlementDt" placeholder="DD-MM-YYYY"
|
><span>Settlement Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.settlementDt"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Face Value</span><input type="number" [(ngModel)]="sellForm.faceValue" min="0"
|
><span>* Face Value</span><input type="number" [(ngModel)]="sellForm.faceValue" min="0"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -942,8 +959,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Transfer Date *</span><input type="text" [(ngModel)]="goiTransferDate" placeholder="DD-MM-YYYY"
|
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="goiTransferDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveGoiTransfer()">Save</button>
|
<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="pos-section-label">Investment - Investment Depreciation (Mark-to-Market Provisioning)</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Valuation Date</span><input type="text" [(ngModel)]="depreciationDate" placeholder="DD-MM-YYYY"
|
><span>Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Category</span>
|
<span>Category</span>
|
||||||
<select [(ngModel)]="depreciationCategory">
|
<select [(ngModel)]="depreciationCategory">
|
||||||
@@ -1043,8 +1060,8 @@
|
|||||||
><span>Units to Transfer *</span><input type="number" [(ngModel)]="mfTransferUnits" min="0" step="0.001"
|
><span>Units to Transfer *</span><input type="number" [(ngModel)]="mfTransferUnits" min="0" step="0.001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Transfer Date *</span><input type="text" [(ngModel)]="mfTransferDate" placeholder="DD-MM-YYYY"
|
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="mfTransferDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Transfer Value (computed)</span
|
><span>Transfer Value (computed)</span
|
||||||
><input type="text" [value]="'₹' + (mfTransferValue | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (mfTransferValue | number: '1.2-2')" readonly
|
||||||
@@ -1069,6 +1086,7 @@
|
|||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
|
<th>GL Posted</th>
|
||||||
<th class="num">Amount</th>
|
<th class="num">Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1082,11 +1100,12 @@
|
|||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ t.txnType }}</td>
|
<td>{{ t.txnType }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
|
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
|
||||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="pg-empty">No transactions yet.</td>
|
<td colspan="7" class="pg-empty">No transactions yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
GOVT_SECURITY_HOLDINGS,
|
GOVT_SECURITY_HOLDINGS,
|
||||||
NON_SLR_HOLDINGS,
|
NON_SLR_HOLDINGS,
|
||||||
@@ -30,12 +31,16 @@ import {
|
|||||||
BOND_RATING_OPTIONS,
|
BOND_RATING_OPTIONS,
|
||||||
INT_TYPE_OPTIONS,
|
INT_TYPE_OPTIONS,
|
||||||
AUTHORITY_OPTIONS,
|
AUTHORITY_OPTIONS,
|
||||||
|
INTEREST_INCOME_GL_MASTER,
|
||||||
|
INTEREST_INCOME_GL_BY_TYPE,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InterestFreq,
|
InterestFreq,
|
||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
accruedInterest,
|
accruedInterest,
|
||||||
|
accruedInterestBetween,
|
||||||
|
canAccrueInterest,
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
maturityAmount,
|
maturityAmount,
|
||||||
addPeriod,
|
addPeriod,
|
||||||
@@ -173,7 +178,8 @@ interface BondSellForm {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BondInvestmentComponent {
|
export class BondInvestmentComponent {
|
||||||
@@ -224,12 +230,17 @@ export class BondInvestmentComponent {
|
|||||||
redeemDate = this.asOnDate;
|
redeemDate = this.asOnDate;
|
||||||
redeemRemarks = '';
|
redeemRemarks = '';
|
||||||
|
|
||||||
// Interest Calculation (legacy: Date / Type / Script / Deal No / ALL + Process)
|
// Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) —
|
||||||
intCalcDate = this.asOnDate;
|
// 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 | '' = '';
|
intCalcType: BondType | '' = '';
|
||||||
intCalcScript = '';
|
intCalcScript = '';
|
||||||
intCalcDealNo = '';
|
intCalcDealNo = '';
|
||||||
intCalcAll = false;
|
intCalcAll = false;
|
||||||
|
intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
|
||||||
|
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
|
||||||
|
|
||||||
// Write-off
|
// Write-off
|
||||||
writeoffCertNo = '';
|
writeoffCertNo = '';
|
||||||
@@ -647,13 +658,15 @@ export class BondInvestmentComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Interest calculation --------------------
|
// -------------------- 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[] {
|
get intCalcTargets(): InvestmentHolding[] {
|
||||||
|
const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
|
||||||
if (this.intCalcAll) {
|
if (this.intCalcAll) {
|
||||||
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
return base;
|
||||||
}
|
}
|
||||||
return this.holdings.filter((h) => {
|
return base.filter((h) => {
|
||||||
if (h.status !== 'ACTIVE') return false;
|
|
||||||
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
|
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
|
||||||
if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false;
|
if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false;
|
||||||
if (this.intCalcDealNo && h.certNo !== this.intCalcDealNo.trim()) 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 {
|
interestAccrued(h: InvestmentHolding): number {
|
||||||
return accruedInterest(h, this.intCalcDate || this.asOnDate);
|
return accruedInterestBetween(h, this.intCalcFromDate || h.purchaseDate, this.intCalcToDate || this.asOnDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
processInterest(): void {
|
processInterest(): void {
|
||||||
const targets = this.intCalcTargets;
|
const targets = this.intCalcTargets.filter((h) => this.interestAccrued(h) > 0);
|
||||||
if (targets.length === 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;
|
return;
|
||||||
}
|
}
|
||||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => ({
|
const toDate = this.intCalcToDate || this.asOnDate;
|
||||||
certNo: h.certNo,
|
const fromDate = this.intCalcFromDate || toDate;
|
||||||
instrumentType: h.instrumentType,
|
const glCode = this.intCalcGlCode;
|
||||||
txnType: 'INTEREST',
|
const glName = this.glName(glCode);
|
||||||
voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`,
|
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
||||||
txnDate: this.intCalcDate || this.asOnDate,
|
const amt = this.interestAccrued(h);
|
||||||
amount: this.interestAccrued(h),
|
h.interestPosted = (h.interestPosted ?? 0) + amt;
|
||||||
particulars: `Interest calculated - ${h.scriptName}`
|
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 = [
|
this.txns = [
|
||||||
...newTxns,
|
...newTxns,
|
||||||
...this.txns
|
...this.txns
|
||||||
];
|
];
|
||||||
|
this.intCalcFromDate = toDate;
|
||||||
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
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 --------------------
|
// -------------------- Sell / Redeem Entry --------------------
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, ElementRef, ViewChild, forwardRef } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for `<input type="text" placeholder="DD-MM-YYYY">` used throughout the
|
||||||
|
* Investment module. Keeps the DD-MM-YYYY string every date field/helper in investment-data.ts
|
||||||
|
* already expects (parseDDMMYYYY, addPeriod, etc.), but adds a calendar button that opens the
|
||||||
|
* browser's native date picker and converts the picked value back to DD-MM-YYYY — so users can
|
||||||
|
* either type the date or pick it, without changing any consuming component's data model.
|
||||||
|
*
|
||||||
|
* Usage: <mifosx-date-ddmmyyyy [(ngModel)]="someField"></mifosx-date-ddmmyyyy>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-date-ddmmyyyy',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => DateDdmmyyyyPickerComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="inv-date-field">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="inv-date-field__text"
|
||||||
|
[ngModel]="value"
|
||||||
|
(ngModelChange)="onTextChange($event)"
|
||||||
|
(blur)="onTouched()"
|
||||||
|
placeholder="DD-MM-YYYY"
|
||||||
|
[disabled]="disabled"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inv-date-field__btn"
|
||||||
|
tabindex="-1"
|
||||||
|
[disabled]="disabled"
|
||||||
|
(click)="openPicker()"
|
||||||
|
aria-label="Pick a date"
|
||||||
|
title="Pick a date"
|
||||||
|
>
|
||||||
|
📅
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
#native
|
||||||
|
type="date"
|
||||||
|
class="inv-date-field__native"
|
||||||
|
tabindex="-1"
|
||||||
|
[ngModel]="isoValue"
|
||||||
|
(ngModelChange)="onPickerChange($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
/* Angular view encapsulation stops the host page's .inv-form-field styles from reaching
|
||||||
|
* inside this component's own template, so the look of the surrounding form fields
|
||||||
|
* (height, border, radius, font) is deliberately re-declared here instead of inherited. */
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-date-field {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-date-field__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #0d1b2a;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.inv-date-field__btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.inv-date-field__btn:hover:not(:disabled) {
|
||||||
|
background: #f2f4f7;
|
||||||
|
}
|
||||||
|
.inv-date-field__btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.inv-date-field__native {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DateDdmmyyyyPickerComponent implements ControlValueAccessor {
|
||||||
|
@ViewChild('native') private nativeInput?: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
value = '';
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
|
private onChangeFn: (v: string) => void = () => {};
|
||||||
|
onTouched: () => void = () => {};
|
||||||
|
|
||||||
|
/** DD-MM-YYYY -> yyyy-MM-dd, the format <input type="date"> requires. */
|
||||||
|
get isoValue(): string {
|
||||||
|
const parts = (this.value || '').split('-');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const [
|
||||||
|
dd,
|
||||||
|
mm,
|
||||||
|
yyyy
|
||||||
|
] = parts;
|
||||||
|
if (!/^\d{2}$/.test(dd) || !/^\d{2}$/.test(mm) || !/^\d{4}$/.test(yyyy)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `${yyyy}-${mm}-${dd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(v: string): void {
|
||||||
|
this.value = v ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: (v: string) => void): void {
|
||||||
|
this.onChangeFn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: () => void): void {
|
||||||
|
this.onTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChange(v: string): void {
|
||||||
|
this.value = v;
|
||||||
|
this.onChangeFn(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** yyyy-MM-dd (from the native picker) -> DD-MM-YYYY, the format the rest of the app expects. */
|
||||||
|
onPickerChange(iso: string): void {
|
||||||
|
const parts = (iso || '').split('-');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [
|
||||||
|
yyyy,
|
||||||
|
mm,
|
||||||
|
dd
|
||||||
|
] = parts;
|
||||||
|
const ddmmyyyy = `${dd}-${mm}-${yyyy}`;
|
||||||
|
this.value = ddmmyyyy;
|
||||||
|
this.onChangeFn(ddmmyyyy);
|
||||||
|
this.onTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
openPicker(): void {
|
||||||
|
const el = this.nativeInput?.nativeElement;
|
||||||
|
if (!el || this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const showPicker = (el as HTMLInputElement & { showPicker?: () => void }).showPicker;
|
||||||
|
if (typeof showPicker === 'function') {
|
||||||
|
try {
|
||||||
|
showPicker.call(el);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Some browsers throw if not called from a "user gesture" context they recognise —
|
||||||
|
// fall back to a plain click(), which still opens the native picker.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -179,8 +179,8 @@
|
|||||||
><span>Certificate No.</span><input type="text" [(ngModel)]="purchaseForm.certNo"
|
><span>Certificate No.</span><input type="text" [(ngModel)]="purchaseForm.certNo"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Certificate Date</span><input type="text" [(ngModel)]="purchaseForm.certDate" placeholder="DD-MM-YYYY"
|
><span>Certificate Date</span><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.certDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Sub Type</span>
|
<span>Sub Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.subType">
|
<select [(ngModel)]="purchaseForm.subType">
|
||||||
@@ -198,12 +198,12 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Transaction Date</span
|
><span>Transaction Date</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.transactionDate" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.transactionDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Deposit Date *</span
|
><span>Deposit Date *</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.depositDate" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.depositDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Deposit Amount *</span><input type="number" [(ngModel)]="purchaseForm.depositAmount" min="0"
|
><span>Deposit Amount *</span><input type="number" [(ngModel)]="purchaseForm.depositAmount" min="0"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -217,8 +217,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Interest Date</span
|
><span>Interest Date</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.interestDate" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.interestDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Interest Rate *</span><input type="number" [(ngModel)]="purchaseForm.interestRate" min="0" step="0.01"
|
><span>Interest Rate *</span><input type="number" [(ngModel)]="purchaseForm.interestRate" min="0" step="0.01"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -235,8 +235,8 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Maturity Date (manual) *</span
|
><span>Maturity Date (manual) *</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.manualMaturityDate" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.manualMaturityDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Resolution No.</span><input type="text" [(ngModel)]="purchaseForm.resolutionNo"
|
><span>Resolution No.</span><input type="text" [(ngModel)]="purchaseForm.resolutionNo"
|
||||||
@@ -327,11 +327,25 @@
|
|||||||
<span>Certificate No *</span>
|
<span>Certificate No *</span>
|
||||||
<select [(ngModel)]="interestCertNo">
|
<select [(ngModel)]="interestCertNo">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
@for (h of activeHoldings; track h.certNo) {
|
@for (h of interestEligibleHoldings; track h.certNo) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="interestFromDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="interestToDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Post To (Interest Income GL)</span>
|
||||||
|
<select [(ngModel)]="interestGlCode">
|
||||||
|
@for (g of interestIncomeGlOptions; track g.glCode) {
|
||||||
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
@if (interestHolding; as h) {
|
@if (interestHolding; as h) {
|
||||||
<label class="inv-form-field"><span>Party</span><input type="text" [value]="h.party" readonly /></label>
|
<label class="inv-form-field"><span>Party</span><input type="text" [value]="h.party" readonly /></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
@@ -341,7 +355,7 @@
|
|||||||
><span>Frequency</span><input type="text" [value]="h.interestFreq" readonly
|
><span>Frequency</span><input type="text" [value]="h.interestFreq" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Accrued Interest (as on {{ asOnDate }})</span
|
><span>Interest for Period</span
|
||||||
><input type="text" [value]="'₹' + (interestAccrued(h) | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (interestAccrued(h) | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
@@ -376,8 +390,8 @@
|
|||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Sell / Premature Date *</span><input type="text" [(ngModel)]="sellDate" placeholder="DD-MM-YYYY"
|
><span>Sell / Premature Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="sellDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"><span>Remarks</span><input type="text" [(ngModel)]="sellRemark" /></label>
|
<label class="inv-form-field"><span>Remarks</span><input type="text" [(ngModel)]="sellRemark" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
@@ -454,6 +468,7 @@
|
|||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
|
<th>GL Posted</th>
|
||||||
<th class="num">Amount</th>
|
<th class="num">Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -467,6 +482,7 @@
|
|||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ t.txnType }}</td>
|
<td>{{ t.txnType }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
|
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
|
||||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
FD_HOLDINGS,
|
FD_HOLDINGS,
|
||||||
ALL_TXNS,
|
ALL_TXNS,
|
||||||
@@ -17,10 +18,14 @@ import {
|
|||||||
FD_TYPE_OPTIONS,
|
FD_TYPE_OPTIONS,
|
||||||
INT_TYPE_OPTIONS,
|
INT_TYPE_OPTIONS,
|
||||||
LIEN_REASON_MASTER,
|
LIEN_REASON_MASTER,
|
||||||
|
INTEREST_INCOME_GL_MASTER,
|
||||||
|
INTEREST_INCOME_GL_BY_TYPE,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
InterestFreq,
|
InterestFreq,
|
||||||
accruedInterest,
|
accruedInterest,
|
||||||
|
accruedInterestBetween,
|
||||||
|
canAccrueInterest,
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
maturityAmount,
|
maturityAmount,
|
||||||
addPeriod,
|
addPeriod,
|
||||||
@@ -83,7 +88,8 @@ interface FdPurchaseForm {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class FdInvestmentComponent {
|
export class FdInvestmentComponent {
|
||||||
@@ -98,6 +104,7 @@ export class FdInvestmentComponent {
|
|||||||
readonly fdTypeOptions = FD_TYPE_OPTIONS;
|
readonly fdTypeOptions = FD_TYPE_OPTIONS;
|
||||||
readonly intTypeOptions = INT_TYPE_OPTIONS;
|
readonly intTypeOptions = INT_TYPE_OPTIONS;
|
||||||
readonly lienReasonOptions = LIEN_REASON_MASTER;
|
readonly lienReasonOptions = LIEN_REASON_MASTER;
|
||||||
|
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
|
||||||
readonly branchOptions = [
|
readonly branchOptions = [
|
||||||
'HEAD OFFICE',
|
'HEAD OFFICE',
|
||||||
'BRANCH 1',
|
'BRANCH 1',
|
||||||
@@ -114,6 +121,9 @@ export class FdInvestmentComponent {
|
|||||||
searchCertNo = '';
|
searchCertNo = '';
|
||||||
|
|
||||||
interestCertNo = '';
|
interestCertNo = '';
|
||||||
|
interestFromDate = addPeriod(this.asOnDate, 0, -3);
|
||||||
|
interestToDate = this.asOnDate;
|
||||||
|
interestGlCode = INTEREST_INCOME_GL_BY_TYPE.FD;
|
||||||
sellCertNo = '';
|
sellCertNo = '';
|
||||||
sellDate = this.asOnDate;
|
sellDate = this.asOnDate;
|
||||||
sellRemark = '';
|
sellRemark = '';
|
||||||
@@ -313,12 +323,24 @@ export class FdInvestmentComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Interest --------------------
|
// -------------------- Interest --------------------
|
||||||
|
/** Certificates eligible for accrual-basis posting — same rule as the Investment Interest
|
||||||
|
* module: a Non-Performing holding never appears here, only through Interest Receive. */
|
||||||
|
get interestEligibleHoldings(): InvestmentHolding[] {
|
||||||
|
return this.activeHoldings.filter((h) => canAccrueInterest(h));
|
||||||
|
}
|
||||||
|
|
||||||
get interestHolding(): InvestmentHolding | undefined {
|
get interestHolding(): InvestmentHolding | undefined {
|
||||||
return this.holdings.find((h) => h.certNo === this.interestCertNo);
|
return this.holdings.find((h) => h.certNo === this.interestCertNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interest accrued strictly within [interestFromDate, interestToDate] — a period-based
|
||||||
|
* Interest Calculation & Post run, same basis as the Investment Interest module. */
|
||||||
interestAccrued(h: InvestmentHolding): number {
|
interestAccrued(h: InvestmentHolding): number {
|
||||||
return accruedInterest(h, this.asOnDate);
|
return accruedInterestBetween(h, this.interestFromDate || h.purchaseDate, this.interestToDate || this.asOnDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
glName(glCode: string): string {
|
||||||
|
return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
postInterest(): void {
|
postInterest(): void {
|
||||||
@@ -328,19 +350,34 @@ export class FdInvestmentComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const amt = this.interestAccrued(h);
|
const amt = this.interestAccrued(h);
|
||||||
|
if (amt <= 0) {
|
||||||
|
this.toast('No interest due to be posted for this period.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toDate = this.interestToDate || this.asOnDate;
|
||||||
|
const fromDate = this.interestFromDate || toDate;
|
||||||
|
const glCode = this.interestGlCode;
|
||||||
|
const glName = this.glName(glCode);
|
||||||
|
h.interestPosted = (h.interestPosted ?? 0) + amt;
|
||||||
|
h.interestPostedTillDate = toDate;
|
||||||
|
this.interestFromDate = toDate;
|
||||||
this.txns = [
|
this.txns = [
|
||||||
{
|
{
|
||||||
certNo: h.certNo,
|
certNo: h.certNo,
|
||||||
instrumentType: 'FD',
|
instrumentType: 'FD',
|
||||||
txnType: 'INTEREST',
|
txnType: 'INTEREST',
|
||||||
voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`,
|
voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`,
|
||||||
txnDate: this.asOnDate,
|
txnDate: toDate,
|
||||||
amount: amt,
|
amount: amt,
|
||||||
particulars: `Interest posted - ${h.party} (${h.interestFreq})`
|
particulars: `Interest posted (accrual) for period ${fromDate} to ${toDate} - ${h.party} (${h.interestFreq})`,
|
||||||
|
glCode,
|
||||||
|
glName
|
||||||
},
|
},
|
||||||
...this.txns
|
...this.txns
|
||||||
];
|
];
|
||||||
this.toast(`Interest of ₹${amt.toLocaleString('en-IN')} posted for Certificate ${h.certNo}.`);
|
this.toast(
|
||||||
|
`Interest of ₹${amt.toLocaleString('en-IN')} posted to ${glCode} - ${glName} for Certificate ${h.certNo}.`
|
||||||
|
);
|
||||||
this.interestCertNo = '';
|
this.interestCertNo = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,10 +85,16 @@
|
|||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
|
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Govt Security, Non-SLR, Mutual Fund and Share targets trace to RBI's SLR floor (18% of NDTL) and Non-SLR ceiling
|
||||||
|
(≤10% of deposits) for Urban Co-operative Banks; Fixed Deposit and Bond are board-approved policy choices within
|
||||||
|
the room that framework leaves.
|
||||||
|
</p>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Instrument Type</th>
|
<th>Instrument Type</th>
|
||||||
|
<th>Target Basis</th>
|
||||||
<th class="num">Target %</th>
|
<th class="num">Target %</th>
|
||||||
<th class="num">Actual %</th>
|
<th class="num">Actual %</th>
|
||||||
<th class="num">Actual Value</th>
|
<th class="num">Actual Value</th>
|
||||||
@@ -100,6 +106,11 @@
|
|||||||
@for (a of allocationVsTarget; track a.type) {
|
@for (a of allocationVsTarget; track a.type) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ a.label }}</td>
|
<td>{{ a.label }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="inv-badge" [class.inv-badge--matured]="a.isRbiLinked">
|
||||||
|
{{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td class="num">{{ a.targetPct }}%</td>
|
<td class="num">{{ a.targetPct }}%</td>
|
||||||
<td class="num">{{ a.actualPct }}%</td>
|
<td class="num">{{ a.actualPct }}%</td>
|
||||||
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
ALL_HOLDINGS,
|
ALL_HOLDINGS,
|
||||||
|
ALLOCATION_TARGET_IS_RBI_LINKED,
|
||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
@@ -39,6 +40,9 @@ interface AllocationRow {
|
|||||||
actualValue: number;
|
actualValue: number;
|
||||||
variancePct: number;
|
variancePct: number;
|
||||||
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
|
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
|
||||||
|
/** True when the target itself traces to an RBI SLR/Non-SLR rule rather than being a
|
||||||
|
* board-only policy choice — see TARGET_ALLOCATION_PCT's doc comment in investment-data.ts. */
|
||||||
|
isRbiLinked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
|
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
|
||||||
@@ -114,7 +118,8 @@ export class InvestmentDashboardComponent {
|
|||||||
actualPct: +actualPct.toFixed(1),
|
actualPct: +actualPct.toFixed(1),
|
||||||
actualValue: value,
|
actualValue: value,
|
||||||
variancePct,
|
variancePct,
|
||||||
statusLabel
|
statusLabel,
|
||||||
|
isRbiLinked: ALLOCATION_TARGET_IS_RBI_LINKED[type]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -195,6 +200,7 @@ export class InvestmentDashboardComponent {
|
|||||||
subtitle: `As on ${this.asOnDate}`,
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
headers: [
|
headers: [
|
||||||
'Instrument Type',
|
'Instrument Type',
|
||||||
|
'Target Basis',
|
||||||
'Target %',
|
'Target %',
|
||||||
'Actual %',
|
'Actual %',
|
||||||
'Actual Value',
|
'Actual Value',
|
||||||
@@ -203,6 +209,7 @@ export class InvestmentDashboardComponent {
|
|||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
26,
|
||||||
|
20,
|
||||||
12,
|
12,
|
||||||
12,
|
12,
|
||||||
18,
|
18,
|
||||||
@@ -211,6 +218,7 @@ export class InvestmentDashboardComponent {
|
|||||||
],
|
],
|
||||||
rows: this.allocationVsTarget.map((a) => [
|
rows: this.allocationVsTarget.map((a) => [
|
||||||
a.label,
|
a.label,
|
||||||
|
a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy',
|
||||||
a.targetPct,
|
a.targetPct,
|
||||||
a.actualPct,
|
a.actualPct,
|
||||||
a.actualValue,
|
a.actualValue,
|
||||||
|
|||||||
@@ -53,16 +53,45 @@ export const PROVISION_STATUS_LABELS: Record<InvestmentProvisionStatus, string>
|
|||||||
LOSS: 'Loss (NPI)'
|
LOSS: 'Loss (NPI)'
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Board-approved portfolio-policy target — % of the total investment corpus each instrument
|
/**
|
||||||
* type should hold. Compared against actual holdings on the Dashboard so a treasury desk can
|
* Portfolio-policy target — % of the total investment corpus each instrument type should hold.
|
||||||
* see at a glance where the book has drifted from policy. */
|
* Compared against actual holdings on the Dashboard so a treasury desk can see at a glance where
|
||||||
|
* the book has drifted from policy. Four of these six trace back to real RBI rules for Urban
|
||||||
|
* Co-operative Banks (RBI Master Direction/Circular — Investment Portfolio of Primary (Urban)
|
||||||
|
* Co-operative Banks):
|
||||||
|
* • SLR: UCBs must hold 18% of NDTL in approved securities — predominantly Central/State Govt
|
||||||
|
* securities (and SLR-eligible State Development Loan bonds). A well-run treasury holds a
|
||||||
|
* safety buffer above the bare floor, so Govt Security dominates the book far beyond the 18%
|
||||||
|
* minimum — it isn't itself an upper target, just the reason this bucket is the largest.
|
||||||
|
* • Non-SLR ceiling: a UCB's Non-SLR investments must not exceed 10% of its total deposits as
|
||||||
|
* on 31 March of the previous year — a hard regulatory cap, not a board choice.
|
||||||
|
* • Mutual Funds: RBI permits UCBs to invest only in debt-oriented schemes of SEBI-registered
|
||||||
|
* mutual funds, in strictly limited amounts — equity/balanced schemes are not permitted.
|
||||||
|
* • Shares: RBI does not permit UCBs to invest in equity shares of companies as a general
|
||||||
|
* investment class; any "Share" holding is essentially the (small, largely static) shares a
|
||||||
|
* UCB holds in affiliated co-operative institutions, not a discretionary trading book.
|
||||||
|
* Fixed Deposit (treasury placements with other banks) and Bond (a mix of SLR-eligible SDL and
|
||||||
|
* Non-SLR PSU/FI bonds) have no RBI-mandated percentage of their own — their split here is a
|
||||||
|
* board-approved policy choice within the room RBI's SLR/Non-SLR framework leaves.
|
||||||
|
*/
|
||||||
export const TARGET_ALLOCATION_PCT: Record<InstrumentType, number> = {
|
export const TARGET_ALLOCATION_PCT: Record<InstrumentType, number> = {
|
||||||
|
GOVT_SECURITY: 55,
|
||||||
|
BOND: 18,
|
||||||
FD: 15,
|
FD: 15,
|
||||||
GOVT_SECURITY: 35,
|
NON_SLR: 7,
|
||||||
NON_SLR: 10,
|
MUTUAL_FUND: 3,
|
||||||
BOND: 20,
|
SHARE: 2
|
||||||
MUTUAL_FUND: 12,
|
};
|
||||||
SHARE: 8
|
|
||||||
|
/** True for the instrument types whose target traces to an actual RBI rule (SLR floor / Non-SLR
|
||||||
|
* ceiling) rather than a board-only policy choice — see TARGET_ALLOCATION_PCT's doc comment. */
|
||||||
|
export const ALLOCATION_TARGET_IS_RBI_LINKED: Record<InstrumentType, boolean> = {
|
||||||
|
GOVT_SECURITY: true,
|
||||||
|
BOND: false,
|
||||||
|
FD: false,
|
||||||
|
NON_SLR: true,
|
||||||
|
MUTUAL_FUND: true,
|
||||||
|
SHARE: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InvestmentHolding {
|
export interface InvestmentHolding {
|
||||||
@@ -90,6 +119,10 @@ export interface InvestmentHolding {
|
|||||||
interestPosted?: number;
|
interestPosted?: number;
|
||||||
/** Cumulative interest actually received in cash to date. */
|
/** Cumulative interest actually received in cash to date. */
|
||||||
interestReceived?: number;
|
interestReceived?: number;
|
||||||
|
/** DD-MM-YYYY through which interest has already been calculated & posted. accruedInterestBetween()
|
||||||
|
* never re-counts days up to this date, so re-running a Calculation & Post for an overlapping
|
||||||
|
* or repeated period always yields 0 instead of double-posting the same interest. */
|
||||||
|
interestPostedTillDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvestmentTxn {
|
export interface InvestmentTxn {
|
||||||
@@ -100,6 +133,10 @@ export interface InvestmentTxn {
|
|||||||
txnDate: string;
|
txnDate: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
particulars: string;
|
particulars: string;
|
||||||
|
/** GL account this entry was posted/received against — the Interest Income GL for a Post,
|
||||||
|
* the Cash/Bank GL for a Receive. Absent on older/seeded entries that predate GL selection. */
|
||||||
|
glCode?: string;
|
||||||
|
glName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Date / interest helpers --------------------
|
// -------------------- Date / interest helpers --------------------
|
||||||
@@ -175,15 +212,6 @@ export function canAccrueInterest(h: InvestmentHolding): boolean {
|
|||||||
return !isNpi(h);
|
return !isNpi(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest still to be calculated & posted this cycle. Always 0 for an NPI holding — posting
|
|
||||||
* an accrual on a non-performing investment would overstate income. */
|
|
||||||
export function interestToPost(h: InvestmentHolding, asOnStr: string): number {
|
|
||||||
if (!canAccrueInterest(h)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.max(0, accruedInterest(h, asOnStr) - (h.interestPosted ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Outstanding "Interest Receivable" GL balance — interest already posted (booked as income)
|
/** Outstanding "Interest Receivable" GL balance — interest already posted (booked as income)
|
||||||
* but not yet actually received in cash. Always 0 for an NPI holding, since nothing is ever
|
* but not yet actually received in cash. Always 0 for an NPI holding, since nothing is ever
|
||||||
* posted as a receivable on one. */
|
* posted as a receivable on one. */
|
||||||
@@ -203,6 +231,23 @@ export function suspenseInterest(h: InvestmentHolding, asOnStr: string): number
|
|||||||
return Math.max(0, accruedInterest(h, asOnStr) - (h.interestReceived ?? 0));
|
return Math.max(0, accruedInterest(h, asOnStr) - (h.interestReceived ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interest accrued strictly within [fromStr, toStr], clipped to the holding's own
|
||||||
|
* purchase→maturity window — the basis for a period-based Interest Calculation & Post run
|
||||||
|
* (as opposed to accruedInterest(), which is always measured from purchase date). Returns 0
|
||||||
|
* for a Non-Performing holding, same accrual-basis rule as accruedInterest(). */
|
||||||
|
export function accruedInterestBetween(h: InvestmentHolding, fromStr: string, toStr: string): number {
|
||||||
|
if (!canAccrueInterest(h)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const purchase = parseDDMMYYYY(h.purchaseDate).getTime();
|
||||||
|
const maturity = parseDDMMYYYY(h.maturityDate).getTime();
|
||||||
|
const postedTill = h.interestPostedTillDate ? parseDDMMYYYY(h.interestPostedTillDate).getTime() : purchase;
|
||||||
|
const from = Math.max(parseDDMMYYYY(fromStr).getTime(), purchase, postedTill);
|
||||||
|
const to = Math.min(parseDDMMYYYY(toStr).getTime(), maturity);
|
||||||
|
const days = Math.max(0, (to - from) / (24 * 3600 * 1000));
|
||||||
|
return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25));
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDDMMYYYY(d: Date): string {
|
export function formatDDMMYYYY(d: Date): string {
|
||||||
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||||
}
|
}
|
||||||
@@ -388,6 +433,68 @@ export const FD_HOLDINGS: InvestmentHolding[] = [
|
|||||||
crToAc: '310070',
|
crToAc: '310070',
|
||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: '15628',
|
||||||
|
instrumentType: 'FD',
|
||||||
|
scriptCode: 'FD-CUM',
|
||||||
|
scriptName: 'Cumulative Fixed Deposit',
|
||||||
|
party: 'Zilla Sahakari Bank Ltd',
|
||||||
|
subType: 'NON SLR',
|
||||||
|
faceValue: 1800000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 1,
|
||||||
|
purchaseDate: '20-08-2025',
|
||||||
|
maturityDate: '20-08-2026',
|
||||||
|
interestRate: 6.8,
|
||||||
|
interestFreq: 'CUMULATIVE',
|
||||||
|
glCode: '320020',
|
||||||
|
glName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: '15629',
|
||||||
|
instrumentType: 'FD',
|
||||||
|
scriptCode: 'FD-MIP',
|
||||||
|
scriptName: 'Monthly Interest Payout FD',
|
||||||
|
party: 'Vidarbha Cooperative Bank',
|
||||||
|
subType: 'NON SLR',
|
||||||
|
faceValue: 2200000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 1,
|
||||||
|
purchaseDate: '25-09-2025',
|
||||||
|
maturityDate: '25-09-2026',
|
||||||
|
interestRate: 7.15,
|
||||||
|
interestFreq: 'MONTHLY',
|
||||||
|
glCode: '320020',
|
||||||
|
glName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
interestPosted: 143220,
|
||||||
|
interestReceived: 130800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: '15630',
|
||||||
|
instrumentType: 'FD',
|
||||||
|
scriptCode: 'FD-CUM',
|
||||||
|
scriptName: 'Cumulative Fixed Deposit',
|
||||||
|
party: 'Konkan Cooperative Bank Ltd',
|
||||||
|
subType: 'NON SLR',
|
||||||
|
faceValue: 900000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 1,
|
||||||
|
purchaseDate: '28-10-2024',
|
||||||
|
maturityDate: '28-10-2026',
|
||||||
|
interestRate: 6.75,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '320020',
|
||||||
|
glName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -515,6 +622,46 @@ export const GOVT_SECURITY_HOLDINGS: InvestmentHolding[] = [
|
|||||||
crToAc: '310070',
|
crToAc: '310070',
|
||||||
brokerCode: 'BRK-002',
|
brokerCode: 'BRK-002',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'GSEC-2007',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
scriptCode: 'TBILL-91D',
|
||||||
|
scriptName: '91 Days Treasury Bill',
|
||||||
|
party: 'RBI - SGL',
|
||||||
|
subType: 'SLR',
|
||||||
|
faceValue: 2000000,
|
||||||
|
purchaseRate: 98.2,
|
||||||
|
units: 20000,
|
||||||
|
purchaseDate: '15-05-2026',
|
||||||
|
maturityDate: '13-08-2026',
|
||||||
|
interestRate: 6.2,
|
||||||
|
interestFreq: 'CUMULATIVE',
|
||||||
|
glCode: '310020',
|
||||||
|
glName: 'INVESTMENT IN GOVT. SECURITIES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-003',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'GSEC-2008',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
scriptCode: 'GJ-SDL-2026',
|
||||||
|
scriptName: '7.85% Gujarat SDL 2026',
|
||||||
|
party: 'RBI - SGL',
|
||||||
|
subType: 'SLR',
|
||||||
|
faceValue: 1800000,
|
||||||
|
purchaseRate: 100.2,
|
||||||
|
units: 18000,
|
||||||
|
purchaseDate: '18-09-2025',
|
||||||
|
maturityDate: '18-09-2026',
|
||||||
|
interestRate: 7.85,
|
||||||
|
interestFreq: 'HALF_YEARLY',
|
||||||
|
glCode: '310020',
|
||||||
|
glName: 'INVESTMENT IN GOVT. SECURITIES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-001',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -620,6 +767,46 @@ export const NON_SLR_HOLDINGS: InvestmentHolding[] = [
|
|||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
interestPosted: 89400,
|
interestPosted: 89400,
|
||||||
interestReceived: 60000
|
interestReceived: 60000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'NSLR-3006',
|
||||||
|
instrumentType: 'NON_SLR',
|
||||||
|
scriptCode: 'IRFC-7.55-2026',
|
||||||
|
scriptName: 'IRFC Bond 7.55% 2026',
|
||||||
|
party: 'Indian Railway Finance Corp',
|
||||||
|
subType: 'NON SLR',
|
||||||
|
faceValue: 1600000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 16000,
|
||||||
|
purchaseDate: '25-08-2025',
|
||||||
|
maturityDate: '25-08-2026',
|
||||||
|
interestRate: 7.55,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310030',
|
||||||
|
glName: 'INVESTMENT IN NON SLR SECURITIES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-004',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'NSLR-3007',
|
||||||
|
instrumentType: 'NON_SLR',
|
||||||
|
scriptCode: 'NHB-7.3-2026',
|
||||||
|
scriptName: 'NHB Bond 7.30% 2026',
|
||||||
|
party: 'National Housing Bank',
|
||||||
|
subType: 'NON SLR',
|
||||||
|
faceValue: 1000000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 10000,
|
||||||
|
purchaseDate: '10-10-2025',
|
||||||
|
maturityDate: '10-10-2026',
|
||||||
|
interestRate: 7.3,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310030',
|
||||||
|
glName: 'INVESTMENT IN NON SLR SECURITIES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-003',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -726,6 +913,46 @@ export const BOND_HOLDINGS: InvestmentHolding[] = [
|
|||||||
crToAc: '310070',
|
crToAc: '310070',
|
||||||
brokerCode: 'BRK-005',
|
brokerCode: 'BRK-005',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'BOND-4006',
|
||||||
|
instrumentType: 'BOND',
|
||||||
|
scriptCode: 'CHOLA-8.35-2026',
|
||||||
|
scriptName: 'Cholamandalam Finance NCD 8.35% 2026',
|
||||||
|
party: 'Cholamandalam Investment & Finance',
|
||||||
|
subType: 'CORPORATE BOND',
|
||||||
|
faceValue: 1400000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 14000,
|
||||||
|
purchaseDate: '15-09-2024',
|
||||||
|
maturityDate: '15-09-2026',
|
||||||
|
interestRate: 8.35,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310040',
|
||||||
|
glName: 'INVESTMENT IN CORPORATE BONDS',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-006',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'BOND-4007',
|
||||||
|
instrumentType: 'BOND',
|
||||||
|
scriptCode: 'PEL-8.4-2026',
|
||||||
|
scriptName: 'Piramal Enterprises NCD 8.40% 2026',
|
||||||
|
party: 'Piramal Enterprises Ltd',
|
||||||
|
subType: 'CORPORATE BOND',
|
||||||
|
faceValue: 900000,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 9000,
|
||||||
|
purchaseDate: '28-10-2025',
|
||||||
|
maturityDate: '28-10-2026',
|
||||||
|
interestRate: 8.4,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310040',
|
||||||
|
glName: 'INVESTMENT IN CORPORATE BONDS',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-005',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -829,6 +1056,46 @@ export const MUTUAL_FUND_HOLDINGS: InvestmentHolding[] = [
|
|||||||
crToAc: '310070',
|
crToAc: '310070',
|
||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'MF-5006',
|
||||||
|
instrumentType: 'MUTUAL_FUND',
|
||||||
|
scriptCode: 'SBI-BPSU',
|
||||||
|
scriptName: 'SBI Banking & PSU Fund',
|
||||||
|
party: 'SBI Mutual Fund',
|
||||||
|
subType: 'DEBT - BANKING & PSU',
|
||||||
|
faceValue: 700000,
|
||||||
|
purchaseRate: 18.6,
|
||||||
|
units: 37634.4,
|
||||||
|
purchaseDate: '10-09-2025',
|
||||||
|
maturityDate: '10-09-2026',
|
||||||
|
interestRate: 7.1,
|
||||||
|
interestFreq: 'CUMULATIVE',
|
||||||
|
glCode: '310050',
|
||||||
|
glName: 'INVESTMENT IN MUTUAL FUNDS',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'MF-5007',
|
||||||
|
instrumentType: 'MUTUAL_FUND',
|
||||||
|
scriptCode: 'ICICI-CB',
|
||||||
|
scriptName: 'ICICI Pru Corporate Bond Fund',
|
||||||
|
party: 'ICICI Prudential Mutual Fund',
|
||||||
|
subType: 'DEBT - CORPORATE BOND',
|
||||||
|
faceValue: 1100000,
|
||||||
|
purchaseRate: 24.8,
|
||||||
|
units: 44354.84,
|
||||||
|
purchaseDate: '14-07-2026',
|
||||||
|
maturityDate: '14-07-2027',
|
||||||
|
interestRate: 7.05,
|
||||||
|
interestFreq: 'CUMULATIVE',
|
||||||
|
glCode: '310050',
|
||||||
|
glName: 'INVESTMENT IN MUTUAL FUNDS',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -932,6 +1199,46 @@ export const SHARE_HOLDINGS: InvestmentHolding[] = [
|
|||||||
crToAc: '310070',
|
crToAc: '310070',
|
||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: 'ACTIVE'
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'SH-6006',
|
||||||
|
instrumentType: 'SHARE',
|
||||||
|
scriptCode: 'ICICIBANK',
|
||||||
|
scriptName: 'ICICI Bank Ltd',
|
||||||
|
party: 'NSE/BSE',
|
||||||
|
subType: 'EQUITY',
|
||||||
|
faceValue: 420000,
|
||||||
|
purchaseRate: 1050,
|
||||||
|
units: 400,
|
||||||
|
purchaseDate: '05-03-2026',
|
||||||
|
maturityDate: '05-03-2036',
|
||||||
|
interestRate: 2.0,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310060',
|
||||||
|
glName: 'INVESTMENT IN SHARES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: 'BRK-007',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
certNo: 'SH-6007',
|
||||||
|
instrumentType: 'SHARE',
|
||||||
|
scriptCode: 'PDCC',
|
||||||
|
scriptName: 'Pune District Central Coop Bank Shares',
|
||||||
|
party: 'Pune District Central Cooperative Bank',
|
||||||
|
subType: 'CO-OP SHARE',
|
||||||
|
faceValue: 60000,
|
||||||
|
purchaseRate: 50,
|
||||||
|
units: 1200,
|
||||||
|
purchaseDate: '18-07-2025',
|
||||||
|
maturityDate: '18-07-2035',
|
||||||
|
interestRate: 5.0,
|
||||||
|
interestFreq: 'YEARLY',
|
||||||
|
glCode: '310060',
|
||||||
|
glName: 'INVESTMENT IN SHARES',
|
||||||
|
crToAc: '310070',
|
||||||
|
brokerCode: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1060,6 +1367,43 @@ export const ALL_TXNS: InvestmentTxn[] = [
|
|||||||
|
|
||||||
// -------------------- Masters --------------------
|
// -------------------- Masters --------------------
|
||||||
|
|
||||||
|
export interface InvestmentGlOption {
|
||||||
|
glCode: string;
|
||||||
|
glName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interest Income (Cr) GL per instrument type — the GL an Interest Calculation & Post run
|
||||||
|
* credits. Selectable on the FD/Govt./Bond Interest Calculation screens instead of being
|
||||||
|
* hard-wired, so a period's posting can be routed to a different income head when needed. */
|
||||||
|
export const INTEREST_INCOME_GL_MASTER: InvestmentGlOption[] = [
|
||||||
|
{ glCode: '410010', glName: 'INTEREST ON GOVT. SECURITIES' },
|
||||||
|
{ glCode: '410020', glName: 'INTEREST ON NON SLR SECURITIES' },
|
||||||
|
{ glCode: '410030', glName: 'INTEREST ON CORPORATE BONDS' },
|
||||||
|
{ glCode: '410040', glName: 'INTEREST ON FIXED DEPOSITS' },
|
||||||
|
{ glCode: '410050', glName: 'INCOME FROM MUTUAL FUNDS' },
|
||||||
|
{ glCode: '410060', glName: 'DIVIDEND ON SHARES' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Default Interest Income GL suggested per instrument type — pre-fills the selector above,
|
||||||
|
* which the user can still override before posting. */
|
||||||
|
export const INTEREST_INCOME_GL_BY_TYPE: Record<InstrumentType, string> = {
|
||||||
|
GOVT_SECURITY: '410010',
|
||||||
|
NON_SLR: '410020',
|
||||||
|
BOND: '410030',
|
||||||
|
FD: '410040',
|
||||||
|
MUTUAL_FUND: '410050',
|
||||||
|
SHARE: '410060'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Cash/Bank (Dr) GL credited when an Interest Receive entry is booked — the account the cash
|
||||||
|
* actually lands in. Selectable on the Interest Receive screen. */
|
||||||
|
export const CASH_BANK_GL_MASTER: InvestmentGlOption[] = [
|
||||||
|
{ glCode: '100010', glName: 'CASH IN HAND' },
|
||||||
|
{ glCode: '110010', glName: 'RBI CURRENT ACCOUNT' },
|
||||||
|
{ glCode: '110020', glName: 'SBI CURRENT ACCOUNT' },
|
||||||
|
{ glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' }
|
||||||
|
];
|
||||||
|
|
||||||
export interface ScriptMaster {
|
export interface ScriptMaster {
|
||||||
scriptCode: string;
|
scriptCode: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
|
|||||||
@@ -184,8 +184,11 @@
|
|||||||
<div class="pos-section-label">Investment - FD Interest Calculation & Post</div>
|
<div class="pos-section-label">Investment - FD Interest Calculation & Post</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Date</span><input type="text" [(ngModel)]="fdCalcDate" placeholder="DD-MM-YYYY"
|
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="fdCalcFromDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="fdCalcToDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Certificate No</span>
|
<span>Certificate No</span>
|
||||||
<select [(ngModel)]="fdCalcCertNo">
|
<select [(ngModel)]="fdCalcCertNo">
|
||||||
@@ -198,6 +201,14 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>ALL</span><input type="checkbox" [(ngModel)]="fdCalcAll" style="height: 18px; width: 18px"
|
><span>ALL</span><input type="checkbox" [(ngModel)]="fdCalcAll" style="height: 18px; width: 18px"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Post To (Interest Income GL)</span>
|
||||||
|
<select [(ngModel)]="fdCalcGlCode">
|
||||||
|
@for (g of interestIncomeGlOptions; track g.glCode) {
|
||||||
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Total Interest to Post</span
|
><span>Total Interest to Post</span
|
||||||
><input type="text" [value]="'₹' + (fdCalcTotal | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (fdCalcTotal | number: '1.2-2')" readonly
|
||||||
@@ -257,11 +268,14 @@
|
|||||||
<div class="pos-section-label">Investment - Govt./Bond Interest Calculation & Post</div>
|
<div class="pos-section-label">Investment - Govt./Bond Interest Calculation & Post</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Date</span><input type="text" [(ngModel)]="bondCalcDate" placeholder="DD-MM-YYYY"
|
><span>From Date</span><mifosx-date-ddmmyyyy [(ngModel)]="bondCalcFromDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To Date</span><mifosx-date-ddmmyyyy [(ngModel)]="bondCalcToDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<select [(ngModel)]="bondCalcType">
|
<select [(ngModel)]="bondCalcType" (ngModelChange)="onBondCalcTypeChange()">
|
||||||
<option value="">-- All Types --</option>
|
<option value="">-- All Types --</option>
|
||||||
<option value="GOVT_SECURITY">GOVT SECURITY</option>
|
<option value="GOVT_SECURITY">GOVT SECURITY</option>
|
||||||
<option value="NON_SLR">NON SLR</option>
|
<option value="NON_SLR">NON SLR</option>
|
||||||
@@ -281,6 +295,14 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>ALL</span><input type="checkbox" [(ngModel)]="bondCalcAll" style="height: 18px; width: 18px"
|
><span>ALL</span><input type="checkbox" [(ngModel)]="bondCalcAll" style="height: 18px; width: 18px"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Post To (Interest Income GL)</span>
|
||||||
|
<select [(ngModel)]="bondCalcGlCode">
|
||||||
|
@for (g of interestIncomeGlOptions; track g.glCode) {
|
||||||
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Total Interest to Post</span
|
><span>Total Interest to Post</span
|
||||||
><input type="text" [value]="'₹' + (bondCalcTotal | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (bondCalcTotal | number: '1.2-2')" readonly
|
||||||
@@ -342,6 +364,16 @@
|
|||||||
@if (activeTab === 'receive') {
|
@if (activeTab === 'receive') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Interest Receive — Record Actual Cash Receipt</div>
|
<div class="pos-section-label">Interest Receive — Record Actual Cash Receipt</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Receive Into (Cash/Bank GL)</span>
|
||||||
|
<select [(ngModel)]="receiveGlCode">
|
||||||
|
@for (g of cashBankGlOptions; track g.glCode) {
|
||||||
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -396,6 +428,7 @@
|
|||||||
<th>Voucher No</th>
|
<th>Voucher No</th>
|
||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
|
<th>GL Credited</th>
|
||||||
<th class="num col-in">Amount Received</th>
|
<th class="num col-in">Amount Received</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -408,17 +441,18 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
|
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
|
||||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="pg-empty">No interest received yet.</td>
|
<td colspan="6" class="pg-empty">No interest received yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td colspan="4">Total Received</td>
|
<td colspan="5">Total Received</td>
|
||||||
<td class="num">₹{{ receivedLogTotal | number: '1.2-2' }}</td>
|
<td class="num">₹{{ receivedLogTotal | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -438,6 +472,7 @@
|
|||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Instrument</th>
|
<th>Instrument</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
|
<th>GL Posted</th>
|
||||||
<th class="num col-in">Interest Amount</th>
|
<th class="num col-in">Interest Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -451,17 +486,18 @@
|
|||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ instrumentLabel(t.instrumentType) }}</td>
|
<td>{{ instrumentLabel(t.instrumentType) }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
|
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
|
||||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="pg-empty">No bond interest entries recorded yet.</td>
|
<td colspan="7" class="pg-empty">No bond interest entries recorded yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td colspan="5">Total</td>
|
<td colspan="6">Total</td>
|
||||||
<td class="num">₹{{ bondIntRegisterTotal | number: '1.2-2' }}</td>
|
<td class="num">₹{{ bondIntRegisterTotal | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -480,6 +516,7 @@
|
|||||||
<th>Voucher No</th>
|
<th>Voucher No</th>
|
||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
|
<th>GL Posted</th>
|
||||||
<th class="num col-in">Interest Amount</th>
|
<th class="num col-in">Interest Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -492,17 +529,18 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
|
<td>{{ t.glCode ? t.glCode + ' — ' + t.glName : '—' }}</td>
|
||||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="pg-empty">No FD interest entries recorded yet.</td>
|
<td colspan="6" class="pg-empty">No FD interest entries recorded yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td colspan="4">Total</td>
|
<td colspan="5">Total</td>
|
||||||
<td class="num">₹{{ fdIntRegisterTotal | number: '1.2-2' }}</td>
|
<td class="num">₹{{ fdIntRegisterTotal | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
FD_HOLDINGS,
|
FD_HOLDINGS,
|
||||||
GOVT_SECURITY_HOLDINGS,
|
GOVT_SECURITY_HOLDINGS,
|
||||||
@@ -16,6 +17,9 @@ import {
|
|||||||
BOND_HOLDINGS,
|
BOND_HOLDINGS,
|
||||||
ALL_TXNS,
|
ALL_TXNS,
|
||||||
SCRIPT_MASTER,
|
SCRIPT_MASTER,
|
||||||
|
INTEREST_INCOME_GL_MASTER,
|
||||||
|
INTEREST_INCOME_GL_BY_TYPE,
|
||||||
|
CASH_BANK_GL_MASTER,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
@@ -23,12 +27,13 @@ import {
|
|||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
PROVISION_STATUS_LABELS,
|
PROVISION_STATUS_LABELS,
|
||||||
accruedInterest,
|
accruedInterest,
|
||||||
|
accruedInterestBetween,
|
||||||
|
addPeriod,
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
maturityAmount,
|
maturityAmount,
|
||||||
todayDDMMYYYY,
|
todayDDMMYYYY,
|
||||||
isNpi,
|
isNpi,
|
||||||
canAccrueInterest,
|
canAccrueInterest,
|
||||||
interestToPost,
|
|
||||||
interestReceivableBalance,
|
interestReceivableBalance,
|
||||||
suspenseInterest,
|
suspenseInterest,
|
||||||
provisionRequiredInv
|
provisionRequiredInv
|
||||||
@@ -67,7 +72,8 @@ type InterestTab =
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentInterestComponent {
|
export class InvestmentInterestComponent {
|
||||||
@@ -83,6 +89,8 @@ export class InvestmentInterestComponent {
|
|||||||
txns: InvestmentTxn[] = [...ALL_TXNS];
|
txns: InvestmentTxn[] = [...ALL_TXNS];
|
||||||
|
|
||||||
readonly scriptOptions = SCRIPT_MASTER;
|
readonly scriptOptions = SCRIPT_MASTER;
|
||||||
|
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
|
||||||
|
readonly cashBankGlOptions = CASH_BANK_GL_MASTER;
|
||||||
|
|
||||||
/** Draft "amount to receive" per certificate, keyed by certNo, for the Interest Receive tab. */
|
/** Draft "amount to receive" per certificate, keyed by certNo, for the Interest Receive tab. */
|
||||||
receiveDrafts: Record<string, number | null> = {};
|
receiveDrafts: Record<string, number | null> = {};
|
||||||
@@ -92,17 +100,24 @@ export class InvestmentInterestComponent {
|
|||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
// 01 FD Interest Calculation
|
// 01 FD Interest Calculation — posts interest accrued strictly within [fromDate, toDate]
|
||||||
fdCalcDate = this.asOnDate;
|
fdCalcFromDate = addPeriod(this.asOnDate, 0, -3);
|
||||||
|
fdCalcToDate = this.asOnDate;
|
||||||
fdCalcCertNo = '';
|
fdCalcCertNo = '';
|
||||||
fdCalcAll = false;
|
fdCalcAll = false;
|
||||||
|
fdCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.FD;
|
||||||
|
|
||||||
// 02 Govt./Bond Int. Calculation
|
// 02 Govt./Bond Int. Calculation — posts interest accrued strictly within [fromDate, toDate]
|
||||||
bondCalcDate = this.asOnDate;
|
bondCalcFromDate = addPeriod(this.asOnDate, 0, -3);
|
||||||
|
bondCalcToDate = this.asOnDate;
|
||||||
bondCalcType: InstrumentType | '' = '';
|
bondCalcType: InstrumentType | '' = '';
|
||||||
bondCalcScript = '';
|
bondCalcScript = '';
|
||||||
bondCalcDealNo = '';
|
bondCalcDealNo = '';
|
||||||
bondCalcAll = false;
|
bondCalcAll = false;
|
||||||
|
bondCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
|
||||||
|
|
||||||
|
// Interest Receive — Cash/Bank GL credited for the day's collection voucher
|
||||||
|
receiveGlCode = CASH_BANK_GL_MASTER[0].glCode;
|
||||||
|
|
||||||
// Register filters
|
// Register filters
|
||||||
registerFromDate = '';
|
registerFromDate = '';
|
||||||
@@ -184,38 +199,56 @@ export class InvestmentInterestComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fdInterest(h: InvestmentHolding): number {
|
fdInterest(h: InvestmentHolding): number {
|
||||||
return interestToPost(h, this.fdCalcDate || this.asOnDate);
|
return accruedInterestBetween(h, this.fdCalcFromDate || h.purchaseDate, this.fdCalcToDate || this.asOnDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
get fdCalcTotal(): number {
|
get fdCalcTotal(): number {
|
||||||
return this.fdCalcTargets.reduce((s, h) => s + this.fdInterest(h), 0);
|
return this.fdCalcTargets.reduce((s, h) => s + this.fdInterest(h), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glName(glCode: string): string {
|
||||||
|
return (
|
||||||
|
this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ??
|
||||||
|
this.cashBankGlOptions.find((g) => g.glCode === glCode)?.glName ??
|
||||||
|
glCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
processFdInterest(): void {
|
processFdInterest(): void {
|
||||||
const targets = this.fdCalcTargets.filter((h) => this.fdInterest(h) > 0);
|
const targets = this.fdCalcTargets.filter((h) => this.fdInterest(h) > 0);
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
this.toast('No FD holdings have interest due to be posted.');
|
this.toast('No FD holdings have interest due to be posted for this period.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const toDate = this.fdCalcToDate || this.asOnDate;
|
||||||
|
const fromDate = this.fdCalcFromDate || toDate;
|
||||||
|
const glCode = this.fdCalcGlCode;
|
||||||
|
const glName = this.glName(glCode);
|
||||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
||||||
const amt = this.fdInterest(h);
|
const amt = this.fdInterest(h);
|
||||||
h.interestPosted = this.posted(h) + amt;
|
h.interestPosted = this.posted(h) + amt;
|
||||||
|
h.interestPostedTillDate = toDate;
|
||||||
return {
|
return {
|
||||||
certNo: h.certNo,
|
certNo: h.certNo,
|
||||||
instrumentType: 'FD',
|
instrumentType: 'FD',
|
||||||
txnType: 'INTEREST',
|
txnType: 'INTEREST',
|
||||||
voucherNo: `INV/FD-INT/${(this.fdCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`,
|
voucherNo: `INV/FD-INT/${toDate.replace(/-/g, '')}-${i + 1}`,
|
||||||
txnDate: this.fdCalcDate || this.asOnDate,
|
txnDate: toDate,
|
||||||
amount: amt,
|
amount: amt,
|
||||||
particulars: `FD interest posted (accrual) - ${h.party}`
|
particulars: `FD interest posted (accrual) for period ${fromDate} to ${toDate} - ${h.party}`,
|
||||||
|
glCode,
|
||||||
|
glName
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
this.fdCalcFromDate = toDate;
|
||||||
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
||||||
this.txns = [
|
this.txns = [
|
||||||
...newTxns,
|
...newTxns,
|
||||||
...this.txns
|
...this.txns
|
||||||
];
|
];
|
||||||
this.toast(`FD interest posted for ${targets.length} certificate(s). Total ₹${total.toLocaleString('en-IN')}.`);
|
this.toast(
|
||||||
|
`FD interest posted for ${targets.length} certificate(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- 02 Govt./Bond Int. Calculation --------------------
|
// -------------------- 02 Govt./Bond Int. Calculation --------------------
|
||||||
@@ -239,30 +272,44 @@ export class InvestmentInterestComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bondInterest(h: InvestmentHolding): number {
|
bondInterest(h: InvestmentHolding): number {
|
||||||
return interestToPost(h, this.bondCalcDate || this.asOnDate);
|
return accruedInterestBetween(h, this.bondCalcFromDate || h.purchaseDate, this.bondCalcToDate || this.asOnDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
get bondCalcTotal(): number {
|
get bondCalcTotal(): number {
|
||||||
return this.bondCalcTargets.reduce((s, h) => s + this.bondInterest(h), 0);
|
return this.bondCalcTargets.reduce((s, h) => s + this.bondInterest(h), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Re-suggests the Interest Income GL when the Type filter changes — still overridable. */
|
||||||
|
onBondCalcTypeChange(): void {
|
||||||
|
if (this.bondCalcType) {
|
||||||
|
this.bondCalcGlCode = INTEREST_INCOME_GL_BY_TYPE[this.bondCalcType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
processBondInterest(): void {
|
processBondInterest(): void {
|
||||||
const targets = this.bondCalcTargets.filter((h) => this.bondInterest(h) > 0);
|
const targets = this.bondCalcTargets.filter((h) => this.bondInterest(h) > 0);
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
this.toast('No Govt/Bond holdings have interest due to be posted.');
|
this.toast('No Govt/Bond holdings have interest due to be posted for this period.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const toDate = this.bondCalcToDate || this.asOnDate;
|
||||||
|
const fromDate = this.bondCalcFromDate || toDate;
|
||||||
|
const glCode = this.bondCalcGlCode;
|
||||||
|
const glName = this.glName(glCode);
|
||||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
||||||
const amt = this.bondInterest(h);
|
const amt = this.bondInterest(h);
|
||||||
h.interestPosted = this.posted(h) + amt;
|
h.interestPosted = this.posted(h) + amt;
|
||||||
|
h.interestPostedTillDate = toDate;
|
||||||
return {
|
return {
|
||||||
certNo: h.certNo,
|
certNo: h.certNo,
|
||||||
instrumentType: h.instrumentType,
|
instrumentType: h.instrumentType,
|
||||||
txnType: 'INTEREST',
|
txnType: 'INTEREST',
|
||||||
voucherNo: `INV/BOND-INT/${(this.bondCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`,
|
voucherNo: `INV/BOND-INT/${toDate.replace(/-/g, '')}-${i + 1}`,
|
||||||
txnDate: this.bondCalcDate || this.asOnDate,
|
txnDate: toDate,
|
||||||
amount: amt,
|
amount: amt,
|
||||||
particulars: `Coupon/interest posted (accrual) - ${h.scriptName}`
|
particulars: `Coupon/interest posted (accrual) for period ${fromDate} to ${toDate} - ${h.scriptName}`,
|
||||||
|
glCode,
|
||||||
|
glName
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
||||||
@@ -270,7 +317,10 @@ export class InvestmentInterestComponent {
|
|||||||
...newTxns,
|
...newTxns,
|
||||||
...this.txns
|
...this.txns
|
||||||
];
|
];
|
||||||
this.toast(`Interest posted for ${targets.length} holding(s). Total ₹${total.toLocaleString('en-IN')}.`);
|
this.bondCalcFromDate = toDate;
|
||||||
|
this.toast(
|
||||||
|
`Interest posted for ${targets.length} holding(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Interest Receivable (all interest-bearing types) --------------------
|
// -------------------- Interest Receivable (all interest-bearing types) --------------------
|
||||||
@@ -331,6 +381,8 @@ export class InvestmentInterestComponent {
|
|||||||
h.interestReceived = this.received(h) + amt;
|
h.interestReceived = this.received(h) + amt;
|
||||||
particulars = `Interest received on cash basis (${this.provisionLabel(h)}) - ${h.scriptName || h.party}`;
|
particulars = `Interest received on cash basis (${this.provisionLabel(h)}) - ${h.scriptName || h.party}`;
|
||||||
}
|
}
|
||||||
|
const glCode = this.receiveGlCode;
|
||||||
|
const glName = this.glName(glCode);
|
||||||
this.txns = [
|
this.txns = [
|
||||||
{
|
{
|
||||||
certNo: h.certNo,
|
certNo: h.certNo,
|
||||||
@@ -339,11 +391,13 @@ export class InvestmentInterestComponent {
|
|||||||
voucherNo: `INV/INT-RCV/${h.certNo}-${this.receivedLog.length + 1}`,
|
voucherNo: `INV/INT-RCV/${h.certNo}-${this.receivedLog.length + 1}`,
|
||||||
txnDate: this.asOnDate,
|
txnDate: this.asOnDate,
|
||||||
amount: amt,
|
amount: amt,
|
||||||
particulars
|
particulars: `${particulars} (Dr. ${glCode} - ${glName})`,
|
||||||
|
glCode,
|
||||||
|
glName
|
||||||
},
|
},
|
||||||
...this.txns
|
...this.txns
|
||||||
];
|
];
|
||||||
this.toast(`₹${amt.toLocaleString('en-IN')} interest received for ${h.certNo}.`);
|
this.toast(`₹${amt.toLocaleString('en-IN')} interest received into ${glCode} - ${glName} for ${h.certNo}.`);
|
||||||
delete this.receiveDrafts[h.certNo];
|
delete this.receiveDrafts[h.certNo];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +454,9 @@ export class InvestmentInterestComponent {
|
|||||||
t.certNo,
|
t.certNo,
|
||||||
t.instrumentType,
|
t.instrumentType,
|
||||||
t.particulars,
|
t.particulars,
|
||||||
t.amount
|
t.amount,
|
||||||
|
t.glCode ?? '',
|
||||||
|
t.glName ?? ''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +468,9 @@ export class InvestmentInterestComponent {
|
|||||||
'Cert No',
|
'Cert No',
|
||||||
'Type',
|
'Type',
|
||||||
'Particulars',
|
'Particulars',
|
||||||
'Amount'
|
'Amount',
|
||||||
|
'GL Code',
|
||||||
|
'GL Name'
|
||||||
];
|
];
|
||||||
const txnWidths = [
|
const txnWidths = [
|
||||||
14,
|
14,
|
||||||
@@ -420,7 +478,9 @@ export class InvestmentInterestComponent {
|
|||||||
14,
|
14,
|
||||||
14,
|
14,
|
||||||
42,
|
42,
|
||||||
16
|
16,
|
||||||
|
12,
|
||||||
|
28
|
||||||
];
|
];
|
||||||
const one = (
|
const one = (
|
||||||
sheetName: string,
|
sheetName: string,
|
||||||
@@ -501,72 +561,78 @@ export class InvestmentInterestComponent {
|
|||||||
return sheets;
|
return sheets;
|
||||||
}
|
}
|
||||||
case 'fdCalc':
|
case 'fdCalc':
|
||||||
return one(
|
return [
|
||||||
'FD Interest Calc',
|
{
|
||||||
'FD Interest Calculation & Post',
|
sheetName: 'FD Interest Calc',
|
||||||
[
|
title: 'FD Interest Calculation & Post',
|
||||||
'Cert No',
|
subtitle: `Period ${this.fdCalcFromDate || '—'} to ${this.fdCalcToDate || this.asOnDate} · Post to ${this.fdCalcGlCode} - ${this.glName(this.fdCalcGlCode)}`,
|
||||||
'Party',
|
headers: [
|
||||||
'Face Value',
|
'Cert No',
|
||||||
'Rate',
|
'Party',
|
||||||
'Frequency',
|
'Face Value',
|
||||||
'Interest'
|
'Rate',
|
||||||
],
|
'Frequency',
|
||||||
[
|
'Interest'
|
||||||
14,
|
],
|
||||||
30,
|
widths: [
|
||||||
16,
|
14,
|
||||||
10,
|
30,
|
||||||
14,
|
16,
|
||||||
16
|
10,
|
||||||
],
|
14,
|
||||||
this.fdCalcTargets.map((h) => [
|
16
|
||||||
h.certNo,
|
],
|
||||||
h.party,
|
rows: this.fdCalcTargets.map((h) => [
|
||||||
h.faceValue,
|
h.certNo,
|
||||||
h.interestRate,
|
h.party,
|
||||||
h.interestFreq,
|
h.faceValue,
|
||||||
this.fdInterest(h)
|
h.interestRate,
|
||||||
])
|
h.interestFreq,
|
||||||
);
|
this.fdInterest(h)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
];
|
||||||
case 'bondCalc':
|
case 'bondCalc':
|
||||||
return one(
|
return [
|
||||||
'Bond Interest Calc',
|
{
|
||||||
'Govt./Bond Interest Calculation & Post',
|
sheetName: 'Bond Interest Calc',
|
||||||
[
|
title: 'Govt./Bond Interest Calculation & Post',
|
||||||
'Cert No',
|
subtitle: `Period ${this.bondCalcFromDate || '—'} to ${this.bondCalcToDate || this.asOnDate} · Post to ${this.bondCalcGlCode} - ${this.glName(this.bondCalcGlCode)}`,
|
||||||
'Script',
|
headers: [
|
||||||
'Type',
|
'Cert No',
|
||||||
'Face Value',
|
'Script',
|
||||||
'Rate',
|
'Type',
|
||||||
'Frequency',
|
'Face Value',
|
||||||
'Interest'
|
'Rate',
|
||||||
],
|
'Frequency',
|
||||||
[
|
'Interest'
|
||||||
14,
|
],
|
||||||
30,
|
widths: [
|
||||||
16,
|
14,
|
||||||
16,
|
30,
|
||||||
10,
|
16,
|
||||||
14,
|
16,
|
||||||
16
|
10,
|
||||||
],
|
14,
|
||||||
this.bondCalcTargets.map((h) => [
|
16
|
||||||
h.certNo,
|
],
|
||||||
h.scriptName,
|
rows: this.bondCalcTargets.map((h) => [
|
||||||
this.instrumentLabel(h.instrumentType),
|
h.certNo,
|
||||||
h.faceValue,
|
h.scriptName,
|
||||||
h.interestRate,
|
this.instrumentLabel(h.instrumentType),
|
||||||
h.interestFreq,
|
h.faceValue,
|
||||||
this.bondInterest(h)
|
h.interestRate,
|
||||||
])
|
h.interestFreq,
|
||||||
);
|
this.bondInterest(h)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
];
|
||||||
case 'receive': {
|
case 'receive': {
|
||||||
const sheets: ExportSheetSpec[] = [
|
const sheets: ExportSheetSpec[] = [
|
||||||
{
|
{
|
||||||
sheetName: 'Receive Targets',
|
sheetName: 'Receive Targets',
|
||||||
title: 'Interest Receive — Outstanding for Collection',
|
title: 'Interest Receive — Outstanding for Collection',
|
||||||
subtitle,
|
subtitle: `${subtitle} · Receive into ${this.receiveGlCode} - ${this.glName(this.receiveGlCode)}`,
|
||||||
headers: [
|
headers: [
|
||||||
'Cert No',
|
'Cert No',
|
||||||
'Script / Party',
|
'Script / Party',
|
||||||
|
|||||||
@@ -267,8 +267,8 @@
|
|||||||
><span>Contact No</span><input type="text" [(ngModel)]="newBroker.contactNo"
|
><span>Contact No</span><input type="text" [(ngModel)]="newBroker.contactNo"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Empanelled On</span><input type="text" [(ngModel)]="newBroker.empanelledOn" placeholder="DD-MM-YYYY"
|
><span>Empanelled On</span><mifosx-date-ddmmyyyy [(ngModel)]="newBroker.empanelledOn"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addBroker()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" (click)="addBroker()">Save</button>
|
||||||
@@ -349,8 +349,8 @@
|
|||||||
<div class="pos-section-label">Add Daily Script Rate</div>
|
<div class="pos-section-label">Add Daily Script Rate</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Rate Date *</span><input type="text" [(ngModel)]="newDailyRate.rateDate" placeholder="DD-MM-YYYY"
|
><span>Rate Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="newDailyRate.rateDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Script Code *</span>
|
<span>Script Code *</span>
|
||||||
<select [(ngModel)]="newDailyRate.scriptCode">
|
<select [(ngModel)]="newDailyRate.scriptCode">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
SCRIPT_MASTER,
|
SCRIPT_MASTER,
|
||||||
BROKER_MASTER,
|
BROKER_MASTER,
|
||||||
@@ -60,7 +61,8 @@ type MasterTab =
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentMastersComponent {
|
export class InvestmentMastersComponent {
|
||||||
|
|||||||
@@ -138,8 +138,8 @@
|
|||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Purchase Date *</span
|
><span>Purchase Date *</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.purchaseDate" placeholder="DD-MM-YYYY"
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.purchaseDate"></mifosx-date-ddmmyyyy
|
||||||
/></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Broker Code</span><input type="text" [(ngModel)]="purchaseForm.brokerCode"
|
><span>Broker Code</span><input type="text" [(ngModel)]="purchaseForm.brokerCode"
|
||||||
/></label>
|
/></label>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data';
|
import { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data';
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ interface SharePurchaseForm {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ShareInvestmentComponent {
|
export class ShareInvestmentComponent {
|
||||||
|
|||||||
@@ -1470,16 +1470,30 @@ export function branchTargetAmount(branchCode: string): number {
|
|||||||
return BRANCH_DISBURSEMENT_TARGET[branchCode] || 0;
|
return BRANCH_DISBURSEMENT_TARGET[branchCode] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Target product mix (% of total disbursement) — weighted towards secured Housing/Vehicle
|
/**
|
||||||
* lending plus the priority-sector KCC/Cash Credit book, adding up to 100%. */
|
* Target product mix (% of total disbursement). Two of these are genuinely RBI-mandated
|
||||||
|
* Priority Sector Lending sub-targets (RBI Master Direction — Priority Sector Lending: Targets
|
||||||
|
* and Classification, FIDD.CO.Plan.BC.13/04.09.01/2020-21, as extended to UCBs) —
|
||||||
|
* • Agriculture: 18% of Adjusted Net Bank Credit (ANBC) — mapped here to Kisan Credit Card.
|
||||||
|
* • Micro Enterprises: 7.5% of ANBC — mapped here to Cash Credit (MSME).
|
||||||
|
* RBI sets these as a % of the BANK'S WHOLE loan book (ANBC), not of any one product line's own
|
||||||
|
* disbursement — shown here against this module's own mix as the closest available proxy, since
|
||||||
|
* this demo has no bank-wide ANBC figure to check against. The remaining five products (Housing,
|
||||||
|
* Vehicle, Gold, Personal, Education) have no RBI-mandated minimum share of their own — Housing
|
||||||
|
* and Education are Priority-Sector-*eligible* within RBI's loan-amount ceilings but aren't
|
||||||
|
* separately targeted, and Vehicle/Gold/Personal aren't Priority Sector categories at all — so
|
||||||
|
* their split below is an internal business-plan assumption, not a regulatory requirement.
|
||||||
|
* (RBI's other PSL sub-targets — Weaker Sections 12%, overall PSL 40% — cut across products by
|
||||||
|
* borrower category rather than by loan type, so they aren't represented as their own row here.)
|
||||||
|
*/
|
||||||
export const PRODUCT_TARGET_PCT: Record<string, number> = {
|
export const PRODUCT_TARGET_PCT: Record<string, number> = {
|
||||||
PL: 12,
|
PL: 12,
|
||||||
HL: 28,
|
HL: 30,
|
||||||
VL: 15,
|
VL: 15,
|
||||||
GL: 10,
|
GL: 12,
|
||||||
CC: 15,
|
CC: 7.5, // RBI Micro Enterprises PSL sub-target
|
||||||
KCC: 12,
|
KCC: 18, // RBI Agriculture PSL sub-target
|
||||||
EL: 8
|
EL: 5.5
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Target channel mix (% of disbursement) — the bank's push towards BC/Digital origination and
|
/** Target channel mix (% of disbursement) — the bank's push towards BC/Digital origination and
|
||||||
|
|||||||
@@ -483,10 +483,17 @@
|
|||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Product Mix — Target % vs Actual % (of Disbursed Amount)</div>
|
<div class="pos-section-label">Product Mix — Target % vs Actual % (of Disbursed Amount)</div>
|
||||||
|
<p class="pg-sub npa-report-note">
|
||||||
|
KCC and Cash Credit (MSME) targets are RBI's actual Priority Sector Lending sub-targets — Agriculture 18% and
|
||||||
|
Micro Enterprises 7.5% of Adjusted Net Bank Credit (shown here against this module's own mix, since RBI sets
|
||||||
|
them against the bank's whole loan book, not one product line). The rest are internal business-plan targets, not
|
||||||
|
RBI-mandated.
|
||||||
|
</p>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Product</th>
|
<th>Product</th>
|
||||||
|
<th>Target Basis</th>
|
||||||
<th class="num">Target %</th>
|
<th class="num">Target %</th>
|
||||||
<th class="num col-in">Actual %</th>
|
<th class="num col-in">Actual %</th>
|
||||||
<th class="num">Variance</th>
|
<th class="num">Variance</th>
|
||||||
@@ -497,7 +504,12 @@
|
|||||||
@for (row of productTargetRows; track row.code) {
|
@for (row of productTargetRows; track row.code) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.label }}</td>
|
<td>{{ row.label }}</td>
|
||||||
<td class="num">{{ row.targetPct | number: '1.0-0' }}%</td>
|
<td>
|
||||||
|
<span class="loan-badge" [class.loan-badge--progress]="row.isRbiMandated">
|
||||||
|
{{ row.isRbiMandated ? 'RBI PSL Sub-Target' : 'Internal Target' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ row.targetPct | number: '1.0-1' }}%</td>
|
||||||
<td class="num col-in">{{ row.actualPct | number: '1.1-1' }}%</td>
|
<td class="num col-in">{{ row.actualPct | number: '1.1-1' }}%</td>
|
||||||
<td class="num" [class.col-out]="row.variancePct < 0" [class.col-in]="row.variancePct > 0">
|
<td class="num" [class.col-out]="row.variancePct < 0" [class.col-in]="row.variancePct > 0">
|
||||||
{{ row.variancePct > 0 ? '+' : '' }}{{ row.variancePct | number: '1.1-1' }} pp
|
{{ row.variancePct > 0 ? '+' : '' }}{{ row.variancePct | number: '1.1-1' }} pp
|
||||||
@@ -533,7 +545,7 @@
|
|||||||
@for (row of channelTargetRows; track row.code) {
|
@for (row of channelTargetRows; track row.code) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.label }}</td>
|
<td>{{ row.label }}</td>
|
||||||
<td class="num">{{ row.targetPct | number: '1.0-0' }}%</td>
|
<td class="num">{{ row.targetPct | number: '1.0-1' }}%</td>
|
||||||
<td class="num col-in">{{ row.actualPct | number: '1.1-1' }}%</td>
|
<td class="num col-in">{{ row.actualPct | number: '1.1-1' }}%</td>
|
||||||
<td class="num" [class.col-out]="row.variancePct < 0" [class.col-in]="row.variancePct > 0">
|
<td class="num" [class.col-out]="row.variancePct < 0" [class.col-in]="row.variancePct > 0">
|
||||||
{{ row.variancePct > 0 ? '+' : '' }}{{ row.variancePct | number: '1.1-1' }} pp
|
{{ row.variancePct > 0 ? '+' : '' }}{{ row.variancePct | number: '1.1-1' }} pp
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ interface MixTargetRow {
|
|||||||
actualPct: number;
|
actualPct: number;
|
||||||
variancePct: number;
|
variancePct: number;
|
||||||
status: TargetStatus;
|
status: TargetStatus;
|
||||||
|
/** True only for the two rows whose target is an actual RBI PSL sub-target (Agriculture 18% /
|
||||||
|
* Micro Enterprises 7.5% of ANBC) rather than an internal business-plan assumption. */
|
||||||
|
isRbiMandated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewId = 'pipeline' | 'new' | 'channel' | 'bc' | 'share' | 'target';
|
type ViewId = 'pipeline' | 'new' | 'channel' | 'bc' | 'share' | 'target';
|
||||||
@@ -389,7 +392,8 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
targetPct,
|
targetPct,
|
||||||
actualPct,
|
actualPct,
|
||||||
variancePct: actualPct - targetPct,
|
variancePct: actualPct - targetPct,
|
||||||
status: targetStatus(actualPct, targetPct, 3)
|
status: targetStatus(actualPct, targetPct, 3),
|
||||||
|
isRbiMandated: p.code === 'KCC' || p.code === 'CC'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -413,7 +417,8 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
targetPct,
|
targetPct,
|
||||||
actualPct,
|
actualPct,
|
||||||
variancePct: actualPct - targetPct,
|
variancePct: actualPct - targetPct,
|
||||||
status: targetStatus(actualPct, targetPct, 3)
|
status: targetStatus(actualPct, targetPct, 3),
|
||||||
|
isRbiMandated: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -626,6 +631,7 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
subtitle,
|
subtitle,
|
||||||
headers: [
|
headers: [
|
||||||
'Mix',
|
'Mix',
|
||||||
|
'Target Basis',
|
||||||
'Target %',
|
'Target %',
|
||||||
'Actual %',
|
'Actual %',
|
||||||
'Variance (pp)',
|
'Variance (pp)',
|
||||||
@@ -633,6 +639,7 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
26,
|
||||||
|
18,
|
||||||
12,
|
12,
|
||||||
12,
|
12,
|
||||||
14,
|
14,
|
||||||
@@ -641,6 +648,7 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
rows: [
|
rows: [
|
||||||
...this.productTargetRows.map((r) => [
|
...this.productTargetRows.map((r) => [
|
||||||
`Product — ${r.label}`,
|
`Product — ${r.label}`,
|
||||||
|
r.isRbiMandated ? 'RBI PSL Sub-Target' : 'Internal Target',
|
||||||
r.targetPct,
|
r.targetPct,
|
||||||
Number(r.actualPct.toFixed(1)),
|
Number(r.actualPct.toFixed(1)),
|
||||||
Number(r.variancePct.toFixed(1)),
|
Number(r.variancePct.toFixed(1)),
|
||||||
@@ -648,6 +656,7 @@ export class LoanPipelineComponent implements OnInit {
|
|||||||
]),
|
]),
|
||||||
...this.channelTargetRows.map((r) => [
|
...this.channelTargetRows.map((r) => [
|
||||||
`Channel — ${r.label}`,
|
`Channel — ${r.label}`,
|
||||||
|
'Internal Target',
|
||||||
r.targetPct,
|
r.targetPct,
|
||||||
Number(r.actualPct.toFixed(1)),
|
Number(r.actualPct.toFixed(1)),
|
||||||
Number(r.variancePct.toFixed(1)),
|
Number(r.variancePct.toFixed(1)),
|
||||||
|
|||||||
@@ -264,16 +264,22 @@
|
|||||||
<div class="pos-section-label">Record a Recovery Against an NPA Account</div>
|
<div class="pos-section-label">Record a Recovery Against an NPA Account</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* NPA Account</span>
|
<span>Account Type</span>
|
||||||
<select [(ngModel)]="newRecovery.accountNo">
|
<select [ngModel]="recoveryProductFilter" (ngModelChange)="setRecoveryProductFilter($event)">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- All Account Types --</option>
|
||||||
@for (a of recoveryOptions; track a.accountNo) {
|
@for (p of recoveryProductOptions; track p) {
|
||||||
<option [value]="a.accountNo">
|
<option [value]="p">{{ p }}</option>
|
||||||
{{ a.accountNo }} — {{ a.customerName }} ({{ stageLabel(stage(a)) }})
|
|
||||||
</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* NPA Account</span>
|
||||||
|
<mifosx-search-select
|
||||||
|
[options]="recoveryAccountSearchOptions"
|
||||||
|
[(ngModel)]="newRecovery.accountNo"
|
||||||
|
placeholder="Search account no. or customer name…"
|
||||||
|
></mifosx-search-select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Recovery Date</span>
|
<span>* Recovery Date</span>
|
||||||
<input type="text" [(ngModel)]="newRecovery.date" placeholder="DD-MM-YYYY" />
|
<input type="text" [(ngModel)]="newRecovery.date" placeholder="DD-MM-YYYY" />
|
||||||
@@ -299,45 +305,66 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">
|
<div
|
||||||
NPA Recovery Register — ₹{{ totalRecoveredAmount | number: '1.0-0' }} recovered against ₹{{
|
class="pos-section-label"
|
||||||
gnpaAmount | number: '1.0-0'
|
style="display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap"
|
||||||
}}
|
>
|
||||||
GNPA outstanding
|
<span>
|
||||||
|
NPA Recovery Register — ₹{{ totalRecoveredAmount | number: '1.0-0' }} recovered against ₹{{
|
||||||
|
gnpaAmount | number: '1.0-0'
|
||||||
|
}}
|
||||||
|
GNPA outstanding
|
||||||
|
</span>
|
||||||
|
@if (recoveryProductFilter) {
|
||||||
|
<span class="pg-chip">Filtered: {{ recoveryProductFilter }}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="pg-sub" style="padding: 8px 16px 12px">
|
||||||
|
Target Recovery is the account's contractual due this month (EMI / interest-servicing / bullet amount) — same
|
||||||
|
basis as the Recovery Management Dashboard.
|
||||||
|
</p>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
|
<th>Account Type</th>
|
||||||
|
<th>NPA Category</th>
|
||||||
<th>Account No</th>
|
<th>Account No</th>
|
||||||
<th>Customer</th>
|
<th>Customer</th>
|
||||||
<th>Branch</th>
|
<th>Branch</th>
|
||||||
<th>Mode</th>
|
<th>Mode</th>
|
||||||
<th class="num col-in">Recovered Amount</th>
|
<th class="num">Target Recovery</th>
|
||||||
|
<th class="num col-in">Recovered Amount (Actual)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (r of paged(recoveryRows); track r.accountNo + r.date + r.amount) {
|
@for (r of paged(recoveryRows); track r.accountNo + r.date + r.amount) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ r.date }}</td>
|
<td>{{ r.date }}</td>
|
||||||
|
<td>{{ r.productName }}</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="r.npaCategoryClass">{{ r.npaCategory }}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ r.accountNo }}</span>
|
<span class="pg-chip">{{ r.accountNo }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ r.customerName }}</td>
|
<td>{{ r.customerName }}</td>
|
||||||
<td>{{ r.branchName }}</td>
|
<td>{{ r.branchName }}</td>
|
||||||
<td>{{ r.mode }}</td>
|
<td>{{ r.mode }}</td>
|
||||||
|
<td class="num">₹{{ r.targetRecovery | number: '1.0-0' }}</td>
|
||||||
<td class="num col-in">₹{{ r.amount | number: '1.0-0' }}</td>
|
<td class="num col-in">₹{{ r.amount | number: '1.0-0' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="pg-empty">No recoveries recorded yet.</td>
|
<td colspan="9" class="pg-empty">No recoveries recorded yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td colspan="5">Total Recovered</td>
|
<td colspan="7">Total (Target once per account · Actual across all entries shown)</td>
|
||||||
<td class="num">₹{{ totalRecoveredAmount | number: '1.0-0' }}</td>
|
<td class="num">₹{{ recoveryTargetTotal | number: '1.0-0' }}</td>
|
||||||
|
<td class="num">₹{{ recoveryActualTotal | number: '1.0-0' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
import { ExportSheetSpec, exportWorkbook } from '../../cashier/cashier-export';
|
import { ExportSheetSpec, exportWorkbook } from '../../cashier/cashier-export';
|
||||||
|
import { SearchSelectComponent, SearchSelectOption } from '../search-select/search-select.component';
|
||||||
import {
|
import {
|
||||||
LOAN_ACCOUNTS,
|
LOAN_ACCOUNTS,
|
||||||
BRANCHES,
|
BRANCHES,
|
||||||
@@ -79,9 +80,16 @@ interface RecoveryRow {
|
|||||||
accountNo: string;
|
accountNo: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
branchName: string;
|
branchName: string;
|
||||||
|
productName: string;
|
||||||
|
npaCategory: string;
|
||||||
|
npaCategoryClass: string;
|
||||||
date: string;
|
date: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
/** What the account was contractually due to pay this month (EMI / interest-servicing /
|
||||||
|
* bullet), the same "Target" basis used by the Recovery Management Dashboard — shown
|
||||||
|
* alongside the actual recovered amount so a desk can see over/under-achievement per entry. */
|
||||||
|
targetRecovery: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RepaymentTypeSummaryRow {
|
interface RepaymentTypeSummaryRow {
|
||||||
@@ -119,7 +127,8 @@ interface AgeingBucketRow {
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
FontAwesomeModule
|
FontAwesomeModule,
|
||||||
|
SearchSelectComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class NpaReportsComponent implements OnInit, OnDestroy {
|
export class NpaReportsComponent implements OnInit, OnDestroy {
|
||||||
@@ -457,25 +466,75 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
|
|||||||
return this.npaAccounts.filter((a) => a.recoveries.length > 0);
|
return this.npaAccounts.filter((a) => a.recoveries.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** '' = All Account Types. One shared filter drives both the "NPA Account" picker above (so
|
||||||
|
* it's quicker to find the right account when adding a new entry) and the Recovery Register
|
||||||
|
* below — a single control instead of two that could drift out of sync. */
|
||||||
|
recoveryProductFilter = '';
|
||||||
|
|
||||||
|
get recoveryProductOptions(): string[] {
|
||||||
|
return [...new Set(this.npaAccounts.map((a) => a.productName))].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecoveryProductFilter(productName: string): void {
|
||||||
|
this.recoveryProductFilter = productName;
|
||||||
|
this.currentPage = 1;
|
||||||
|
if (!this.recoveryOptions.some((a) => a.accountNo === this.newRecovery.accountNo)) {
|
||||||
|
this.newRecovery.accountNo = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NPA accounts selectable in "Record a Recovery", narrowed by the Account Type filter. */
|
||||||
get recoveryOptions(): LoanAccount[] {
|
get recoveryOptions(): LoanAccount[] {
|
||||||
return this.npaAccounts;
|
return this.npaAccounts.filter((a) => !this.recoveryProductFilter || a.productName === this.recoveryProductFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** recoveryOptions, reshaped for the searchable NPA Account picker — matches on account no,
|
||||||
|
* customer name and branch, not just the collapsed label text. */
|
||||||
|
get recoveryAccountSearchOptions(): SearchSelectOption[] {
|
||||||
|
return this.recoveryOptions.map((a) => ({
|
||||||
|
value: a.accountNo,
|
||||||
|
label: `${a.accountNo} — ${a.customerName} (${this.stageLabel(this.stage(a))})`,
|
||||||
|
searchText: `${a.accountNo} ${a.customerName} ${a.productName} ${a.branchName}`
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
get recoveryRows(): RecoveryRow[] {
|
get recoveryRows(): RecoveryRow[] {
|
||||||
return this.recoveryAccounts
|
return this.recoveryAccounts
|
||||||
|
.filter((a) => !this.recoveryProductFilter || a.productName === this.recoveryProductFilter)
|
||||||
.flatMap((a) =>
|
.flatMap((a) =>
|
||||||
a.recoveries.map((r) => ({
|
a.recoveries.map((r) => ({
|
||||||
accountNo: a.accountNo,
|
accountNo: a.accountNo,
|
||||||
customerName: a.customerName,
|
customerName: a.customerName,
|
||||||
branchName: a.branchName,
|
branchName: a.branchName,
|
||||||
|
productName: a.productName,
|
||||||
|
npaCategory: this.stageLabel(this.stage(a)),
|
||||||
|
npaCategoryClass: this.badgeClass(this.stage(a)),
|
||||||
date: r.date,
|
date: r.date,
|
||||||
amount: r.amount,
|
amount: r.amount,
|
||||||
mode: r.mode
|
mode: r.mode,
|
||||||
|
targetRecovery: expectedRecoveryThisMonth(a)
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
.sort((a, b) => b.amount - a.amount);
|
.sort((a, b) => b.amount - a.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The accounts behind recoveryRows, honouring the Account Type filter — used to total the
|
||||||
|
* monthly Target once per account rather than once per recovery transaction (an account with
|
||||||
|
* two recovery entries must not have its target counted twice). */
|
||||||
|
get recoveryFilteredAccounts(): LoanAccount[] {
|
||||||
|
return this.recoveryAccounts.filter(
|
||||||
|
(a) => !this.recoveryProductFilter || a.productName === this.recoveryProductFilter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get recoveryTargetTotal(): number {
|
||||||
|
return this.recoveryFilteredAccounts.reduce((s, a) => s + expectedRecoveryThisMonth(a), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get recoveryActualTotal(): number {
|
||||||
|
return this.recoveryRows.reduce((s, r) => s + r.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- Portfolio-wide KPIs (shown on every screen) --------------------
|
// -------------------- Portfolio-wide KPIs (shown on every screen) --------------------
|
||||||
get totalAdvances(): number {
|
get totalAdvances(): number {
|
||||||
return this.allAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
return this.allAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||||
@@ -616,8 +675,11 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- NPA Summary Report: stage-wise + branch-wise cuts --------------------
|
// -------------------- NPA Summary Report: stage-wise + branch-wise cuts --------------------
|
||||||
|
/** SMA-0/1/2 are early-warning "special mention" buckets, not NPA stages — this report only
|
||||||
|
* shows Standard vs the actual NPA classification ladder (Sub-Standard through Loss). SMA
|
||||||
|
* accounts still have their own dedicated Day-wise Overdue report. */
|
||||||
get stageSummary(): StageSummaryRow[] {
|
get stageSummary(): StageSummaryRow[] {
|
||||||
return STAGE_ORDER.map((stage) => {
|
return STAGE_ORDER.filter((stage) => !isSmaStage(stage)).map((stage) => {
|
||||||
const accts = this.allAccounts.filter((a) => this.stage(a) === stage);
|
const accts = this.allAccounts.filter((a) => this.stage(a) === stage);
|
||||||
return {
|
return {
|
||||||
stage,
|
stage,
|
||||||
@@ -649,15 +711,15 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get grandTotalAccounts(): number {
|
get grandTotalAccounts(): number {
|
||||||
return this.allAccounts.length;
|
return this.stageSummary.reduce((s, row) => s + row.count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get grandTotalOutstanding(): number {
|
get grandTotalOutstanding(): number {
|
||||||
return this.allAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
return this.stageSummary.reduce((s, row) => s + row.outstanding, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get grandTotalProvision(): number {
|
get grandTotalProvision(): number {
|
||||||
return this.allAccounts.reduce((s, a) => s + this.provisionReq(a), 0);
|
return this.stageSummary.reduce((s, row) => s + row.provisionReq, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get bankWideTotalAccounts(): number {
|
get bankWideTotalAccounts(): number {
|
||||||
@@ -865,39 +927,53 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
|
|||||||
{
|
{
|
||||||
sheetName: 'NPA Recovery',
|
sheetName: 'NPA Recovery',
|
||||||
title: 'NPA Recovery Register',
|
title: 'NPA Recovery Register',
|
||||||
subtitle,
|
subtitle: this.recoveryProductFilter
|
||||||
|
? `${subtitle} · Account Type: ${this.recoveryProductFilter}`
|
||||||
|
: subtitle,
|
||||||
headers: [
|
headers: [
|
||||||
'Date',
|
'Date',
|
||||||
|
'Account Type',
|
||||||
|
'NPA Category',
|
||||||
'Account No',
|
'Account No',
|
||||||
'Customer',
|
'Customer',
|
||||||
'Branch',
|
'Branch',
|
||||||
'Mode',
|
'Mode',
|
||||||
'Recovered Amount'
|
'Target Recovery',
|
||||||
|
'Recovered Amount (Actual)'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
12,
|
12,
|
||||||
|
18,
|
||||||
|
14,
|
||||||
16,
|
16,
|
||||||
22,
|
22,
|
||||||
16,
|
16,
|
||||||
18,
|
18,
|
||||||
18
|
16,
|
||||||
|
20
|
||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
...this.recoveryRows.map((r) => [
|
...this.recoveryRows.map((r) => [
|
||||||
r.date,
|
r.date,
|
||||||
|
r.productName,
|
||||||
|
r.npaCategory,
|
||||||
r.accountNo,
|
r.accountNo,
|
||||||
r.customerName,
|
r.customerName,
|
||||||
r.branchName,
|
r.branchName,
|
||||||
r.mode,
|
r.mode,
|
||||||
|
r.targetRecovery,
|
||||||
r.amount
|
r.amount
|
||||||
]),
|
]),
|
||||||
[
|
[
|
||||||
'Total Recovered',
|
'Total (Target once per account · Actual across all entries)',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
this.totalRecoveredAmount
|
'',
|
||||||
|
'',
|
||||||
|
this.recoveryTargetTotal,
|
||||||
|
this.recoveryActualTotal
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, ElementRef, forwardRef, HostListener, Input, ViewChild } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
|
export interface SearchSelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
/** What typed text is matched against — defaults to `label` when omitted, but a caller can
|
||||||
|
* pass a richer string (e.g. account no + customer name + branch) so a search hits fields
|
||||||
|
* that aren't all visible in the collapsed label. */
|
||||||
|
searchText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for a plain `<select>` when the option list is long enough that scanning
|
||||||
|
* it is impractical (e.g. hundreds of loan accounts) — a text box that filters the option list
|
||||||
|
* as you type, instead of a native dropdown you can only scroll through. Selection only commits
|
||||||
|
* on click; typing without picking an option reverts the box to the last committed selection on
|
||||||
|
* blur, so the bound ngModel value never drifts from an actual, valid option.
|
||||||
|
*
|
||||||
|
* The option list is positioned `fixed` and anchored to the input's own on-screen coordinates
|
||||||
|
* (recomputed on open/scroll/resize) rather than `absolute` inside the component — most of this
|
||||||
|
* app's cards use `overflow: hidden` for rounded corners, which would otherwise clip a dropdown
|
||||||
|
* that extends past the card's edge.
|
||||||
|
*
|
||||||
|
* Usage: <mifosx-search-select [options]="opts" [(ngModel)]="selectedValue"></mifosx-search-select>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-search-select',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => SearchSelectComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="inv-search-select">
|
||||||
|
<input
|
||||||
|
#inputEl
|
||||||
|
type="text"
|
||||||
|
class="inv-search-select__input"
|
||||||
|
[value]="query"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[disabled]="disabled"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
(focus)="onFocus()"
|
||||||
|
(blur)="onBlur()"
|
||||||
|
/>
|
||||||
|
@if (open) {
|
||||||
|
<ul
|
||||||
|
class="inv-search-select__list"
|
||||||
|
[style.top.px]="listTop"
|
||||||
|
[style.left.px]="listLeft"
|
||||||
|
[style.width.px]="listWidth"
|
||||||
|
(mousedown)="$event.preventDefault()"
|
||||||
|
>
|
||||||
|
@for (o of filteredOptions; track o.value) {
|
||||||
|
<li
|
||||||
|
class="inv-search-select__option"
|
||||||
|
[class.inv-search-select__option--active]="o.value === value"
|
||||||
|
(click)="selectOption(o)"
|
||||||
|
>
|
||||||
|
{{ o.label }}
|
||||||
|
</li>
|
||||||
|
} @empty {
|
||||||
|
<li class="inv-search-select__empty">No matches</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
/* Angular view encapsulation stops the host page's shared styles from reaching inside this
|
||||||
|
* component's own template, so its look is re-declared here rather than inherited. */
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-search-select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-search-select__input {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #0d1b2a;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.inv-search-select__list {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px;
|
||||||
|
list-style: none;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(13, 27, 42, 0.18);
|
||||||
|
}
|
||||||
|
.inv-search-select__option {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #0d1b2a;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.inv-search-select__option:hover,
|
||||||
|
.inv-search-select__option--active {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
.inv-search-select__empty {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SearchSelectComponent implements ControlValueAccessor {
|
||||||
|
@Input() options: SearchSelectOption[] = [];
|
||||||
|
@Input() placeholder = 'Search…';
|
||||||
|
|
||||||
|
@ViewChild('inputEl') private inputEl?: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
value = '';
|
||||||
|
query = '';
|
||||||
|
open = false;
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
|
listTop = 0;
|
||||||
|
listLeft = 0;
|
||||||
|
listWidth = 0;
|
||||||
|
|
||||||
|
private onChangeFn: (v: string) => void = () => {};
|
||||||
|
onTouched: () => void = () => {};
|
||||||
|
|
||||||
|
get filteredOptions(): SearchSelectOption[] {
|
||||||
|
const q = this.query.trim().toLowerCase();
|
||||||
|
if (!q) {
|
||||||
|
return this.options;
|
||||||
|
}
|
||||||
|
return this.options.filter((o) => (o.searchText || o.label).toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
private labelFor(value: string): string {
|
||||||
|
return this.options.find((o) => o.value === value)?.label ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(v: string): void {
|
||||||
|
this.value = v ?? '';
|
||||||
|
this.query = this.labelFor(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: (v: string) => void): void {
|
||||||
|
this.onChangeFn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: () => void): void {
|
||||||
|
this.onTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anchors the fixed-position list to the input's current on-screen box. */
|
||||||
|
private updateListPosition(): void {
|
||||||
|
const el = this.inputEl?.nativeElement;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
this.listTop = rect.bottom + 4;
|
||||||
|
this.listLeft = rect.left;
|
||||||
|
this.listWidth = rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:scroll')
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onWindowChange(): void {
|
||||||
|
if (this.open) {
|
||||||
|
this.updateListPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(event: Event): void {
|
||||||
|
this.query = (event.target as HTMLInputElement).value;
|
||||||
|
this.open = true;
|
||||||
|
this.updateListPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus(): void {
|
||||||
|
this.open = true;
|
||||||
|
this.updateListPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur(): void {
|
||||||
|
this.open = false;
|
||||||
|
this.onTouched();
|
||||||
|
// Discard any uncommitted typed text — only a clicked option ever changes `value`.
|
||||||
|
this.query = this.labelFor(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOption(o: SearchSelectOption): void {
|
||||||
|
this.value = o.value;
|
||||||
|
this.query = o.label;
|
||||||
|
this.open = false;
|
||||||
|
this.onChangeFn(this.value);
|
||||||
|
this.onTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user