feat: add investment reports, bond components, dashboard, and service implementation
This commit is contained in:
@@ -74,7 +74,11 @@ import {
|
|||||||
InvestmentNonSlrLimitDto,
|
InvestmentNonSlrLimitDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
toHolding
|
toHolding,
|
||||||
|
toInterestTxn,
|
||||||
|
toPurchaseTxn,
|
||||||
|
toSaleTxn,
|
||||||
|
toTransferTxn
|
||||||
} from '../investment.service';
|
} from '../investment.service';
|
||||||
|
|
||||||
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
|
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
|
||||||
@@ -222,7 +226,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
this.investmentService.getPurchases().subscribe({
|
this.investmentService.getPurchases().subscribe({
|
||||||
next: (purchases) => {
|
next: (purchases) => {
|
||||||
this.holdings = purchases.map(toHolding);
|
this.holdings = purchases.map(toHolding);
|
||||||
this.txns = purchases.map((d) => this.purchaseTxn(d));
|
this.txns = purchases.map(toPurchaseTxn);
|
||||||
this.purchaseForm = this.blankPurchaseForm();
|
this.purchaseForm = this.blankPurchaseForm();
|
||||||
this.loadNonSlrLimit();
|
this.loadNonSlrLimit();
|
||||||
this.loadInterestLog();
|
this.loadInterestLog();
|
||||||
@@ -253,115 +257,13 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
this.txns = [
|
this.txns = [
|
||||||
...others,
|
...others,
|
||||||
...postings.map((x) => this.interestTxn(x)),
|
...postings.map(toInterestTxn),
|
||||||
...sales.map((x) => this.saleTxn(x)),
|
...sales.map(toSaleTxn),
|
||||||
...transfers.map((x) => this.transferTxn(x))
|
...transfers.map(toTransferTxn)
|
||||||
].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A security transfer shows the book value carried to the new security. */
|
|
||||||
private transferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
|
|
||||||
return {
|
|
||||||
certNo: x.certNo,
|
|
||||||
instrumentType: x.bondType as InstrumentType,
|
|
||||||
txnType: 'TRANSFER',
|
|
||||||
voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`,
|
|
||||||
txnDate: x.transferDate,
|
|
||||||
amount: x.bookValue,
|
|
||||||
particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`,
|
|
||||||
glCode: x.toGlCode,
|
|
||||||
glName: x.glTransactionId
|
|
||||||
? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})`
|
|
||||||
: `${x.toGlName} (same GL, no entry)`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */
|
|
||||||
private saleTxn(x: InvestmentSaleDto): InvestmentTxn {
|
|
||||||
if (x.saleType === 'AMORTISE') {
|
|
||||||
return {
|
|
||||||
certNo: x.certNo,
|
|
||||||
instrumentType: x.bondType as InstrumentType,
|
|
||||||
txnType: 'AMORTIZE',
|
|
||||||
voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`,
|
|
||||||
txnDate: x.settlementDate,
|
|
||||||
amount: x.bookValueSold,
|
|
||||||
particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`,
|
|
||||||
glCode: '500604',
|
|
||||||
glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (x.saleType === 'WRITEOFF') {
|
|
||||||
return {
|
|
||||||
certNo: x.certNo,
|
|
||||||
instrumentType: x.bondType as InstrumentType,
|
|
||||||
txnType: 'WRITE_OFF',
|
|
||||||
voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`,
|
|
||||||
txnDate: x.settlementDate,
|
|
||||||
amount: x.bookValueSold,
|
|
||||||
particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`,
|
|
||||||
glCode: '500605',
|
|
||||||
glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const redeem = x.saleType === 'REDEEM';
|
|
||||||
const pl =
|
|
||||||
x.profitLoss >= 0
|
|
||||||
? `profit ₹${x.profitLoss}`
|
|
||||||
: redeem
|
|
||||||
? `premium written off ₹${-x.profitLoss}`
|
|
||||||
: `loss ₹${-x.profitLoss}`;
|
|
||||||
return {
|
|
||||||
certNo: x.certNo,
|
|
||||||
instrumentType: x.bondType as InstrumentType,
|
|
||||||
txnType: x.saleType,
|
|
||||||
voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`,
|
|
||||||
txnDate: x.settlementDate,
|
|
||||||
amount: x.netAmount,
|
|
||||||
particulars: redeem
|
|
||||||
? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`
|
|
||||||
: `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`,
|
|
||||||
glCode: x.status === 'SETTLED' ? x.bankGlCode : '',
|
|
||||||
glName:
|
|
||||||
x.status === 'SETTLED'
|
|
||||||
? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
|
||||||
: 'Not posted (pending settlement)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
|
|
||||||
private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
|
|
||||||
const coupon = x.lineType === 'COUPON';
|
|
||||||
return {
|
|
||||||
certNo: x.certNo,
|
|
||||||
instrumentType: x.bondType as InstrumentType,
|
|
||||||
txnType: 'INTEREST',
|
|
||||||
voucherNo: x.voucherNo,
|
|
||||||
txnDate: x.intDate,
|
|
||||||
amount: coupon ? x.grossInterest : x.amount,
|
|
||||||
particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`,
|
|
||||||
glCode: x.drGlCode,
|
|
||||||
glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
|
|
||||||
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
|
|
||||||
const posted = !!d.glTransactionId;
|
|
||||||
return {
|
|
||||||
certNo: d.certNo,
|
|
||||||
instrumentType: d.bondType,
|
|
||||||
txnType: 'PURCHASE',
|
|
||||||
voucherNo: `INV/PUR/${d.dealNo}`,
|
|
||||||
txnDate: d.settlementDate,
|
|
||||||
amount: d.settlementAmount,
|
|
||||||
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
|
|
||||||
glCode: posted ? d.investmentGlCode : '',
|
|
||||||
glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
activeTab: BondTab = 'holdings';
|
activeTab: BondTab = 'holdings';
|
||||||
/** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */
|
/** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */
|
||||||
showMoreDetails = false;
|
showMoreDetails = false;
|
||||||
|
|||||||
@@ -12,34 +12,50 @@
|
|||||||
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
|
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
|
||||||
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
|
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
|
||||||
<p class="pg-sub">
|
<p class="pg-sub">
|
||||||
Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on
|
What the bank holds today across Govt Securities, FDs with banks, Non-SLR, Bonds, Mutual Funds and Shares, as on
|
||||||
{{ asOnDate }}.
|
{{ asOnDate }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-toolbar">
|
<div class="pg-toolbar">
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating || loading" (click)="exportExcel()">
|
||||||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@if (error) {
|
||||||
|
<div class="pg-card">
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="inv-kpis">
|
<div class="inv-kpis">
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Total Holdings</span>
|
<span class="inv-kpi__label">Holdings</span>
|
||||||
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
||||||
<span class="inv-kpi__sub">{{ activeCount }} active · {{ maturedCount }} matured</span>
|
<span class="inv-kpi__sub"
|
||||||
|
>{{ maturingSoon.length }} due within 90 days{{ overdueCount ? ' (' + overdueCount + ' overdue)' : '' }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Total Invested (Face Value)</span>
|
<span class="inv-kpi__label">Total Invested (Book Value)</span>
|
||||||
<span class="inv-kpi__value">₹{{ totalFaceValue | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value">₹{{ totalBookValue | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub">Face value ₹{{ totalFaceValue | number: '1.2-2' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Accrued Interest / Income</span>
|
<span class="inv-kpi__label">Interest Receivable</span>
|
||||||
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub"
|
||||||
|
>Earned till today, not received · ₹{{ totalProvisioned | number: '1.2-2' }} provisioned</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Projected Value at Maturity</span>
|
<span class="inv-kpi__label">Value at Maturity</span>
|
||||||
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub">Securities at face value, FDs with interest</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,7 +67,9 @@
|
|||||||
<th>Instrument Type</th>
|
<th>Instrument Type</th>
|
||||||
<th class="num">Holdings</th>
|
<th class="num">Holdings</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th class="num col-in">Accrued Interest</th>
|
<th class="num">Book Value</th>
|
||||||
|
<th class="num">Share</th>
|
||||||
|
<th class="num col-in">Interest Receivable</th>
|
||||||
<th class="num col-net">Maturity Value</th>
|
<th class="num col-net">Maturity Value</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -62,6 +80,8 @@
|
|||||||
<td>{{ s.label }}</td>
|
<td>{{ s.label }}</td>
|
||||||
<td class="num">{{ s.count }}</td>
|
<td class="num">{{ s.count }}</td>
|
||||||
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ s.bookValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ s.sharePct | number: '1.1-1' }}%</td>
|
||||||
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
|
||||||
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
|
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -75,6 +95,8 @@
|
|||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td class="num">{{ totalHoldings }}</td>
|
<td class="num">{{ totalHoldings }}</td>
|
||||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totalBookValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ totalBookValue > 0 ? '100%' : '—' }}</td>
|
||||||
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -84,93 +106,70 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
|
<div class="pos-section-label">RBI Limits</div>
|
||||||
<p class="pg-sub">
|
@if (limitNote) {
|
||||||
Govt Security, Non-SLR, Mutual Fund and Share targets trace to RBI's SLR floor (18% of NDTL) and Non-SLR ceiling
|
<p class="inv-hint inv-hint--bad">{{ limitNote }}</p>
|
||||||
(≤10% of deposits) for Urban Co-operative Banks; Fixed Deposit and Bond are board-approved policy choices within
|
}
|
||||||
the room that framework leaves.
|
@if (limits.length) {
|
||||||
</p>
|
<table class="pg-table">
|
||||||
<table class="pg-table">
|
<thead>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Instrument Type</th>
|
|
||||||
<th>Target Basis</th>
|
|
||||||
<th class="num">Target %</th>
|
|
||||||
<th class="num">Actual %</th>
|
|
||||||
<th class="num">Actual Value</th>
|
|
||||||
<th class="num">Variance</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (a of allocationVsTarget; track a.type) {
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ a.label }}</td>
|
<th>Limit</th>
|
||||||
<td>
|
<th>Basis</th>
|
||||||
<span class="inv-badge" [class.inv-badge--matured]="a.isRbiLinked">
|
<th class="num">Allowed</th>
|
||||||
{{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }}
|
<th class="num">Used</th>
|
||||||
</span>
|
<th class="num">Used %</th>
|
||||||
</td>
|
<th>Status</th>
|
||||||
<td class="num">{{ a.targetPct }}%</td>
|
|
||||||
<td class="num">{{ a.actualPct }}%</td>
|
|
||||||
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
|
|
||||||
<td
|
|
||||||
class="num"
|
|
||||||
[class.col-out]="a.statusLabel !== 'Within Target'"
|
|
||||||
[class.col-in]="a.statusLabel === 'Within Target'"
|
|
||||||
>
|
|
||||||
{{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}%
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
class="inv-badge"
|
|
||||||
[class.inv-badge--active]="a.statusLabel === 'Within Target'"
|
|
||||||
[class.inv-badge--sold]="a.statusLabel === 'Over-Allocated'"
|
|
||||||
[class.inv-badge--written_off]="a.statusLabel === 'Under-Allocated'"
|
|
||||||
>
|
|
||||||
{{ a.statusLabel }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
<tfoot>
|
@for (l of limits; track l.label) {
|
||||||
<tr class="cctr-total-row">
|
<tr>
|
||||||
<td>Total</td>
|
<td>{{ l.label }}</td>
|
||||||
<td class="num">100%</td>
|
<td>{{ l.basis }}</td>
|
||||||
<td class="num">100%</td>
|
<td class="num">₹{{ l.limit | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ l.used | number: '1.2-2' }}</td>
|
||||||
<td colspan="2"></td>
|
<td class="num">{{ l.usedPct === null ? '—' : (l.usedPct | number: '1.1-1') + '%' }}</td>
|
||||||
</tr>
|
<td>
|
||||||
</tfoot>
|
<span class="inv-badge" [class.inv-badge--active]="l.within" [class.inv-badge--written_off]="!l.within">
|
||||||
</table>
|
{{ l.within ? 'Within' : 'Above limit' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Due-Date Diary — Maturing Within 90 Days</div>
|
<div class="pos-section-label">Maturing Within 90 Days</div>
|
||||||
@if (maturingSoon.length === 0) {
|
@if (maturingSoon.length === 0) {
|
||||||
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div>
|
<div>
|
||||||
@for (h of maturingSoon; track h.certNo) {
|
@for (m of maturingSoon; track m.dealNo) {
|
||||||
<div class="inv-maturity-row">
|
<div class="inv-maturity-row">
|
||||||
<span>
|
<span>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ m.dealNo }}</span>
|
||||||
{{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
|
{{ m.particulars }} ({{ m.instrument }})
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Matures {{ h.maturityDate }} ·
|
Matures {{ m.maturityDate }} · ₹{{ m.amount | number: '1.2-2' }} ·
|
||||||
<span
|
<span
|
||||||
class="inv-maturity-days"
|
class="inv-maturity-days"
|
||||||
[class.inv-maturity-days--soon]="daysLeft(h) <= 30"
|
[class.inv-maturity-days--soon]="m.days <= 30"
|
||||||
[class.inv-maturity-days--ok]="daysLeft(h) > 30"
|
[class.inv-maturity-days--ok]="m.days > 30"
|
||||||
>
|
>
|
||||||
{{ daysLeft(h) }} days
|
{{ m.days < 0 ? 'Overdue ' + -m.days + ' days' : m.days + ' days' }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inv-form-actions" style="padding: 8px 16px">
|
||||||
|
<a class="pg-btn pg-btn--ghost" routerLink="/investment/diary">Open Due Date Diary</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,19 +178,17 @@
|
|||||||
<div class="inv-nav-grid" style="padding: 16px">
|
<div class="inv-nav-grid" style="padding: 16px">
|
||||||
<a class="inv-nav-card" routerLink="/investment/fd">
|
<a class="inv-nav-card" routerLink="/investment/fd">
|
||||||
<span class="inv-nav-card__label">Fixed Deposit</span>
|
<span class="inv-nav-card__label">Fixed Deposit</span>
|
||||||
<span class="inv-nav-card__count">{{ summaries[0].count }}</span>
|
<span class="inv-nav-card__count">{{ countOf('FD') }}</span>
|
||||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Write-off</span>
|
<span class="inv-nav-card__value">Purchase / Interest / Maturity / Renewal</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/bonds">
|
<a class="inv-nav-card" routerLink="/investment/bonds">
|
||||||
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
|
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
|
||||||
<span class="inv-nav-card__count">{{
|
<span class="inv-nav-card__count">{{ countOf('GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND') }}</span>
|
||||||
summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count
|
|
||||||
}}</span>
|
|
||||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
|
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/share">
|
<a class="inv-nav-card" routerLink="/investment/share">
|
||||||
<span class="inv-nav-card__label">Share Entry</span>
|
<span class="inv-nav-card__label">Share Entry</span>
|
||||||
<span class="inv-nav-card__count">{{ summaries[5].count }}</span>
|
<span class="inv-nav-card__count">{{ countOf('SHARE') }}</span>
|
||||||
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
|
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/interest">
|
<a class="inv-nav-card" routerLink="/investment/interest">
|
||||||
|
|||||||
@@ -6,21 +6,23 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import {
|
import { forkJoin } from 'rxjs';
|
||||||
ALL_HOLDINGS,
|
import { INSTRUMENT_LABELS, InstrumentType, parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
|
||||||
ALLOCATION_TARGET_IS_RBI_LINKED,
|
|
||||||
INSTRUMENT_LABELS,
|
|
||||||
InstrumentType,
|
|
||||||
InvestmentHolding,
|
|
||||||
TARGET_ALLOCATION_PCT,
|
|
||||||
accruedInterest,
|
|
||||||
daysToMaturity,
|
|
||||||
maturityAmount
|
|
||||||
} from '../investment-data';
|
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentBucketReportDto,
|
||||||
|
InvestmentBucketRowDto,
|
||||||
|
InvestmentFdDto,
|
||||||
|
InvestmentFdLimitDto,
|
||||||
|
InvestmentInterestReportDto,
|
||||||
|
InvestmentNonSlrLimitDto,
|
||||||
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
interface InstrumentSummary {
|
interface InstrumentSummary {
|
||||||
type: InstrumentType;
|
type: InstrumentType;
|
||||||
@@ -28,25 +30,45 @@ interface InstrumentSummary {
|
|||||||
route: string;
|
route: string;
|
||||||
count: number;
|
count: number;
|
||||||
faceValue: number;
|
faceValue: number;
|
||||||
|
bookValue: number;
|
||||||
accruedInterest: number;
|
accruedInterest: number;
|
||||||
maturityValue: number;
|
maturityValue: number;
|
||||||
|
sharePct: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AllocationRow {
|
interface LimitRow {
|
||||||
type: InstrumentType;
|
|
||||||
label: string;
|
label: string;
|
||||||
targetPct: number;
|
basis: string;
|
||||||
actualPct: number;
|
limit: number | null;
|
||||||
actualValue: number;
|
used: number;
|
||||||
variancePct: number;
|
usedPct: number | null;
|
||||||
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
|
within: boolean;
|
||||||
/** 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
|
/** A holding maturing soon, or past maturity and not yet redeemed / closed. */
|
||||||
* hand-duplicated, so it can never drift from the per-instrument screens or Reports. */
|
interface MaturingRow {
|
||||||
|
dealNo: string;
|
||||||
|
particulars: string;
|
||||||
|
instrument: string;
|
||||||
|
maturityDate: string;
|
||||||
|
days: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROUTE: Record<InstrumentType, string> = {
|
||||||
|
FD: '/investment/fd',
|
||||||
|
GOVT_SECURITY: '/investment/bonds',
|
||||||
|
NON_SLR: '/investment/bonds',
|
||||||
|
BOND: '/investment/bonds',
|
||||||
|
MUTUAL_FUND: '/investment/bonds',
|
||||||
|
SHARE: '/investment/share'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-instrument portfolio dashboard on live data: what is held today by instrument (face value, book value,
|
||||||
|
* interest earned and not yet received, value at maturity), where the portfolio stands against the RBI limits, and
|
||||||
|
* what matures in the next 90 days.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-investment-dashboard',
|
selector: 'mifosx-investment-dashboard',
|
||||||
templateUrl: './investment-dashboard.component.html',
|
templateUrl: './investment-dashboard.component.html',
|
||||||
@@ -60,107 +82,216 @@ interface AllocationRow {
|
|||||||
RouterLink
|
RouterLink
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentDashboardComponent {
|
export class InvestmentDashboardComponent implements OnInit {
|
||||||
asOnDate = '';
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
asOnDate = todayDDMMYYYY();
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
generating = false;
|
generating = false;
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
private readonly types: Array<{ type: InstrumentType; route: string }> = [
|
|
||||||
{ type: 'FD', route: '/investment/fd' },
|
|
||||||
{ type: 'GOVT_SECURITY', route: '/investment/bonds' },
|
|
||||||
{ type: 'NON_SLR', route: '/investment/bonds' },
|
|
||||||
{ type: 'BOND', route: '/investment/bonds' },
|
|
||||||
{ type: 'MUTUAL_FUND', route: '/investment/bonds' },
|
|
||||||
{ type: 'SHARE', route: '/investment/share' }
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
summaries: InstrumentSummary[] = [];
|
||||||
const d = new Date();
|
limits: LimitRow[] = [];
|
||||||
this.asOnDate = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
limitNote = '';
|
||||||
|
maturingSoon: MaturingRow[] = [];
|
||||||
|
totalReceivable = 0;
|
||||||
|
totalProvisioned = 0;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
private holdingsOf(type: InstrumentType): InvestmentHolding[] {
|
load(): void {
|
||||||
return ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
const today = this.asOnDate;
|
||||||
}
|
const d = parseDDMMYYYY(today);
|
||||||
|
const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||||
get summaries(): InstrumentSummary[] {
|
this.loading = true;
|
||||||
return this.types.map(({ type, route }) => {
|
this.error = '';
|
||||||
const holdings = this.holdingsOf(type);
|
forkJoin({
|
||||||
return {
|
bucket: this.investmentService.getBucketReport(today),
|
||||||
type,
|
interest: this.investmentService.getInterestReport(fyStart, today),
|
||||||
label: INSTRUMENT_LABELS[type],
|
purchases: this.investmentService.getPurchases(),
|
||||||
route,
|
fds: this.investmentService.getFds(),
|
||||||
count: holdings.length,
|
fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }),
|
||||||
faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
|
nonSlr: this.investmentService.getNonSlrLimit(null, today, 0)
|
||||||
accruedInterest: holdings.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0),
|
}).subscribe({
|
||||||
maturityValue: holdings.reduce((s, h) => s + maturityAmount(h), 0)
|
next: (r) => {
|
||||||
};
|
this.build(r.bucket, r.interest, r.purchases, r.fds, r.fdQuote.limit ?? null, r.nonSlr);
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Board-approved policy target % per instrument type vs the portfolio's actual allocation
|
private build(
|
||||||
* (by invested/face value) — a ±2 percentage-point band is treated as "Within Target" since
|
bucket: InvestmentBucketReportDto,
|
||||||
* a treasury desk can't rebalance to the exact decimal every day. */
|
interest: InvestmentInterestReportDto,
|
||||||
get allocationVsTarget(): AllocationRow[] {
|
purchases: InvestmentPurchaseDto[],
|
||||||
const total = this.totalFaceValue;
|
fds: InvestmentFdDto[],
|
||||||
return this.types.map(({ type }) => {
|
fdLimit: InvestmentFdLimitDto | null,
|
||||||
const value = this.holdingsOf(type).reduce((s, h) => s + h.faceValue, 0);
|
nonSlr: InvestmentNonSlrLimitDto
|
||||||
const actualPct = total ? (value / total) * 100 : 0;
|
): void {
|
||||||
const targetPct = TARGET_ALLOCATION_PCT[type];
|
const purchaseById = new Map<number, InvestmentPurchaseDto>(
|
||||||
const variancePct = +(actualPct - targetPct).toFixed(1);
|
purchases.map((p) => [
|
||||||
const statusLabel: AllocationRow['statusLabel'] =
|
p.id,
|
||||||
Math.abs(variancePct) <= 2 ? 'Within Target' : variancePct > 0 ? 'Over-Allocated' : 'Under-Allocated';
|
p
|
||||||
return {
|
])
|
||||||
type,
|
);
|
||||||
label: INSTRUMENT_LABELS[type],
|
const fdById = new Map<number, InvestmentFdDto>(
|
||||||
targetPct,
|
fds
|
||||||
actualPct: +actualPct.toFixed(1),
|
.filter((f) => f.id != null)
|
||||||
actualValue: value,
|
.map((f) => [
|
||||||
variancePct,
|
f.id as number,
|
||||||
statusLabel,
|
f
|
||||||
isRbiLinked: ALLOCATION_TARGET_IS_RBI_LINKED[type]
|
])
|
||||||
};
|
);
|
||||||
});
|
const receivable = new Map<string, number>(
|
||||||
|
interest.rows.map((x) => [
|
||||||
|
x.instrument + ':' + x.refId,
|
||||||
|
x.receivable
|
||||||
|
])
|
||||||
|
);
|
||||||
|
this.totalReceivable = interest.totalReceivable;
|
||||||
|
this.totalProvisioned = interest.totalProvisioned;
|
||||||
|
|
||||||
|
// Bucket rows are the Govt Securities, bonds and FDs held today; the purchase tells a Non-SLR from a Bond.
|
||||||
|
const typeOf = (r: InvestmentBucketRowDto): InstrumentType =>
|
||||||
|
r.instrument === 'FD' ? 'FD' : (purchaseById.get(r.refId)?.bondType ?? 'BOND');
|
||||||
|
const acc = new Map<InstrumentType, InstrumentSummary>();
|
||||||
|
const sum = (type: InstrumentType): InstrumentSummary => {
|
||||||
|
let s = acc.get(type);
|
||||||
|
if (!s) {
|
||||||
|
s = {
|
||||||
|
type,
|
||||||
|
label: INSTRUMENT_LABELS[type],
|
||||||
|
route: ROUTE[type],
|
||||||
|
count: 0,
|
||||||
|
faceValue: 0,
|
||||||
|
bookValue: 0,
|
||||||
|
accruedInterest: 0,
|
||||||
|
maturityValue: 0,
|
||||||
|
sharePct: 0
|
||||||
|
};
|
||||||
|
acc.set(type, s);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
for (const r of bucket.rows) {
|
||||||
|
const s = sum(typeOf(r));
|
||||||
|
s.count += 1;
|
||||||
|
s.faceValue += r.faceValue;
|
||||||
|
s.bookValue += r.bookValue;
|
||||||
|
s.accruedInterest += receivable.get((r.instrument === 'FD' ? 'FD' : 'SECURITY') + ':' + r.refId) ?? 0;
|
||||||
|
// A security is redeemed at face value; an FD pays its maturity amount (just the principal on a payout FD).
|
||||||
|
s.maturityValue +=
|
||||||
|
r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue;
|
||||||
|
}
|
||||||
|
// Mutual funds and shares have no maturity, so they are not in the buckets; their units are held at cost.
|
||||||
|
for (const p of purchases) {
|
||||||
|
if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') {
|
||||||
|
const s = sum(p.bondType);
|
||||||
|
s.count += 1;
|
||||||
|
s.faceValue += p.faceValue;
|
||||||
|
s.bookValue += p.bookValue;
|
||||||
|
s.maturityValue += p.bookValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const order: InstrumentType[] = [
|
||||||
|
'GOVT_SECURITY',
|
||||||
|
'FD',
|
||||||
|
'NON_SLR',
|
||||||
|
'BOND',
|
||||||
|
'MUTUAL_FUND',
|
||||||
|
'SHARE'
|
||||||
|
];
|
||||||
|
const total = [...acc.values()].reduce((t, s) => t + s.bookValue, 0);
|
||||||
|
this.summaries = order
|
||||||
|
.map((t) => sum(t))
|
||||||
|
.map((s) => ({ ...s, sharePct: total > 0 ? (s.bookValue / total) * 100 : 0 }));
|
||||||
|
|
||||||
|
this.limits = [];
|
||||||
|
this.limitNote = '';
|
||||||
|
if (fdLimit?.grossLimit != null) {
|
||||||
|
const used = fdLimit.fdOutstanding + fdLimit.bankBalances;
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Deposits with other banks',
|
||||||
|
'FDs + current accounts, ≤ 20% of deposits',
|
||||||
|
fdLimit.grossLimit,
|
||||||
|
used
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nonSlr.nonSlrLimit != null) {
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Non-SLR investments',
|
||||||
|
'Bonds, CP / CD, MF, ≤ 10% of deposits',
|
||||||
|
nonSlr.nonSlrLimit,
|
||||||
|
nonSlr.nonSlrOutstanding
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Unlisted non-SLR',
|
||||||
|
`≤ 10% of non-SLR held on ${nonSlr.asOnDate}`,
|
||||||
|
nonSlr.unlistedLimit,
|
||||||
|
nonSlr.unlistedOutstanding
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.limitNote = `Total deposits as on ${nonSlr.asOnDate} are not entered yet; enter them on the FD screen to see the RBI limits.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maturingSoon = bucket.rows
|
||||||
|
.filter((r) => r.daysToMaturity <= 90)
|
||||||
|
.map((r) => ({
|
||||||
|
dealNo: r.dealNo,
|
||||||
|
particulars: r.particulars,
|
||||||
|
instrument: INSTRUMENT_LABELS[typeOf(r)],
|
||||||
|
maturityDate: r.maturityDate,
|
||||||
|
days: r.daysToMaturity,
|
||||||
|
amount: r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private limitRow(label: string, basis: string, limit: number, used: number): LimitRow {
|
||||||
|
return { label, basis, limit, used, usedPct: limit > 0 ? (used / limit) * 100 : null, within: used <= limit };
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalHoldings(): number {
|
get totalHoldings(): number {
|
||||||
return ALL_HOLDINGS.length;
|
return this.summaries.reduce((t, s) => t + s.count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalFaceValue(): number {
|
get totalFaceValue(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0);
|
return this.summaries.reduce((t, s) => t + s.faceValue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalBookValue(): number {
|
||||||
|
return this.summaries.reduce((t, s) => t + s.bookValue, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalAccruedInterest(): number {
|
get totalAccruedInterest(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0);
|
return this.summaries.reduce((t, s) => t + s.accruedInterest, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalMaturityValue(): number {
|
get totalMaturityValue(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0);
|
return this.summaries.reduce((t, s) => t + s.maturityValue, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeCount(): number {
|
get overdueCount(): number {
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length;
|
return this.maturingSoon.filter((m) => m.days < 0).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maturedCount(): number {
|
/** Holdings of one or more instrument types, for the Quick Entry cards. */
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'MATURED').length;
|
countOf(...types: InstrumentType[]): number {
|
||||||
}
|
return this.summaries.filter((s) => types.includes(s.type)).reduce((t, s) => t + s.count, 0);
|
||||||
|
|
||||||
/** Holdings maturing within the next 90 days — a "due-date diary" style widget. */
|
|
||||||
get maturingSoon(): InvestmentHolding[] {
|
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && daysToMaturity(h, this.asOnDate) <= 90).sort(
|
|
||||||
(a, b) => daysToMaturity(a, this.asOnDate) - daysToMaturity(b, this.asOnDate)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
daysLeft(h: InvestmentHolding): number {
|
|
||||||
return daysToMaturity(h, this.asOnDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
instrumentLabel(type: InstrumentType): string {
|
|
||||||
return INSTRUMENT_LABELS[type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportExcel(): Promise<void> {
|
async exportExcel(): Promise<void> {
|
||||||
@@ -176,54 +307,97 @@ export class InvestmentDashboardComponent {
|
|||||||
'Instrument Type',
|
'Instrument Type',
|
||||||
'Holdings',
|
'Holdings',
|
||||||
'Face Value',
|
'Face Value',
|
||||||
'Accrued Interest',
|
'Book Value',
|
||||||
|
'Share %',
|
||||||
|
'Interest Receivable',
|
||||||
'Maturity Value'
|
'Maturity Value'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
22,
|
||||||
12,
|
10,
|
||||||
18,
|
18,
|
||||||
18,
|
18,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
18
|
18
|
||||||
],
|
],
|
||||||
rows: this.summaries.map((s) => [
|
rows: [
|
||||||
s.label,
|
...this.summaries.map((s) => [
|
||||||
s.count,
|
s.label,
|
||||||
s.faceValue,
|
s.count,
|
||||||
s.accruedInterest,
|
s.faceValue,
|
||||||
s.maturityValue
|
s.bookValue,
|
||||||
])
|
Number(s.sharePct.toFixed(2)),
|
||||||
|
s.accruedInterest,
|
||||||
|
s.maturityValue
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
this.totalHoldings,
|
||||||
|
this.totalFaceValue,
|
||||||
|
this.totalBookValue,
|
||||||
|
100,
|
||||||
|
this.totalAccruedInterest,
|
||||||
|
this.totalMaturityValue
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sheetName: 'Portfolio Allocation',
|
sheetName: 'RBI Limits',
|
||||||
title: 'Portfolio Allocation — Target vs Actual',
|
title: 'RBI Limits',
|
||||||
subtitle: `As on ${this.asOnDate}`,
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
headers: [
|
headers: [
|
||||||
'Instrument Type',
|
'Limit',
|
||||||
'Target Basis',
|
'Basis',
|
||||||
'Target %',
|
'Allowed',
|
||||||
'Actual %',
|
'Used',
|
||||||
'Actual Value',
|
'Used %',
|
||||||
'Variance %',
|
|
||||||
'Status'
|
'Status'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
26,
|
||||||
20,
|
40,
|
||||||
12,
|
|
||||||
12,
|
|
||||||
18,
|
18,
|
||||||
|
18,
|
||||||
|
10,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
rows: this.limits.map((l) => [
|
||||||
|
l.label,
|
||||||
|
l.basis,
|
||||||
|
l.limit,
|
||||||
|
l.used,
|
||||||
|
l.usedPct === null ? null : Number(l.usedPct.toFixed(2)),
|
||||||
|
l.within ? 'Within' : 'Above limit'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sheetName: 'Maturing in 90 days',
|
||||||
|
title: 'Maturing Within 90 Days (and overdue)',
|
||||||
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
|
headers: [
|
||||||
|
'Deal No',
|
||||||
|
'Investment',
|
||||||
|
'Particulars',
|
||||||
|
'Maturity',
|
||||||
|
'Days',
|
||||||
|
'Amount'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
40,
|
||||||
12,
|
12,
|
||||||
|
10,
|
||||||
18
|
18
|
||||||
],
|
],
|
||||||
rows: this.allocationVsTarget.map((a) => [
|
rows: this.maturingSoon.map((m) => [
|
||||||
a.label,
|
m.dealNo,
|
||||||
a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy',
|
m.instrument,
|
||||||
a.targetPct,
|
m.particulars,
|
||||||
a.actualPct,
|
m.maturityDate,
|
||||||
a.actualValue,
|
m.days,
|
||||||
a.variancePct,
|
m.amount
|
||||||
a.statusLabel
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,17 +12,26 @@
|
|||||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||||
<h1 class="pg-title">Investment Reports</h1>
|
<h1 class="pg-title">Investment Reports</h1>
|
||||||
<p class="pg-sub">
|
<p class="pg-sub">
|
||||||
Balance reports, ledgers, registers, maturity lists and the consolidated portfolio — all derived live from the
|
Balance reports, ledgers, registers, maturity lists and the consolidated portfolio from the saved deals, FDs and
|
||||||
same holdings/transactions used by the Entry screens.
|
their GL entries, as on {{ asOnDate }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-toolbar">
|
<div class="pg-toolbar">
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating || loading" (click)="exportExcel()">
|
||||||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@if (error) {
|
||||||
|
<div class="pg-card">
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="cctr-tabs">
|
<div class="cctr-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -124,7 +133,7 @@
|
|||||||
: activeReport === 'shareBalance'
|
: activeReport === 'shareBalance'
|
||||||
? shareHoldings
|
? shareHoldings
|
||||||
: fdHoldings;
|
: fdHoldings;
|
||||||
track h.certNo
|
track h.instrumentType + ':' + h.id
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -138,6 +147,12 @@
|
|||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="pg-empty">
|
||||||
|
{{ activeReport === 'shareBalance' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -280,7 +295,7 @@
|
|||||||
: activeReport === 'amortizeLedger'
|
: activeReport === 'amortizeLedger'
|
||||||
? amortizeTxns
|
? amortizeTxns
|
||||||
: dealSlips;
|
: dealSlips;
|
||||||
track t.voucherNo
|
track $index
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ t.txnDate }}</td>
|
||||||
@@ -303,7 +318,10 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'intDueRegister') {
|
@if (activeReport === 'intDueRegister') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Interest Due Register (Non-Cumulative Holdings)</div>
|
<div class="pos-section-label">
|
||||||
|
Interest Due Register — interest earned up to {{ asOnDate }} and not received yet ·
|
||||||
|
<a routerLink="/investment/diary">Due Date Diary</a>
|
||||||
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -311,11 +329,11 @@
|
|||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th>Frequency</th>
|
<th>Frequency</th>
|
||||||
<th class="num">Rate</th>
|
<th class="num">Rate</th>
|
||||||
<th class="num col-in">Due Amount (as on {{ asOnDate }})</th>
|
<th class="num col-in">Receivable (as on {{ asOnDate }})</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of interestDueRegister; track h.certNo) {
|
@for (h of interestDueRegister; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -325,6 +343,10 @@
|
|||||||
<td class="num">{{ h.interestRate }}%</td>
|
<td class="num">{{ h.interestRate }}%</td>
|
||||||
<td class="num col-in">₹{{ h.dueAmount | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ h.dueAmount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="pg-empty">No interest receivable.</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -344,7 +366,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of slrBondRegister; track h.certNo) {
|
@for (h of slrBondRegister; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -372,29 +394,33 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (t of receivedInterestTxns; track t.voucherNo) {
|
@for (t of receivedInterestTxns; track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ t.txnDate }}</td>
|
||||||
<td>{{ t.certNo }}</td>
|
<td>{{ t.certNo }}</td>
|
||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</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 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="pg-empty">No interest received yet.</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Pending Interest (No Interest Posted Yet)</div>
|
<div class="pos-section-label">Pending Interest — earned up to {{ asOnDate }}, not received yet</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th class="num col-out">Accrued (Pending)</th>
|
<th class="num col-out">Receivable</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of pendingInterestHoldings; track h.certNo) {
|
@for (h of pendingInterestHoldings; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -487,11 +513,11 @@
|
|||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Party</th>
|
<th>Party</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th>Matured On</th>
|
<th>Maturity Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of maturedFdList; track h.certNo) {
|
@for (h of maturedFdList; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -524,7 +550,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of maturingList; track h.certNo) {
|
@for (h of maturingList; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -543,24 +569,12 @@
|
|||||||
@if (activeReport === 'maturityPattern') {
|
@if (activeReport === 'maturityPattern') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Maturity Pattern Register</div>
|
<div class="pos-section-label">Maturity Pattern Register</div>
|
||||||
<table class="pg-table">
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
<thead>
|
The maturity pattern (up to 1 year, 1–3, 3–5 and over 5 years) is the Bucket Wise Investment report.
|
||||||
<tr>
|
</p>
|
||||||
<th>Bucket</th>
|
<div class="inv-form-actions" style="padding: 8px 16px">
|
||||||
<th class="num">Holdings</th>
|
<a class="pg-btn pg-btn--primary" routerLink="/investment/bucket-report">Open Bucket Wise Investment</a>
|
||||||
<th class="num">Face Value</th>
|
</div>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (b of maturityBuckets; track b.label) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ b.label }}</td>
|
|
||||||
<td class="num">{{ b.count }}</td>
|
|
||||||
<td class="num">₹{{ b.faceValue | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (
|
@if (
|
||||||
@@ -601,7 +615,7 @@
|
|||||||
: activeReport === 'shareSecList'
|
: activeReport === 'shareSecList'
|
||||||
? shareSecList
|
? shareSecList
|
||||||
: mutualFundList;
|
: mutualFundList;
|
||||||
track h.certNo
|
track h.instrumentType + ':' + h.id
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -615,6 +629,12 @@
|
|||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="pg-empty">
|
||||||
|
{{ activeReport === 'shareSecList' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -622,35 +642,35 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'shiftingReport') {
|
@if (activeReport === 'shiftingReport') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">
|
<div class="pos-section-label">Shifting Report — Govt Securities transferred to another script / category</div>
|
||||||
Shifting Report — SLR Securities with Market Deviation ≥ 1% (AFS/HTM review candidates)
|
|
||||||
</div>
|
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Date</th>
|
||||||
<th>Script</th>
|
<th>Deal No</th>
|
||||||
<th class="num">Purchase Rate</th>
|
<th>From</th>
|
||||||
<th class="num">Market Price</th>
|
<th>To</th>
|
||||||
<th class="num">Deviation %</th>
|
<th class="num">Units</th>
|
||||||
|
<th class="num">Book Value</th>
|
||||||
|
<th>Voucher</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of shiftingCandidates; track h.certNo) {
|
@for (x of transfers; track x.id) {
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{{ x.transferDate }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
</td>
|
|
||||||
<td>{{ h.scriptName }}</td>
|
|
||||||
<td class="num">{{ h.purchaseRate }}</td>
|
|
||||||
<td class="num">{{ h.marketPrice }}</td>
|
|
||||||
<td class="num" [class.col-in]="h.deviationPct > 0" [class.col-out]="h.deviationPct < 0">
|
|
||||||
{{ h.deviationPct }}%
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ x.fromScriptCode }} {{ x.fromScriptDesc }}</td>
|
||||||
|
<td>{{ x.toScriptCode }} {{ x.toScriptDesc }}</td>
|
||||||
|
<td class="num">{{ x.units | number: '1.0-4' }}</td>
|
||||||
|
<td class="num">₹{{ x.bookValue | number: '1.2-2' }}</td>
|
||||||
|
<td>{{ x.voucherNo || '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="pg-empty">No securities crossing the 1% deviation threshold today.</td>
|
<td colspan="7" class="pg-empty">No securities shifted.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -687,7 +707,7 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td class="num"></td>
|
<td class="num">{{ portfolioTotalCount }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalFaceValue | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalAccrued | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalAccrued | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalMaturity | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalMaturity | number: '1.2-2' }}</td>
|
||||||
|
|||||||
@@ -6,35 +6,44 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { forkJoin, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
ALL_HOLDINGS,
|
|
||||||
ALL_TXNS,
|
|
||||||
FD_HOLDINGS,
|
|
||||||
GOVT_SECURITY_HOLDINGS,
|
|
||||||
NON_SLR_HOLDINGS,
|
|
||||||
BOND_HOLDINGS,
|
|
||||||
MUTUAL_FUND_HOLDINGS,
|
|
||||||
SHARE_HOLDINGS,
|
|
||||||
SCRIPT_MARKET_DETAIL,
|
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
accruedInterest,
|
|
||||||
maturityAmount,
|
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
|
parseDDMMYYYY,
|
||||||
todayDDMMYYYY
|
todayDDMMYYYY
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentDailyRateDto,
|
||||||
|
InvestmentFdDto,
|
||||||
|
InvestmentFdScheduleDto,
|
||||||
|
InvestmentSecurityTransferDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage,
|
||||||
|
toFdHolding,
|
||||||
|
toFdTxns,
|
||||||
|
toHolding,
|
||||||
|
toInterestTxn,
|
||||||
|
toPurchaseTxn,
|
||||||
|
toSaleTxn,
|
||||||
|
toTransferTxn
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ~20 legacy Investment reports, grouped into 4 tabs. Every figure here is derived from the
|
* ~20 legacy Investment reports in 4 tabs, on live data: the saved purchases and FDs as holdings, and their GL
|
||||||
* same ALL_HOLDINGS/ALL_TXNS arrays used by the Entry screens and Dashboard — so a report can
|
* entries (settlements, interest, sales, redemptions, amortisation, transfers, FD placements and closures) as the
|
||||||
* never drift out of sync with what a Purchase/Sell/Interest entry actually recorded.
|
* ledgers. Interest receivable comes from the Interest Received & Receivable report, market rates from the Daily
|
||||||
|
* Script Master.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-investment-reports',
|
selector: 'mifosx-investment-reports',
|
||||||
@@ -44,37 +53,176 @@ type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
|||||||
'../investment-shared.scss'
|
'../investment-shared.scss'
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule]
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterLink
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentReportsComponent {
|
export class InvestmentReportsComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
activeGroup: ReportGroup = 'balance';
|
activeGroup: ReportGroup = 'balance';
|
||||||
activeReport = 'govtSecBalance';
|
activeReport = 'govtSecBalance';
|
||||||
asOnDate = todayDDMMYYYY();
|
asOnDate = todayDDMMYYYY();
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
generating = false;
|
generating = false;
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
/** Saved purchases and FDs as holdings, and their GL entries, newest first. */
|
||||||
|
holdings: InvestmentHolding[] = [];
|
||||||
|
txns: InvestmentTxn[] = [];
|
||||||
|
/** Coupons and FD interest paid out — interest actually received. */
|
||||||
|
receivedTxns: InvestmentTxn[] = [];
|
||||||
|
/** GOI category transfers (security shifted to another script / category). */
|
||||||
|
transfers: InvestmentSecurityTransferDto[] = [];
|
||||||
|
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
||||||
|
fdInterestLoaded = false;
|
||||||
|
private fds: InvestmentFdDto[] = [];
|
||||||
|
private receivable = new Map<string, number>();
|
||||||
|
private fdMaturity = new Map<number, number>();
|
||||||
|
private latestRate = new Map<string, number>();
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
const d = parseDDMMYYYY(this.asOnDate);
|
||||||
|
const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.fdInterestLoaded = false;
|
||||||
|
forkJoin({
|
||||||
|
purchases: this.investmentService.getPurchases(),
|
||||||
|
fds: this.investmentService.getFds(),
|
||||||
|
sales: this.investmentService.getSales(),
|
||||||
|
postings: this.investmentService.getInterestPostings(),
|
||||||
|
transfers: this.investmentService.getTransfers(),
|
||||||
|
interest: this.investmentService.getInterestReport(fyStart, this.asOnDate),
|
||||||
|
rates: this.investmentService.getDailyRates().pipe(catchError(() => of([] as InvestmentDailyRateDto[])))
|
||||||
|
}).subscribe({
|
||||||
|
next: (r) => {
|
||||||
|
this.fds = r.fds;
|
||||||
|
this.transfers = r.transfers;
|
||||||
|
this.holdings = [
|
||||||
|
...r.purchases.map(toHolding),
|
||||||
|
...r.fds.map(toFdHolding)
|
||||||
|
];
|
||||||
|
this.txns = this.byDateDesc([
|
||||||
|
...r.purchases.map(toPurchaseTxn),
|
||||||
|
...r.postings.map(toInterestTxn),
|
||||||
|
...r.sales.map(toSaleTxn),
|
||||||
|
...r.transfers.map(toTransferTxn),
|
||||||
|
...r.fds.flatMap((f) => toFdTxns(f, []))
|
||||||
|
]);
|
||||||
|
this.receivedTxns = this.byDateDesc(r.postings.filter((x) => x.lineType === 'COUPON').map(toInterestTxn));
|
||||||
|
this.receivable = new Map(
|
||||||
|
r.interest.rows.map((x) => [
|
||||||
|
x.instrument + ':' + x.refId,
|
||||||
|
x.receivable
|
||||||
|
])
|
||||||
|
);
|
||||||
|
this.fdMaturity = new Map(
|
||||||
|
r.fds
|
||||||
|
.filter((f) => f.id != null)
|
||||||
|
.map((f) => [
|
||||||
|
f.id as number,
|
||||||
|
f.expectedMaturityAmount ?? f.principal ?? 0
|
||||||
|
])
|
||||||
|
);
|
||||||
|
// Latest close on or before the as-on date, per script.
|
||||||
|
const asOn = parseDDMMYYYY(this.asOnDate).getTime();
|
||||||
|
this.latestRate = new Map();
|
||||||
|
[...r.rates]
|
||||||
|
.filter((x) => parseDDMMYYYY(x.rateDate).getTime() <= asOn)
|
||||||
|
.sort((a, b) => parseDDMMYYYY(a.rateDate).getTime() - parseDDMMYYYY(b.rateDate).getTime())
|
||||||
|
.forEach((x) => this.latestRate.set(x.scriptCode, x.closeRate));
|
||||||
|
this.loading = false;
|
||||||
|
if (this.activeReport === 'fdLedger' || this.activeReport === 'pendingReceived') {
|
||||||
|
this.loadFdInterest();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds each FD's posted interest lines to the ledgers (one schedule call per FD). */
|
||||||
|
private loadFdInterest(): void {
|
||||||
|
if (this.fdInterestLoaded || !this.fds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fdInterestLoaded = true;
|
||||||
|
const withId = this.fds.filter((f) => f.id != null);
|
||||||
|
forkJoin(
|
||||||
|
withId.map((f) =>
|
||||||
|
this.investmentService
|
||||||
|
.getFdSchedule(f.id as number)
|
||||||
|
.pipe(catchError(() => of(null as InvestmentFdScheduleDto | null)))
|
||||||
|
)
|
||||||
|
).subscribe((schedules) => {
|
||||||
|
const lines: InvestmentTxn[] = [];
|
||||||
|
const received: InvestmentTxn[] = [];
|
||||||
|
schedules.forEach((s, i) => {
|
||||||
|
if (!s) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const interest = toFdTxns(withId[i], s.rows).filter((t) => t.txnType === 'INTEREST');
|
||||||
|
lines.push(...interest);
|
||||||
|
received.push(...interest.filter((t) => !t.particulars.startsWith('Provision')));
|
||||||
|
});
|
||||||
|
this.txns = this.byDateDesc([
|
||||||
|
...this.txns,
|
||||||
|
...lines
|
||||||
|
]);
|
||||||
|
this.receivedTxns = this.byDateDesc([
|
||||||
|
...this.receivedTxns,
|
||||||
|
...received
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byDateDesc(txns: InvestmentTxn[]): InvestmentTxn[] {
|
||||||
|
return txns
|
||||||
|
.filter((t) => !!t.txnDate)
|
||||||
|
.sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
setGroup(group: ReportGroup, defaultReport: string): void {
|
setGroup(group: ReportGroup, defaultReport: string): void {
|
||||||
this.activeGroup = group;
|
this.activeGroup = group;
|
||||||
this.activeReport = defaultReport;
|
this.setReport(defaultReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReport(report: string): void {
|
setReport(report: string): void {
|
||||||
this.activeReport = report;
|
this.activeReport = report;
|
||||||
|
if (report === 'fdLedger' || report === 'pendingReceived') {
|
||||||
|
this.loadFdInterest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instrumentLabel(type: InstrumentType): string {
|
instrumentLabel(type: InstrumentType): string {
|
||||||
return INSTRUMENT_LABELS[type];
|
return INSTRUMENT_LABELS[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interest earned up to the as-on date and not yet received (Interest Received & Receivable report). */
|
||||||
accrued(h: InvestmentHolding): number {
|
accrued(h: InvestmentHolding): number {
|
||||||
return accruedInterest(h, this.asOnDate);
|
return this.receivable.get((h.instrumentType === 'FD' ? 'FD' : 'SECURITY') + ':' + h.id) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What comes back at maturity: the FD maturity amount, a security's face value, a fund's cost. */
|
||||||
maturity(h: InvestmentHolding): number {
|
maturity(h: InvestmentHolding): number {
|
||||||
return maturityAmount(h);
|
if (h.instrumentType === 'FD') {
|
||||||
|
return this.fdMaturity.get(h.id ?? -1) ?? h.faceValue;
|
||||||
|
}
|
||||||
|
return h.instrumentType === 'MUTUAL_FUND' || h.instrumentType === 'SHARE'
|
||||||
|
? (h.bookValue ?? h.faceValue)
|
||||||
|
: h.faceValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
daysLeft(h: InvestmentHolding): number {
|
daysLeft(h: InvestmentHolding): number {
|
||||||
@@ -85,23 +233,32 @@ export class InvestmentReportsComponent {
|
|||||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Latest close in the Daily Script Master on or before the as-on date. */
|
||||||
marketPrice(scriptCode: string): number | null {
|
marketPrice(scriptCode: string): number | null {
|
||||||
return SCRIPT_MARKET_DETAIL.find((m) => m.scriptCode === scriptCode)?.marketPrice ?? null;
|
return this.latestRate.get(scriptCode) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private active(type?: InstrumentType): InvestmentHolding[] {
|
||||||
|
return this.holdings.filter((h) => h.status === 'ACTIVE' && (!type || h.instrumentType === type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ofType(type: InstrumentType): InvestmentHolding[] {
|
||||||
|
return this.holdings.filter((h) => h.instrumentType === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Balance Reports --------------------
|
// -------------------- Balance Reports --------------------
|
||||||
get govtSecHoldings(): InvestmentHolding[] {
|
get govtSecHoldings(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS;
|
return this.active('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
get shareHoldings(): InvestmentHolding[] {
|
get shareHoldings(): InvestmentHolding[] {
|
||||||
return SHARE_HOLDINGS;
|
return this.active('SHARE');
|
||||||
}
|
}
|
||||||
get fdHoldings(): InvestmentHolding[] {
|
get fdHoldings(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS;
|
return this.active('FD');
|
||||||
}
|
}
|
||||||
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
|
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
|
||||||
const map = new Map<string, { party: string; count: number; totalFaceValue: number }>();
|
const map = new Map<string, { party: string; count: number; totalFaceValue: number }>();
|
||||||
for (const h of ALL_HOLDINGS) {
|
for (const h of this.active()) {
|
||||||
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
|
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
|
||||||
row.count += 1;
|
row.count += 1;
|
||||||
row.totalFaceValue += h.faceValue;
|
row.totalFaceValue += h.faceValue;
|
||||||
@@ -112,112 +269,68 @@ export class InvestmentReportsComponent {
|
|||||||
|
|
||||||
// -------------------- Ledgers & Registers --------------------
|
// -------------------- Ledgers & Registers --------------------
|
||||||
get fdTxns(): InvestmentTxn[] {
|
get fdTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.instrumentType === 'FD');
|
return this.txns.filter((t) => t.instrumentType === 'FD');
|
||||||
}
|
}
|
||||||
get bondTxns(): InvestmentTxn[] {
|
get bondTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => ([
|
return this.txns.filter((t) => ([
|
||||||
'GOVT_SECURITY',
|
'GOVT_SECURITY',
|
||||||
'NON_SLR',
|
'NON_SLR',
|
||||||
'BOND'
|
'BOND',
|
||||||
|
'MUTUAL_FUND'
|
||||||
] as InstrumentType[]).includes(t.instrumentType));
|
] as InstrumentType[]).includes(t.instrumentType));
|
||||||
}
|
}
|
||||||
get shareTxns(): InvestmentTxn[] {
|
get shareTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
return this.txns.filter((t) => t.instrumentType === 'SHARE');
|
||||||
}
|
}
|
||||||
get amortizeTxns(): InvestmentTxn[] {
|
get amortizeTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'AMORTIZE');
|
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
||||||
}
|
}
|
||||||
get dealSlips(): InvestmentTxn[] {
|
get dealSlips(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'PURCHASE');
|
return this.txns.filter((t) => t.txnType === 'PURCHASE' && t.instrumentType !== 'FD');
|
||||||
}
|
}
|
||||||
get slrBondRegister(): InvestmentHolding[] {
|
get slrBondRegister(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS.filter((h) => h.subType === 'SLR');
|
return this.active('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
|
/** Holdings with interest earned up to the as-on date and not received yet, with the amount. */
|
||||||
get interestDueRegister(): Array<InvestmentHolding & { dueAmount: number }> {
|
get interestDueRegister(): Array<InvestmentHolding & { dueAmount: number }> {
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE').map((h) => ({
|
return this.active()
|
||||||
...h,
|
.map((h) => ({ ...h, dueAmount: this.accrued(h) }))
|
||||||
dueAmount: this.accrued(h)
|
.filter((h) => h.dueAmount > 0);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
get receivedInterestTxns(): InvestmentTxn[] {
|
get receivedInterestTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'INTEREST' || t.txnType === 'DIVIDEND');
|
return this.receivedTxns;
|
||||||
}
|
}
|
||||||
get pendingInterestHoldings(): InvestmentHolding[] {
|
get pendingInterestHoldings(): InvestmentHolding[] {
|
||||||
const receivedCerts = new Set(this.receivedInterestTxns.map((t) => t.certNo));
|
return this.active().filter((h) => this.accrued(h) > 0);
|
||||||
return ALL_HOLDINGS.filter(
|
|
||||||
(h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE' && !receivedCerts.has(h.certNo)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Maturity & Lists --------------------
|
// -------------------- Maturity & Lists --------------------
|
||||||
get maturedFdList(): InvestmentHolding[] {
|
get maturedFdList(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS.filter((h) => h.status === 'MATURED');
|
return this.ofType('FD').filter((h) => h.status === 'MATURED');
|
||||||
}
|
}
|
||||||
get maturingList(): InvestmentHolding[] {
|
get maturingList(): InvestmentHolding[] {
|
||||||
return [
|
return [
|
||||||
...FD_HOLDINGS,
|
...this.active('FD'),
|
||||||
...GOVT_SECURITY_HOLDINGS
|
...this.active('GOVT_SECURITY')
|
||||||
]
|
].sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
|
||||||
.filter((h) => h.status === 'ACTIVE')
|
|
||||||
.sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
|
|
||||||
}
|
|
||||||
get maturityBuckets(): Array<{ label: string; count: number; faceValue: number }> {
|
|
||||||
const buckets = [
|
|
||||||
{ label: '0-1 Year', min: -99999, max: 365, count: 0, faceValue: 0 },
|
|
||||||
{ label: '1-3 Years', min: 366, max: 1095, count: 0, faceValue: 0 },
|
|
||||||
{ label: '3-5 Years', min: 1096, max: 1825, count: 0, faceValue: 0 },
|
|
||||||
{ label: 'Above 5 Years', min: 1826, max: 999999, count: 0, faceValue: 0 }
|
|
||||||
];
|
|
||||||
for (const h of ALL_HOLDINGS.filter((x) => x.status === 'ACTIVE')) {
|
|
||||||
const days = this.daysLeft(h);
|
|
||||||
const bucket = buckets.find((b) => days >= b.min && days <= b.max);
|
|
||||||
if (bucket) {
|
|
||||||
bucket.count += 1;
|
|
||||||
bucket.faceValue += h.faceValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buckets;
|
|
||||||
}
|
}
|
||||||
get govtSecList(): InvestmentHolding[] {
|
get govtSecList(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS;
|
return this.ofType('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
get fdSecList(): InvestmentHolding[] {
|
get fdSecList(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS;
|
return this.ofType('FD');
|
||||||
}
|
}
|
||||||
get shareSecList(): InvestmentHolding[] {
|
get shareSecList(): InvestmentHolding[] {
|
||||||
return SHARE_HOLDINGS;
|
return this.ofType('SHARE');
|
||||||
}
|
}
|
||||||
get mutualFundList(): InvestmentHolding[] {
|
get mutualFundList(): InvestmentHolding[] {
|
||||||
return MUTUAL_FUND_HOLDINGS;
|
return this.ofType('MUTUAL_FUND');
|
||||||
}
|
|
||||||
get nonSlrList(): InvestmentHolding[] {
|
|
||||||
return NON_SLR_HOLDINGS;
|
|
||||||
}
|
|
||||||
get bondList(): InvestmentHolding[] {
|
|
||||||
return BOND_HOLDINGS;
|
|
||||||
}
|
|
||||||
/** Shifting Report — SLR securities whose market value has moved away from book value by
|
|
||||||
* more than 1%, a proxy for "category shift" candidates (AFS/HTM reclassification review). */
|
|
||||||
get shiftingCandidates(): Array<InvestmentHolding & { marketPrice: number; deviationPct: number }> {
|
|
||||||
return GOVT_SECURITY_HOLDINGS.map((h) => {
|
|
||||||
const mp = this.marketPrice(h.scriptCode);
|
|
||||||
if (mp === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const deviationPct = +(((mp - h.purchaseRate) / h.purchaseRate) * 100).toFixed(2);
|
|
||||||
return { ...h, marketPrice: mp, deviationPct };
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
(x): x is InvestmentHolding & { marketPrice: number; deviationPct: number } =>
|
|
||||||
x !== null && Math.abs(x.deviationPct) >= 1
|
|
||||||
)
|
|
||||||
.sort((a, b) => Math.abs(b.deviationPct) - Math.abs(a.deviationPct));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Portfolio --------------------
|
// -------------------- Portfolio --------------------
|
||||||
private readonly portfolioTypes: InstrumentType[] = [
|
private readonly portfolioTypes: InstrumentType[] = [
|
||||||
'FD',
|
|
||||||
'GOVT_SECURITY',
|
'GOVT_SECURITY',
|
||||||
|
'FD',
|
||||||
'NON_SLR',
|
'NON_SLR',
|
||||||
'BOND',
|
'BOND',
|
||||||
'MUTUAL_FUND',
|
'MUTUAL_FUND',
|
||||||
@@ -231,7 +344,7 @@ export class InvestmentReportsComponent {
|
|||||||
maturityValue: number;
|
maturityValue: number;
|
||||||
}> {
|
}> {
|
||||||
return this.portfolioTypes.map((type) => {
|
return this.portfolioTypes.map((type) => {
|
||||||
const holdings = ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
const holdings = this.active(type);
|
||||||
return {
|
return {
|
||||||
label: INSTRUMENT_LABELS[type],
|
label: INSTRUMENT_LABELS[type],
|
||||||
count: holdings.length,
|
count: holdings.length,
|
||||||
@@ -241,6 +354,9 @@ export class InvestmentReportsComponent {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
get portfolioTotalCount(): number {
|
||||||
|
return this.portfolioSummary.reduce((s, r) => s + r.count, 0);
|
||||||
|
}
|
||||||
get portfolioTotalFaceValue(): number {
|
get portfolioTotalFaceValue(): number {
|
||||||
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
|
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
|
||||||
}
|
}
|
||||||
@@ -480,26 +596,6 @@ export class InvestmentReportsComponent {
|
|||||||
this.holdingsExportWidths,
|
this.holdingsExportWidths,
|
||||||
this.maturingList.map((h) => this.holdingsExportRow(h))
|
this.maturingList.map((h) => this.holdingsExportRow(h))
|
||||||
);
|
);
|
||||||
case 'maturityPattern':
|
|
||||||
return one(
|
|
||||||
'Maturity Pattern',
|
|
||||||
'Maturity Pattern Register',
|
|
||||||
[
|
|
||||||
'Bucket',
|
|
||||||
'Holdings',
|
|
||||||
'Face Value'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22,
|
|
||||||
14,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
this.maturityBuckets.map((b) => [
|
|
||||||
b.label,
|
|
||||||
b.count,
|
|
||||||
b.faceValue
|
|
||||||
])
|
|
||||||
);
|
|
||||||
case 'govtSecList':
|
case 'govtSecList':
|
||||||
return one(
|
return one(
|
||||||
'Govt Security List',
|
'Govt Security List',
|
||||||
@@ -535,27 +631,33 @@ export class InvestmentReportsComponent {
|
|||||||
case 'shiftingReport':
|
case 'shiftingReport':
|
||||||
return one(
|
return one(
|
||||||
'Shifting Report',
|
'Shifting Report',
|
||||||
'Shifting Report — SLR Market Deviation ≥ 1%',
|
'Shifting Report — Govt Securities transferred to another script / category',
|
||||||
[
|
[
|
||||||
'Cert No',
|
'Date',
|
||||||
'Script',
|
'Deal No',
|
||||||
'Purchase Rate',
|
'From',
|
||||||
'Market Price',
|
'To',
|
||||||
'Deviation %'
|
'Units',
|
||||||
|
'Book Value',
|
||||||
|
'Voucher No'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
14,
|
12,
|
||||||
30,
|
|
||||||
16,
|
16,
|
||||||
|
34,
|
||||||
|
34,
|
||||||
|
12,
|
||||||
16,
|
16,
|
||||||
14
|
20
|
||||||
],
|
],
|
||||||
this.shiftingCandidates.map((x) => [
|
this.transfers.map((x) => [
|
||||||
x.certNo,
|
x.transferDate,
|
||||||
x.scriptName,
|
x.dealNo,
|
||||||
x.purchaseRate,
|
`${x.fromScriptCode} ${x.fromScriptDesc}`,
|
||||||
x.marketPrice,
|
`${x.toScriptCode} ${x.toScriptDesc}`,
|
||||||
x.deviationPct
|
x.units,
|
||||||
|
x.bookValue,
|
||||||
|
x.voucherNo ?? ''
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
case 'portfolio':
|
case 'portfolio':
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
HoldingStatus,
|
HoldingStatus,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
|
InvestmentTxn,
|
||||||
PARTY_MASTER,
|
PARTY_MASTER,
|
||||||
PartyMaster,
|
PartyMaster,
|
||||||
SCRIPT_MASTER,
|
SCRIPT_MASTER,
|
||||||
@@ -756,6 +757,178 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A security transfer shows the book value carried to the new security. */
|
||||||
|
export function toTransferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'TRANSFER',
|
||||||
|
voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.transferDate,
|
||||||
|
amount: x.bookValue,
|
||||||
|
particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`,
|
||||||
|
glCode: x.toGlCode,
|
||||||
|
glName: x.glTransactionId
|
||||||
|
? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})`
|
||||||
|
: `${x.toGlName} (same GL, no entry)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */
|
||||||
|
export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
|
||||||
|
if (x.saleType === 'AMORTISE') {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'AMORTIZE',
|
||||||
|
voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.bookValueSold,
|
||||||
|
particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`,
|
||||||
|
glCode: '500604',
|
||||||
|
glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (x.saleType === 'WRITEOFF') {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'WRITE_OFF',
|
||||||
|
voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.bookValueSold,
|
||||||
|
particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`,
|
||||||
|
glCode: '500605',
|
||||||
|
glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const redeem = x.saleType === 'REDEEM';
|
||||||
|
const pl =
|
||||||
|
x.profitLoss >= 0
|
||||||
|
? `profit ₹${x.profitLoss}`
|
||||||
|
: redeem
|
||||||
|
? `premium written off ₹${-x.profitLoss}`
|
||||||
|
: `loss ₹${-x.profitLoss}`;
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: x.saleType,
|
||||||
|
voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.netAmount,
|
||||||
|
particulars: redeem
|
||||||
|
? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`
|
||||||
|
: `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`,
|
||||||
|
glCode: x.status === 'SETTLED' ? x.bankGlCode : '',
|
||||||
|
glName:
|
||||||
|
x.status === 'SETTLED'
|
||||||
|
? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
||||||
|
: 'Not posted (pending settlement)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
|
||||||
|
export function toInterestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
|
||||||
|
const coupon = x.lineType === 'COUPON';
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'INTEREST',
|
||||||
|
voucherNo: x.voucherNo,
|
||||||
|
txnDate: x.intDate,
|
||||||
|
amount: coupon ? x.grossInterest : x.amount,
|
||||||
|
particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`,
|
||||||
|
glCode: x.drGlCode,
|
||||||
|
glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
|
||||||
|
export function toPurchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
|
||||||
|
const posted = !!d.glTransactionId;
|
||||||
|
return {
|
||||||
|
certNo: d.certNo,
|
||||||
|
instrumentType: d.bondType,
|
||||||
|
txnType: 'PURCHASE',
|
||||||
|
voucherNo: `INV/PUR/${d.dealNo}`,
|
||||||
|
txnDate: d.settlementDate,
|
||||||
|
amount: d.settlementAmount,
|
||||||
|
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
|
||||||
|
glCode: posted ? d.investmentGlCode : '',
|
||||||
|
glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An FD as a row of the holdings registers: principal as face value, closed FDs shown as matured. */
|
||||||
|
export function toFdHolding(f: InvestmentFdDto): InvestmentHolding {
|
||||||
|
return {
|
||||||
|
id: f.id,
|
||||||
|
dealNo: f.dealNo,
|
||||||
|
certNo: f.certNo ?? '',
|
||||||
|
instrumentType: 'FD',
|
||||||
|
scriptCode: '',
|
||||||
|
scriptName: `FD @ ${f.intRate ?? 0}%`,
|
||||||
|
party: f.partyName ?? '',
|
||||||
|
partyCode: f.partyCode,
|
||||||
|
subType: '',
|
||||||
|
faceValue: f.principal ?? 0,
|
||||||
|
purchaseRate: 100,
|
||||||
|
units: 1,
|
||||||
|
purchaseDate: f.dealDate ?? '',
|
||||||
|
maturityDate: f.maturityDate ?? '',
|
||||||
|
interestRate: f.intRate ?? 0,
|
||||||
|
interestFreq: f.payout === 'HALF_YEARLY' ? 'HALF_YEARLY' : f.payout === 'YEARLY' ? 'YEARLY' : 'CUMULATIVE',
|
||||||
|
glCode: f.fdGlCode ?? '',
|
||||||
|
glName: f.fdGlName ?? '',
|
||||||
|
crToAc: f.bankGlCode ?? '',
|
||||||
|
brokerCode: '',
|
||||||
|
status: f.status === 'ACTIVE' ? 'ACTIVE' : 'MATURED',
|
||||||
|
bookValue: f.principal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The GL entries of an FD: placing it, each posted interest line, and its closure. */
|
||||||
|
export function toFdTxns(f: InvestmentFdDto, rows: InvestmentFdInterestRowDto[]): InvestmentTxn[] {
|
||||||
|
const base = { certNo: f.certNo ?? '', instrumentType: 'FD' as InstrumentType };
|
||||||
|
const out: InvestmentTxn[] = [
|
||||||
|
{
|
||||||
|
...base,
|
||||||
|
txnType: 'PURCHASE',
|
||||||
|
voucherNo: f.voucherNo ?? `FD/${f.dealNo}`,
|
||||||
|
txnDate: f.dealDate ?? '',
|
||||||
|
amount: f.principal ?? 0,
|
||||||
|
particulars: `FD placed - Deal ${f.dealNo} - ${f.partyName} @ ${f.intRate}% to ${f.maturityDate}`,
|
||||||
|
glCode: f.fdGlCode,
|
||||||
|
glName: `${f.fdGlName} / Cr ${f.bankGlCode} (txn ${f.glTransactionId})`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (const r of rows.filter((x) => x.posted)) {
|
||||||
|
out.push({
|
||||||
|
...base,
|
||||||
|
txnType: 'INTEREST',
|
||||||
|
voucherNo: r.voucherNo ?? `FDI/${f.dealNo}/${r.intDate}`,
|
||||||
|
txnDate: r.intDate,
|
||||||
|
amount: r.lineType === 'PROVISION' ? r.amount : r.grossInterest,
|
||||||
|
particulars: `${r.lineType === 'PROVISION' ? 'Provision' : 'Interest payout'} ${r.fromDate} to ${r.toDate} - Deal ${f.dealNo} - ${f.partyName}${r.tds ? ` (TDS ₹${r.tds})` : ''}`,
|
||||||
|
glCode: f.interestGlCode,
|
||||||
|
glName: `Cr ${f.interestGlName} (txn ${r.glTransactionId})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (f.closedOnDate) {
|
||||||
|
out.push({
|
||||||
|
...base,
|
||||||
|
txnType: 'REDEEM',
|
||||||
|
voucherNo: f.closureVoucherNo ?? `FDC/${f.dealNo}`,
|
||||||
|
txnDate: f.closedOnDate,
|
||||||
|
amount: f.closureAmount ?? 0,
|
||||||
|
particulars: `FD ${(f.closureType ?? 'closed').toLowerCase()} - Deal ${f.dealNo} - ${f.partyName} (interest ₹${f.closureInterest ?? 0}${f.closureTds ? `, TDS ₹${f.closureTds}` : ''}${f.closurePenal ? `, penal ₹${f.closurePenal}` : ''})`,
|
||||||
|
glCode: f.bankGlCode,
|
||||||
|
glName: `${f.bankGlName} / Cr ${f.fdGlCode} (txn ${f.closureGlTransactionId})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
|
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
|
||||||
export interface InvestmentInterestRowDto {
|
export interface InvestmentInterestRowDto {
|
||||||
type: 'COUPON' | 'PROVISION';
|
type: 'COUPON' | 'PROVISION';
|
||||||
|
|||||||
Reference in New Issue
Block a user