feat(investment): non-SLR bonds and mutual funds, rate upload, UAT fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@
|
|||||||
@if (pendingInvestmentInterest.length) {
|
@if (pendingInvestmentInterest.length) {
|
||||||
<div class="iia-row">
|
<div class="iia-row">
|
||||||
<span class="iia-summary"
|
<span class="iia-summary"
|
||||||
>GOI / Bonds: {{ pendingInvestmentInterest.length }} interest voucher(s){{
|
>GOI / Bonds: {{ pendingInvestmentInterest.length }} holding(s) with interest pending{{
|
||||||
overdueNote(pendingInvestmentInterest)
|
overdueNote(pendingInvestmentInterest)
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
@if (pendingFd.length) {
|
@if (pendingFd.length) {
|
||||||
<div class="iia-row">
|
<div class="iia-row">
|
||||||
<span class="iia-summary"
|
<span class="iia-summary"
|
||||||
>FD with banks: {{ pendingFd.length }} entry(ies){{ overdueNote(pendingFd) }}</span
|
>FD with banks: {{ pendingFd.length }} FD(s) with entries pending{{ overdueNote(pendingFd) }}</span
|
||||||
>
|
>
|
||||||
<button type="button" class="iia-toggle" (click)="showFdDetails = !showFdDetails">
|
<button type="button" class="iia-toggle" (click)="showFdDetails = !showFdDetails">
|
||||||
{{ showFdDetails ? 'Hide' : 'Details' }}
|
{{ showFdDetails ? 'Hide' : 'Details' }}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
<td class="num">₹{{ carryingValue(h) | number: '1.2-2' }}</td>
|
<td class="num">₹{{ carryingValue(h) | number: '1.2-2' }}</td>
|
||||||
<td class="num">{{ h.interestRate }}%</td>
|
<td class="num">{{ h.interestRate }}%</td>
|
||||||
<td>{{ h.purchaseDate }}</td>
|
<td>{{ h.purchaseDate }}</td>
|
||||||
<td>{{ h.maturityDate }}</td>
|
<td>{{ h.instrumentType === 'MUTUAL_FUND' ? '—' : h.maturityDate }}</td>
|
||||||
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td>
|
||||||
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
|
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Bond Type</span>
|
<span>* Bond Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.bondType">
|
<select [ngModel]="purchaseForm.bondType" (ngModelChange)="setBondType($event)">
|
||||||
@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>
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Settlement Type</span>
|
<span>* Settlement Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.settlementType">
|
<select [(ngModel)]="purchaseForm.settlementType" (ngModelChange)="onSettlementTypeChange()">
|
||||||
<option value="T+0">T+0</option>
|
<option value="T+0">T+0</option>
|
||||||
<option value="T+1">T+1</option>
|
<option value="T+1">T+1</option>
|
||||||
<option value="T+2">T+2</option>
|
<option value="T+2">T+2</option>
|
||||||
@@ -264,14 +264,14 @@
|
|||||||
}
|
}
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Category</span>
|
<span>* Category</span>
|
||||||
<select [(ngModel)]="purchaseForm.category">
|
<select [(ngModel)]="purchaseForm.category" [disabled]="isNonSlrPurchase">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
@for (c of categoryOptions; track c) {
|
@for (c of purchaseCategoryOptions; track c) {
|
||||||
<option [value]="c">{{ c }}</option>
|
<option [value]="c">{{ c }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (!isMutualFund) {
|
@if (!isNonSlrPurchase) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Last Interest Date</span
|
><span>* Last Interest Date</span
|
||||||
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
|
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.lastIntDt"></mifosx-date-ddmmyyyy
|
||||||
@@ -373,7 +373,8 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Book Value</span><input type="text" [value]="'₹' + (purchaseBookValue | number: '1.2-2')" readonly
|
><span>Book Value</span><input type="text" [value]="'₹' + (purchaseBookValue | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
@if (!isMutualFund) {
|
<!-- Non-SLR has no premium split, broken-period interest or yield: it is booked at the price paid. -->
|
||||||
|
@if (!isNonSlrPurchase) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Premium (+) / Discount (−)</span
|
><span>Premium (+) / Discount (−)</span
|
||||||
><input type="text" [value]="'₹' + (purchasePremiumDiscount | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (purchasePremiumDiscount | number: '1.2-2')" readonly
|
||||||
@@ -394,7 +395,7 @@
|
|||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!isMutualFund && purchaseCouponDates) {
|
@if (!isNonSlrPurchase && purchaseCouponDates) {
|
||||||
<div class="pos-section-label">Interest Cycle</div>
|
<div class="pos-section-label">Interest Cycle</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
@@ -753,7 +754,13 @@
|
|||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>{{
|
<span>{{
|
||||||
q.profitLoss >= 0 ? 'Discount realised (profit 400405)' : 'Premium written off (expense 500604)'
|
isNonSlrSale(q)
|
||||||
|
? q.profitLoss >= 0
|
||||||
|
? 'Profit on redemption (income 400402)'
|
||||||
|
: 'Loss on redemption (expense 500605)'
|
||||||
|
: q.profitLoss >= 0
|
||||||
|
? 'Discount realised (profit 400405)'
|
||||||
|
: 'Premium written off (expense 500604)'
|
||||||
}}</span>
|
}}</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -786,9 +793,13 @@
|
|||||||
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
||||||
}
|
}
|
||||||
@if (q.profitLoss > 0) {
|
@if (q.profitLoss > 0) {
|
||||||
· Cr 400405 Profit on Redemption ₹{{ q.profitLoss | number: '1.2-2' }}
|
· Cr {{ isNonSlrSale(q) ? '400402 Interest on Bonds' : '400405 Profit on Redemption' }} ₹{{
|
||||||
|
q.profitLoss | number: '1.2-2'
|
||||||
|
}}
|
||||||
} @else if (q.profitLoss < 0) {
|
} @else if (q.profitLoss < 0) {
|
||||||
· Dr 500604 Premium Amortisation ₹{{ -q.profitLoss | number: '1.2-2' }}
|
· Dr {{ isNonSlrSale(q) ? '500605 Loss on Investments' : '500604 Premium Amortisation' }} ₹{{
|
||||||
|
-q.profitLoss | number: '1.2-2'
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -1074,7 +1085,7 @@
|
|||||||
><input type="number" min="0" [(ngModel)]="sale.units" (change)="refreshSaleQuote()"
|
><input type="number" min="0" [(ngModel)]="sale.units" (change)="refreshSaleQuote()"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Sale Rate (per ₹100)</span
|
><span>* {{ saleIsMutualFund ? 'NAV per unit' : 'Sale Rate (per ₹100)' }}</span
|
||||||
><input type="number" min="0" step="0.0001" [(ngModel)]="sale.saleRate" (change)="refreshSaleQuote()"
|
><input type="number" min="0" step="0.0001" [(ngModel)]="sale.saleRate" (change)="refreshSaleQuote()"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
@@ -1148,7 +1159,9 @@
|
|||||||
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
||||||
}
|
}
|
||||||
@if (q.profitLoss > 0) {
|
@if (q.profitLoss > 0) {
|
||||||
· Cr 400405 Profit ₹{{ q.profitLoss | number: '1.2-2' }}
|
· Cr {{ isNonSlrSale(q) ? '400402 Interest on Bonds' : '400405 Profit' }} ₹{{
|
||||||
|
q.profitLoss | number: '1.2-2'
|
||||||
|
}}
|
||||||
} @else if (q.profitLoss < 0) {
|
} @else if (q.profitLoss < 0) {
|
||||||
· Dr 500605 Loss ₹{{ -q.profitLoss | number: '1.2-2' }}
|
· Dr 500605 Loss ₹{{ -q.profitLoss | number: '1.2-2' }}
|
||||||
}
|
}
|
||||||
@@ -1471,7 +1484,7 @@
|
|||||||
<span>* Deal No</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="goiPurchaseId" (ngModelChange)="onGoiDealChange()">
|
<select [(ngModel)]="goiPurchaseId" (ngModelChange)="onGoiDealChange()">
|
||||||
<option [ngValue]="null">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of saleableHoldings; track h.certNo) {
|
@for (h of goiTransferHoldings; track h.certNo) {
|
||||||
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
InvestmentSecurityTransferDto,
|
InvestmentSecurityTransferDto,
|
||||||
InvestmentNonSlrLimitDto,
|
InvestmentNonSlrLimitDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
|
NON_SLR_TYPES,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
toHolding,
|
toHolding,
|
||||||
toInterestTxn,
|
toInterestTxn,
|
||||||
@@ -200,6 +201,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
const tab = this.route.snapshot.queryParamMap.get('tab');
|
const tab = this.route.snapshot.queryParamMap.get('tab');
|
||||||
if (tab === 'interest' || tab === 'redemption' || tab === 'amortize') {
|
if (tab === 'interest' || tab === 'redemption' || tab === 'amortize') {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
|
// The Due Date Diary also names the deal (?purchase=id): it is opened once the holdings have loaded.
|
||||||
|
this.deepLinkPurchaseId = Number(this.route.snapshot.queryParamMap.get('purchase')) || null;
|
||||||
}
|
}
|
||||||
this.investmentService.syncSharedMasters().subscribe({
|
this.investmentService.syncSharedMasters().subscribe({
|
||||||
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
|
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
|
||||||
@@ -230,11 +233,34 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
this.purchaseForm = this.blankPurchaseForm();
|
this.purchaseForm = this.blankPurchaseForm();
|
||||||
this.loadNonSlrLimit();
|
this.loadNonSlrLimit();
|
||||||
this.loadInterestLog();
|
this.loadInterestLog();
|
||||||
|
this.openDeepLinkedDeal();
|
||||||
},
|
},
|
||||||
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
|
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deepLinkPurchaseId: number | null = null;
|
||||||
|
|
||||||
|
/** Selects the deal a Due Date Diary link named, on its tab, and loads it. */
|
||||||
|
private openDeepLinkedDeal(): void {
|
||||||
|
const h = this.holdings.find((x) => x.id === this.deepLinkPurchaseId);
|
||||||
|
this.deepLinkPurchaseId = null;
|
||||||
|
if (!h) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setBondType(h.instrumentType as BondType);
|
||||||
|
if (this.activeTab === 'interest') {
|
||||||
|
this.intPurchaseId = h.id ?? null;
|
||||||
|
this.retrieveInterestSchedule();
|
||||||
|
} else if (this.activeTab === 'redemption') {
|
||||||
|
this.redeemPurchaseId = h.id ?? null;
|
||||||
|
this.onRedeemDealChange();
|
||||||
|
} else if (this.activeTab === 'amortize') {
|
||||||
|
this.amortizePurchaseId = h.id ?? null;
|
||||||
|
this.onAmortizeDealChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Posted coupons / provisions join the purchases in the transaction log, latest date first. */
|
/** Posted coupons / provisions join the purchases in the transaction log, latest date first. */
|
||||||
private loadInterestLog(): void {
|
private loadInterestLog(): void {
|
||||||
forkJoin({
|
forkJoin({
|
||||||
@@ -350,12 +376,30 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBondType(type: BondType): void {
|
setBondType(type: BondType): void {
|
||||||
|
const typeChanged = this.purchaseForm.bondType !== type;
|
||||||
this.bondType = type;
|
this.bondType = type;
|
||||||
this.purchaseForm.bondType = type;
|
this.purchaseForm.bondType = type;
|
||||||
this.purchaseForm.subType = this.defaultSubType(type);
|
this.purchaseForm.subType = this.defaultSubType(type);
|
||||||
|
if (this.isNonSlrPurchase) {
|
||||||
|
this.purchaseForm.category = this.categoryOptions.find((c) => c.startsWith('AFS')) ?? '';
|
||||||
|
} else if (typeChanged && this.purchaseForm.category.startsWith('AFS')) {
|
||||||
|
this.purchaseForm.category = '';
|
||||||
|
}
|
||||||
|
if (typeChanged) {
|
||||||
|
// The script, its ISIN, rating and market rate belong to the old bond type.
|
||||||
|
this.purchaseForm.scriptCode = '';
|
||||||
|
this.purchaseForm.isin = '';
|
||||||
|
this.purchaseForm.bondRating = '';
|
||||||
|
this.purchaseMarketRate = undefined;
|
||||||
|
}
|
||||||
this.loadNonSlrLimit();
|
this.loadNonSlrLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Non-SLR (bonds, mutual funds) can be bought and sold at any time, so it is always AFS (Jitendra Sir). */
|
||||||
|
get purchaseCategoryOptions(): string[] {
|
||||||
|
return this.isNonSlrPurchase ? this.categoryOptions.filter((c) => c.startsWith('AFS')) : this.categoryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
private defaultSubType(type: BondType): string {
|
private defaultSubType(type: BondType): string {
|
||||||
return type === 'GOVT_SECURITY'
|
return type === 'GOVT_SECURITY'
|
||||||
? 'SLR'
|
? 'SLR'
|
||||||
@@ -372,11 +416,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** Bonds, CPs, CDs and mutual funds are non-SLR investments, limited by RBI to 10% of deposits. */
|
/** Bonds, CPs, CDs and mutual funds are non-SLR investments, limited by RBI to 10% of deposits. */
|
||||||
get isNonSlrPurchase(): boolean {
|
get isNonSlrPurchase(): boolean {
|
||||||
return [
|
return NON_SLR_TYPES.includes(this.purchaseForm.bondType);
|
||||||
'NON_SLR',
|
|
||||||
'BOND',
|
|
||||||
'MUTUAL_FUND'
|
|
||||||
].includes(this.purchaseForm.bondType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNonSlrLimit(): void {
|
loadNonSlrLimit(): void {
|
||||||
@@ -530,7 +570,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
fromDist: '',
|
fromDist: '',
|
||||||
toDist: '',
|
toDist: '',
|
||||||
redumMatDt: '',
|
redumMatDt: '',
|
||||||
category: '',
|
// Non-SLR (bond, mutual fund) is always AFS (Jitendra Sir).
|
||||||
|
category: NON_SLR_TYPES.includes(type) ? 'AFS (Available For Sale)' : '',
|
||||||
intRate: null,
|
intRate: null,
|
||||||
intType: 'HALF_YEARLY',
|
intType: 'HALF_YEARLY',
|
||||||
intDate: this.asOnDate,
|
intDate: this.asOnDate,
|
||||||
@@ -640,13 +681,19 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
if (!f.intRate || !this.purchaseMaturityDate) {
|
if (!f.intRate || !this.purchaseMaturityDate) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return approxYtm(f.intRate, f.purchaseRate || 100, yearsBetween(f.issueDate, this.purchaseMaturityDate));
|
// Over the life still to run from settlement, not the security's whole life.
|
||||||
|
return approxYtm(
|
||||||
|
f.intRate,
|
||||||
|
f.purchaseRate || 100,
|
||||||
|
yearsBetween(this.purchaseSettlementDate, this.purchaseMaturityDate)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Broken-period interest owed to the seller from the last coupon date up to the settlement date, 30/360. */
|
/** Broken-period interest owed to the seller from the last coupon date up to the settlement date, 30/360. */
|
||||||
get purchaseAccruedInterest(): number {
|
get purchaseAccruedInterest(): number {
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
if (!f.intRate || !f.lastIntDt || !f.dealDate) {
|
// Non-SLR has no interest scheme (Jitendra Sir): no broken-period interest.
|
||||||
|
if (this.isNonSlrPurchase || !f.intRate || !f.lastIntDt || !f.dealDate) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Math.round((this.purchaseBaseAmount * f.intRate * this.purchaseAccruedDays) / 360) / 100;
|
return Math.round((this.purchaseBaseAmount * f.intRate * this.purchaseAccruedDays) / 360) / 100;
|
||||||
@@ -661,7 +708,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
/** Premium (+) or discount (−) paid over face value. */
|
/** Premium (+) or discount (−) paid over face value. */
|
||||||
get purchasePremiumDiscount(): number {
|
get purchasePremiumDiscount(): number {
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
if (this.isMutualFund || !f.faceValue || !f.purchaseRate) {
|
// Non-SLR is booked at the price paid; no premium is split out or amortised.
|
||||||
|
if (this.isNonSlrPurchase || !f.faceValue || !f.purchaseRate) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Math.round(f.faceValue * (f.purchaseRate - 100)) / 100;
|
return Math.round(f.faceValue * (f.purchaseRate - 100)) / 100;
|
||||||
@@ -682,11 +730,23 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
private suggestLastIntDate(): void {
|
private suggestLastIntDate(): void {
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
const s = this.purchaseScript;
|
const s = this.purchaseScript;
|
||||||
if (!this.isMutualFund && s?.maturityDate && f.dealDate) {
|
// The last coupon on or before SETTLEMENT (one falling between deal and settlement goes to the seller), or the
|
||||||
f.lastIntDt = lastHalfYearlyCouponDate(s.maturityDate, f.dealDate) || f.lastIntDt;
|
// issue date when the security has not paid its first coupon yet — the same date the backend insists on.
|
||||||
|
const settlement = this.purchaseSettlementDate;
|
||||||
|
if (!this.isNonSlrPurchase && s?.maturityDate && settlement) {
|
||||||
|
let last = lastHalfYearlyCouponDate(s.maturityDate, settlement);
|
||||||
|
if (last && s.issueDate && parseDDMMYYYY(last) < parseDDMMYYYY(s.issueDate)) {
|
||||||
|
last = s.issueDate;
|
||||||
|
}
|
||||||
|
f.lastIntDt = last || f.lastIntDt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A different T+n moves the settlement date, and with it the last coupon the deal starts from. */
|
||||||
|
onSettlementTypeChange(): void {
|
||||||
|
this.suggestLastIntDate();
|
||||||
|
}
|
||||||
|
|
||||||
/** Settlement Amt of Purchase — book value, accrued interest payable to the seller and the charges. */
|
/** Settlement Amt of Purchase — book value, accrued interest payable to the seller and the charges. */
|
||||||
get purchaseSettlementAmount(): number {
|
get purchaseSettlementAmount(): number {
|
||||||
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
|
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
|
||||||
@@ -817,7 +877,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** Parties offered for a new deal — inactive parties are hidden. */
|
/** Parties offered for a new deal — inactive parties are hidden. */
|
||||||
get activePartyOptions(): typeof PARTY_MASTER {
|
get activePartyOptions(): typeof PARTY_MASTER {
|
||||||
return this.partyOptions.filter((p) => p.status !== 'INACTIVE');
|
// An FD bank is where deposits are placed, not a dealer securities are bought from or sold to.
|
||||||
|
return this.partyOptions.filter((p) => p.status !== 'INACTIVE' && p.bondType !== 'FD');
|
||||||
}
|
}
|
||||||
|
|
||||||
get purchaseInvestmentGlCode(): string {
|
get purchaseInvestmentGlCode(): string {
|
||||||
@@ -898,7 +959,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
if (!f.faceValue) missing.push('Face Value');
|
if (!f.faceValue) missing.push('Face Value');
|
||||||
if (!f.purchaseRate) missing.push('Purchase Rate');
|
if (!f.purchaseRate) missing.push('Purchase Rate');
|
||||||
if (!f.lastIntDt) missing.push('Last Interest Date');
|
// Non-SLR has no interest scheme, so no last interest date (the field is hidden).
|
||||||
|
if (!this.isNonSlrPurchase && !f.lastIntDt) missing.push('Last Interest Date');
|
||||||
}
|
}
|
||||||
if (!f.category) missing.push('Category');
|
if (!f.category) missing.push('Category');
|
||||||
if (!f.crToAc) missing.push('CR. To A/c');
|
if (!f.crToAc) missing.push('CR. To A/c');
|
||||||
@@ -926,7 +988,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
settlementType: f.settlementType,
|
settlementType: f.settlementType,
|
||||||
...(this.isMutualFund
|
...(this.isMutualFund
|
||||||
? { considerAmt: f.considerAmt, nav: f.nav }
|
? { considerAmt: f.considerAmt, nav: f.nav }
|
||||||
: { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
|
: {
|
||||||
|
faceValue: f.faceValue,
|
||||||
|
purchaseRate: f.purchaseRate,
|
||||||
|
...(this.isNonSlrPurchase ? {} : { lastIntDate: f.lastIntDt })
|
||||||
|
}),
|
||||||
stampDuty: f.stampDuty ?? null,
|
stampDuty: f.stampDuty ?? null,
|
||||||
brokerage: f.brokerage ?? null,
|
brokerage: f.brokerage ?? null,
|
||||||
otherCharges: f.otherCharges ?? null,
|
otherCharges: f.otherCharges ?? null,
|
||||||
@@ -1095,14 +1161,24 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
redeeming = false;
|
redeeming = false;
|
||||||
|
|
||||||
/** Settled holdings of this type still held — the ones that can be sold or redeemed. */
|
/** Settled holdings of this type still held — the ones that can be sold or redeemed. */
|
||||||
|
/** A mutual fund is sold / redeemed at its NAV per unit, not at a price per ₹100. */
|
||||||
|
get saleIsMutualFund(): boolean {
|
||||||
|
return this.holdings.find((h) => h.id === this.sale.purchaseId)?.instrumentType === 'MUTUAL_FUND';
|
||||||
|
}
|
||||||
|
|
||||||
get saleableHoldings(): InvestmentHolding[] {
|
get saleableHoldings(): InvestmentHolding[] {
|
||||||
return this.activeHoldingsForType.filter((h) => !!h.id);
|
return this.activeHoldingsForType.filter((h) => !!h.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redemption is at maturity, so only holdings that have reached it are offered. */
|
/**
|
||||||
|
* Redemption is at maturity, so only holdings that have reached it are offered. A mutual fund has no maturity: it
|
||||||
|
* is redeemed at its NAV through the Sell entry.
|
||||||
|
*/
|
||||||
get redeemableHoldings(): InvestmentHolding[] {
|
get redeemableHoldings(): InvestmentHolding[] {
|
||||||
const today = parseDDMMYYYY(this.asOnDate);
|
const today = parseDDMMYYYY(this.asOnDate);
|
||||||
return this.saleableHoldings.filter((h) => !!h.maturityDate && parseDDMMYYYY(h.maturityDate) <= today);
|
return this.saleableHoldings.filter(
|
||||||
|
(h) => h.instrumentType !== 'MUTUAL_FUND' && !!h.maturityDate && parseDDMMYYYY(h.maturityDate) <= today
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
|
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
|
||||||
@@ -1158,6 +1234,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
(h) =>
|
(h) =>
|
||||||
h.id != null &&
|
h.id != null &&
|
||||||
h.status === 'ACTIVE' &&
|
h.status === 'ACTIVE' &&
|
||||||
|
h.instrumentType === 'GOVT_SECURITY' &&
|
||||||
(!this.intType || h.instrumentType === this.intType) &&
|
(!this.intType || h.instrumentType === this.intType) &&
|
||||||
(!this.intScript || h.scriptCode === this.intScript)
|
(!this.intScript || h.scriptCode === this.intScript)
|
||||||
);
|
);
|
||||||
@@ -1257,7 +1334,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** Total of the lines falling due up to the as-on date. */
|
/** Total of the lines falling due up to the as-on date. */
|
||||||
get intDueTotal(): number {
|
get intDueTotal(): number {
|
||||||
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
|
return (this.intSchedule?.rows ?? []).filter((r) => r.due && !r.posted).reduce((sum, r) => sum + r.amount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Sell Entry --------------------
|
// -------------------- Sell Entry --------------------
|
||||||
@@ -1310,8 +1387,12 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Price, interest from the buyer, cost and profit / loss worked out by the backend, as Settle will post them. */
|
/** Price, interest from the buyer, cost and profit / loss worked out by the backend, as Settle will post them. */
|
||||||
|
/** Each quote request is numbered so a slower, older response cannot overwrite the latest one. */
|
||||||
|
private saleQuoteSeq = 0;
|
||||||
|
|
||||||
refreshSaleQuote(): void {
|
refreshSaleQuote(): void {
|
||||||
const f = this.sale;
|
const f = this.sale;
|
||||||
|
const seq = ++this.saleQuoteSeq;
|
||||||
this.saleQuote = null;
|
this.saleQuote = null;
|
||||||
this.saleQuoteError = '';
|
this.saleQuoteError = '';
|
||||||
if (!f.purchaseId || !f.units || !f.saleRate || !f.dealDate) {
|
if (!f.purchaseId || !f.units || !f.saleRate || !f.dealDate) {
|
||||||
@@ -1329,8 +1410,16 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
...(bankGlAccountId ? { bankGlAccountId } : {})
|
...(bankGlAccountId ? { bankGlAccountId } : {})
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (q) => (this.saleQuote = q),
|
next: (q) => {
|
||||||
error: (err) => (this.saleQuoteError = apiErrorMessage(err))
|
if (seq === this.saleQuoteSeq) {
|
||||||
|
this.saleQuote = q;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
if (seq === this.saleQuoteSeq) {
|
||||||
|
this.saleQuoteError = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1434,6 +1523,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return !!q.premiumCleared && q.premiumCleared > 0 && !!q.premiumGlCode && q.premiumGlCode !== q.investmentGlCode;
|
return !!q.premiumCleared && q.premiumCleared > 0 && !!q.premiumGlCode && q.premiumGlCode !== q.investmentGlCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Non-SLR gain on sale / redemption is its income, 400402; a shortfall is a loss, 500605 (Jitendra Sir). */
|
||||||
|
isNonSlrSale(q: InvestmentSaleDto): boolean {
|
||||||
|
return NON_SLR_TYPES.includes(q.bondType);
|
||||||
|
}
|
||||||
|
|
||||||
/** Book value of a holding as it stands: cost less what sales and write-offs have taken off. */
|
/** Book value of a holding as it stands: cost less what sales and write-offs have taken off. */
|
||||||
carryingValue(h: InvestmentHolding): number {
|
carryingValue(h: InvestmentHolding): number {
|
||||||
const taken = this.sales
|
const taken = this.sales
|
||||||
@@ -1573,6 +1667,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
goiRemarks = '';
|
goiRemarks = '';
|
||||||
goiSaving = false;
|
goiSaving = false;
|
||||||
|
|
||||||
|
/** Only Govt Security holdings can be transferred (the backend refuses bonds, which would skip the non-SLR checks). */
|
||||||
|
get goiTransferHoldings(): InvestmentHolding[] {
|
||||||
|
return this.saleableHoldings.filter((h) => h.instrumentType === 'GOVT_SECURITY');
|
||||||
|
}
|
||||||
|
|
||||||
get goiHolding(): InvestmentHolding | undefined {
|
get goiHolding(): InvestmentHolding | undefined {
|
||||||
return this.holdings.find((h) => h.id === this.goiPurchaseId);
|
return this.holdings.find((h) => h.id === this.goiPurchaseId);
|
||||||
}
|
}
|
||||||
@@ -1763,6 +1862,10 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
* Coupons fall every six months from the purchase's last interest date; a coupon received resets it to zero.
|
* Coupons fall every six months from the purchase's last interest date; a coupon received resets it to zero.
|
||||||
*/
|
*/
|
||||||
accrued(h: InvestmentHolding): number {
|
accrued(h: InvestmentHolding): number {
|
||||||
|
// Non-SLR has no interest scheme: nothing accrues.
|
||||||
|
if (NON_SLR_TYPES.includes(h.instrumentType)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (!h.lastIntDate || h.status !== 'ACTIVE') {
|
if (!h.lastIntDate || h.status !== 'ACTIVE') {
|
||||||
return h.lastIntDate ? 0 : accruedInterest(h, this.asOnDate);
|
return h.lastIntDate ? 0 : accruedInterest(h, this.asOnDate);
|
||||||
}
|
}
|
||||||
@@ -1774,8 +1877,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return Math.round((h.faceValue * h.interestRate * days360(lastCoupon, asOn)) / 360) / 100;
|
return Math.round((h.faceValue * h.interestRate * days360(lastCoupon, asOn)) / 360) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A fund or share has no maturity: it comes back at the book value still held, as Reports and the Dashboard show. */
|
||||||
maturity(h: InvestmentHolding): number {
|
maturity(h: InvestmentHolding): number {
|
||||||
return maturityAmount(h);
|
return h.instrumentType === 'MUTUAL_FUND' || h.instrumentType === 'SHARE'
|
||||||
|
? this.carryingValue(h)
|
||||||
|
: maturityAmount(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
daysLeft(h: InvestmentHolding): number {
|
daysLeft(h: InvestmentHolding): number {
|
||||||
@@ -1833,7 +1939,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
h.faceValue,
|
h.faceValue,
|
||||||
h.interestRate,
|
h.interestRate,
|
||||||
h.purchaseDate,
|
h.purchaseDate,
|
||||||
h.maturityDate,
|
h.instrumentType === 'MUTUAL_FUND' ? '' : h.maturityDate,
|
||||||
this.accrued(h),
|
this.accrued(h),
|
||||||
this.maturity(h),
|
this.maturity(h),
|
||||||
h.status
|
h.status
|
||||||
|
|||||||
@@ -254,7 +254,8 @@
|
|||||||
/></label>
|
/></label>
|
||||||
@if (q.payout !== 'MATURITY') {
|
@if (q.payout !== 'MATURITY') {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Interest per {{ q.payout === 'HALF_YEARLY' ? 'half year' : 'year' }}</span
|
><span
|
||||||
|
>Approx. interest per {{ q.payout === 'HALF_YEARLY' ? 'half year' : 'year' }} (actual days vary)</span
|
||||||
><input type="text" [value]="'₹' + (q.periodInterest | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (q.periodInterest | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
@@ -415,8 +416,10 @@
|
|||||||
>
|
>
|
||||||
Generate Voucher
|
Generate Voucher
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else if (isDueSoon(r.intDate)) {
|
||||||
🔔 Due {{ r.intDate }}
|
🔔 Due {{ r.intDate }}
|
||||||
|
} @else {
|
||||||
|
Scheduled {{ r.intDate }}
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ export class FdInvestmentComponent implements OnInit {
|
|||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
/** An unposted line is due from about two days before its date (the reminder window); before that it is scheduled. */
|
||||||
|
isDueSoon(intDate: string): boolean {
|
||||||
|
const due = parseDDMMYYYY(intDate).getTime();
|
||||||
|
return due - parseDDMMYYYY(todayDDMMYYYY()).getTime() <= 2 * 24 * 3600 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.investmentService.getParties().subscribe({
|
this.investmentService.getParties().subscribe({
|
||||||
next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active && !!p.interestGlAccountId)),
|
next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active && !!p.interestGlAccountId)),
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import {
|
|||||||
InvestmentInterestReportDto,
|
InvestmentInterestReportDto,
|
||||||
InvestmentNonSlrLimitDto,
|
InvestmentNonSlrLimitDto,
|
||||||
InvestmentPurchaseDto,
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentSaleDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
apiErrorMessage
|
apiErrorMessage,
|
||||||
|
bookValueSoldByPurchase
|
||||||
} from '../investment.service';
|
} from '../investment.service';
|
||||||
|
|
||||||
interface InstrumentSummary {
|
interface InstrumentSummary {
|
||||||
@@ -114,12 +116,13 @@ export class InvestmentDashboardComponent implements OnInit {
|
|||||||
bucket: this.investmentService.getBucketReport(today),
|
bucket: this.investmentService.getBucketReport(today),
|
||||||
interest: this.investmentService.getInterestReport(fyStart, today),
|
interest: this.investmentService.getInterestReport(fyStart, today),
|
||||||
purchases: this.investmentService.getPurchases(),
|
purchases: this.investmentService.getPurchases(),
|
||||||
|
sales: this.investmentService.getSales(),
|
||||||
fds: this.investmentService.getFds(),
|
fds: this.investmentService.getFds(),
|
||||||
fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }),
|
fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }),
|
||||||
nonSlr: this.investmentService.getNonSlrLimit(null, today, 0)
|
nonSlr: this.investmentService.getNonSlrLimit(null, today, 0)
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: (r) => {
|
next: (r) => {
|
||||||
this.build(r.bucket, r.interest, r.purchases, r.fds, r.fdQuote.limit ?? null, r.nonSlr);
|
this.build(r.bucket, r.interest, r.purchases, r.sales, r.fds, r.fdQuote.limit ?? null, r.nonSlr);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error: (err: unknown) => {
|
error: (err: unknown) => {
|
||||||
@@ -133,6 +136,7 @@ export class InvestmentDashboardComponent implements OnInit {
|
|||||||
bucket: InvestmentBucketReportDto,
|
bucket: InvestmentBucketReportDto,
|
||||||
interest: InvestmentInterestReportDto,
|
interest: InvestmentInterestReportDto,
|
||||||
purchases: InvestmentPurchaseDto[],
|
purchases: InvestmentPurchaseDto[],
|
||||||
|
sales: InvestmentSaleDto[],
|
||||||
fds: InvestmentFdDto[],
|
fds: InvestmentFdDto[],
|
||||||
fdLimit: InvestmentFdLimitDto | null,
|
fdLimit: InvestmentFdLimitDto | null,
|
||||||
nonSlr: InvestmentNonSlrLimitDto
|
nonSlr: InvestmentNonSlrLimitDto
|
||||||
@@ -192,14 +196,16 @@ export class InvestmentDashboardComponent implements OnInit {
|
|||||||
s.maturityValue +=
|
s.maturityValue +=
|
||||||
r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue;
|
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.
|
// Mutual funds and shares have no maturity, so they are not in the buckets; the units still held are at cost.
|
||||||
|
const sold = bookValueSoldByPurchase(sales);
|
||||||
for (const p of purchases) {
|
for (const p of purchases) {
|
||||||
if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') {
|
if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') {
|
||||||
const s = sum(p.bondType);
|
const s = sum(p.bondType);
|
||||||
|
const book = p.bookValue - (sold.get(p.id) ?? 0);
|
||||||
s.count += 1;
|
s.count += 1;
|
||||||
s.faceValue += p.faceValue;
|
s.faceValue += p.units ? (p.faceValue * p.balanceUnits) / p.units : p.faceValue;
|
||||||
s.bookValue += p.bookValue;
|
s.bookValue += book;
|
||||||
s.maturityValue += p.bookValue;
|
s.maturityValue += book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const order: InstrumentType[] = [
|
const order: InstrumentType[] = [
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export type TxnType =
|
|||||||
| 'WRITE_OFF'
|
| 'WRITE_OFF'
|
||||||
| 'AMORTIZE'
|
| 'AMORTIZE'
|
||||||
| 'TRANSFER'
|
| 'TRANSFER'
|
||||||
| 'PART_PAYMENT';
|
| 'PART_PAYMENT'
|
||||||
|
| 'PREMATURE'
|
||||||
|
| 'RENEWAL';
|
||||||
|
|
||||||
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
||||||
FD: 'Fixed Deposit',
|
FD: 'Fixed Deposit',
|
||||||
@@ -134,6 +136,9 @@ export interface InvestmentHolding {
|
|||||||
partyCode?: string;
|
partyCode?: string;
|
||||||
bookValue?: number;
|
bookValue?: number;
|
||||||
settlementAmount?: number;
|
settlementAmount?: number;
|
||||||
|
/** How and when a closed FD ended (MATURITY / PREMATURE / RENEWAL). */
|
||||||
|
closureType?: string;
|
||||||
|
closedOnDate?: string;
|
||||||
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
|
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
|
||||||
settlementDate?: string;
|
settlementDate?: string;
|
||||||
/** Last coupon date before the purchase; coupons fall every six months from it. */
|
/** Last coupon date before the purchase; coupons fall every six months from it. */
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class InvestmentDiaryComponent implements OnInit {
|
|||||||
return { path: '/investment/fd', query: { tab: 'interest', fd: r.refId } };
|
return { path: '/investment/fd', query: { tab: 'interest', fd: r.refId } };
|
||||||
}
|
}
|
||||||
const tab = r.kind === 'MATURITY' ? 'redemption' : r.kind === 'AMORTISATION' ? 'amortize' : 'interest';
|
const tab = r.kind === 'MATURITY' ? 'redemption' : r.kind === 'AMORTISATION' ? 'amortize' : 'interest';
|
||||||
return { path: '/investment/bonds', query: { tab } };
|
return { path: '/investment/bonds', query: { tab, purchase: r.refId } };
|
||||||
}
|
}
|
||||||
|
|
||||||
statusLabel(r: InvestmentDiaryRowDto): string {
|
statusLabel(r: InvestmentDiaryRowDto): string {
|
||||||
|
|||||||
@@ -301,6 +301,10 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Face Value per Unit</span
|
||||||
|
><input type="number" min="0" step="0.01" placeholder="100" [(ngModel)]="scriptForm.faceValuePerUnit"
|
||||||
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %</span
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %</span
|
||||||
><input type="number" step="0.01" min="0" [(ngModel)]="scriptForm.couponRate"
|
><input type="number" step="0.01" min="0" [(ngModel)]="scriptForm.couponRate"
|
||||||
@@ -364,7 +368,7 @@
|
|||||||
{{
|
{{
|
||||||
scriptRatingEligible
|
scriptRatingEligible
|
||||||
? 'Rating meets the RBI minimum; this security can be bought.'
|
? 'Rating meets the RBI minimum; this security can be bought.'
|
||||||
: 'Below the RBI minimum rating; a purchase of this security will be blocked.'
|
: 'Below the RBI minimum rating, or the wrong scale (CP / CD up to one year: A1+ to A4; bond: AAA to D).'
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -604,21 +608,36 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Open Rate</span><input type="number" [(ngModel)]="newDailyRate.openRate" step="0.01"
|
><span>Open Rate</span><input type="number" [(ngModel)]="newDailyRate.openRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Close Rate *</span><input type="number" [(ngModel)]="newDailyRate.closeRate" step="0.01"
|
><span>Close Rate *</span><input type="number" [(ngModel)]="newDailyRate.closeRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>High Rate</span><input type="number" [(ngModel)]="newDailyRate.highRate" step="0.01"
|
><span>High Rate</span><input type="number" [(ngModel)]="newDailyRate.highRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Low Rate</span><input type="number" [(ngModel)]="newDailyRate.lowRate" step="0.01"
|
><span>Low Rate</span><input type="number" [(ngModel)]="newDailyRate.lowRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addDailyRate()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" (click)="addDailyRate()">Save</button>
|
||||||
|
<label class="pg-btn pg-btn--ghost">
|
||||||
|
{{ uploading ? 'Uploading…' : 'Upload CSV / Excel' }}
|
||||||
|
<input type="file" accept=".csv,.xlsx" hidden [disabled]="uploading" (change)="uploadDailyRates($event)" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
File columns: Script Code (or ISIN), Rate Date (DD-MM-YYYY), Close, Open, High, Low — first row is the header.
|
||||||
|
Use it for the day's market rates and mutual fund NAVs.
|
||||||
|
</p>
|
||||||
|
@if (uploadResults.length) {
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
@for (r of uploadResults; track r.row) {
|
||||||
|
<p class="inv-hint" [class.inv-hint--bad]="!r.ok">Row {{ r.row }}: {{ r.text }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -640,10 +659,11 @@
|
|||||||
<span class="pg-chip">{{ r.scriptCode }}</span>
|
<span class="pg-chip">{{ r.scriptCode }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ r.scriptName }}</td>
|
<td>{{ r.scriptName }}</td>
|
||||||
<td class="num">{{ r.openRate | number: '1.2-2' }}</td>
|
<!-- Mutual fund NAVs run to 4 decimals. -->
|
||||||
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.openRate | number: '1.2-4' }}</td>
|
||||||
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.highRate | number: '1.2-4' }}</td>
|
||||||
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.lowRate | number: '1.2-4' }}</td>
|
||||||
|
<td class="num">{{ r.closeRate | number: '1.2-4' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropDailyRate(r)">Drop</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropDailyRate(r)">Drop</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
BROKER_MASTER,
|
BROKER_MASTER,
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
SLR_TYPE_OPTIONS,
|
SLR_TYPE_OPTIONS,
|
||||||
INVESTMENT_GL_LOOKUP,
|
INVESTMENT_GL_LOOKUP,
|
||||||
ELIGIBLE_RATINGS,
|
ELIGIBLE_RATINGS,
|
||||||
|
parseDDMMYYYY,
|
||||||
RATING_AGENCY_OPTIONS,
|
RATING_AGENCY_OPTIONS,
|
||||||
RATING_OPTIONS,
|
RATING_OPTIONS,
|
||||||
periodBetween
|
periodBetween
|
||||||
@@ -310,7 +312,19 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
|
|
||||||
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
|
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
|
||||||
get scriptRatingEligible(): boolean {
|
get scriptRatingEligible(): boolean {
|
||||||
return !!this.scriptForm.rating && ELIGIBLE_RATINGS.includes(this.scriptForm.rating);
|
const r = this.scriptForm.rating;
|
||||||
|
return !!r && ELIGIBLE_RATINGS.includes(r) && this.scriptIsShortTerm === /^A[1-4]\+?$/.test(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A CP / CD runs one year or less and takes a short-term rating (A1+ to A4); a bond takes a long-term one. */
|
||||||
|
get scriptIsShortTerm(): boolean {
|
||||||
|
const f = this.scriptForm;
|
||||||
|
if (!f.issueDate || !f.maturityDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const issue = parseDDMMYYYY(f.issueDate);
|
||||||
|
const limit = new Date(issue.getFullYear() + 1, issue.getMonth(), issue.getDate());
|
||||||
|
return parseDDMMYYYY(f.maturityDate) <= limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
|
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
|
||||||
@@ -361,6 +375,8 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
glAccountId: Number(f.glAccountId),
|
glAccountId: Number(f.glAccountId),
|
||||||
isin: f.isin ?? '',
|
isin: f.isin ?? '',
|
||||||
intRate: f.couponRate ?? null,
|
intRate: f.couponRate ?? null,
|
||||||
|
// Units = face value / face value per unit (100 for G-Secs by default; bonds can be 1,000 or 10,00,000).
|
||||||
|
...(f.faceValuePerUnit ? { faceValuePerUnit: Number(f.faceValuePerUnit) } : {}),
|
||||||
issueDate: f.issueDate ?? '',
|
issueDate: f.issueDate ?? '',
|
||||||
maturityDate: f.maturityDate ?? '',
|
maturityDate: f.maturityDate ?? '',
|
||||||
companyName: f.companyName ?? '',
|
companyName: f.companyName ?? '',
|
||||||
@@ -541,6 +557,113 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Daily rate upload (CSV / Excel) --------------------
|
||||||
|
/** Result of the last upload, one line per row of the file. */
|
||||||
|
uploadResults: Array<{ row: number; text: string; ok: boolean }> = [];
|
||||||
|
uploading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the day's rates / NAVs from a CSV or Excel file (Jitendra Sir): columns Script Code (or ISIN), Rate Date
|
||||||
|
* (DD-MM-YYYY), Close, and optionally Open, High, Low; the first row is the header. Each row is saved on its own,
|
||||||
|
* so one bad row does not stop the rest.
|
||||||
|
*/
|
||||||
|
async uploadDailyRates(event: Event): Promise<void> {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
input.value = '';
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploadResults = [];
|
||||||
|
let rows: string[][];
|
||||||
|
try {
|
||||||
|
rows = file.name.toLowerCase().endsWith('.xlsx') ? await this.readXlsx(file) : this.readCsv(await file.text());
|
||||||
|
} catch {
|
||||||
|
this.toast('Could not read the file. Use CSV or Excel (.xlsx).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ''));
|
||||||
|
if (!data.length) {
|
||||||
|
this.toast('The file has no rate rows.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploading = true;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const [
|
||||||
|
code,
|
||||||
|
rateDate,
|
||||||
|
close,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low
|
||||||
|
] = data[i].map((c) => c.trim());
|
||||||
|
const script = this.scripts.find(
|
||||||
|
(s) =>
|
||||||
|
s.scriptCode.toUpperCase() === code.toUpperCase() || (!!s.isin && s.isin.toUpperCase() === code.toUpperCase())
|
||||||
|
);
|
||||||
|
const num = (v?: string) => (v ? Number(v.replace(/,/g, '')) : null);
|
||||||
|
if (!script?.id || !rateDate || num(close) == null || isNaN(num(close) as number)) {
|
||||||
|
this.uploadResults.push({
|
||||||
|
row: i + 2,
|
||||||
|
text: `${code || '?'}: script not found or date / close missing`,
|
||||||
|
ok: false
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
scriptId: script.id,
|
||||||
|
rateDate,
|
||||||
|
closeRate: num(close),
|
||||||
|
...(num(open) != null ? { openRate: num(open) } : {}),
|
||||||
|
...(num(high) != null ? { highRate: num(high) } : {}),
|
||||||
|
...(num(low) != null ? { lowRate: num(low) } : {}),
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await firstValueFrom(this.investmentService.createDailyRate(body, true));
|
||||||
|
this.uploadResults.push({ row: i + 2, text: `${script.scriptCode} ${rateDate}: saved`, ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
this.uploadResults.push({
|
||||||
|
row: i + 2,
|
||||||
|
text: `${script.scriptCode} ${rateDate}: ${apiErrorMessage(err)}`,
|
||||||
|
ok: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.uploading = false;
|
||||||
|
const saved = this.uploadResults.filter((r) => r.ok).length;
|
||||||
|
this.toast(`${saved} of ${data.length} rate(s) saved.`);
|
||||||
|
this.loadDailyRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readCsv(text: string): string[][] {
|
||||||
|
return text.split(/\r?\n/).map((line) => line.split(',').map((c) => c.replace(/^"|"$/g, '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readXlsx(file: File): Promise<string[][]> {
|
||||||
|
const ExcelJS = await import('exceljs');
|
||||||
|
const wb = new ExcelJS.Workbook();
|
||||||
|
await wb.xlsx.load(await file.arrayBuffer());
|
||||||
|
const sheet = wb.worksheets[0];
|
||||||
|
const rows: string[][] = [];
|
||||||
|
sheet.eachRow({ includeEmpty: false }, (row) => {
|
||||||
|
const cells: string[] = [];
|
||||||
|
for (let c = 1; c <= 6; c++) {
|
||||||
|
const v = row.getCell(c).value;
|
||||||
|
cells.push(
|
||||||
|
v instanceof Date
|
||||||
|
? `${String(v.getDate()).padStart(2, '0')}-${String(v.getMonth() + 1).padStart(2, '0')}-${v.getFullYear()}`
|
||||||
|
: v == null
|
||||||
|
? ''
|
||||||
|
: String(v)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.push(cells);
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
/** A wrong rate is corrected by dropping it and entering it again. */
|
/** A wrong rate is corrected by dropping it and entering it again. */
|
||||||
dropDailyRate(r: DailyScriptRate): void {
|
dropDailyRate(r: DailyScriptRate): void {
|
||||||
if (!r.id) {
|
if (!r.id) {
|
||||||
|
|||||||
@@ -122,7 +122,8 @@
|
|||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th class="num">Units</th>
|
<th class="num">Units</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th class="num">Market/Purchase Rate</th>
|
<th class="num">Book Value</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -142,14 +143,21 @@
|
|||||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||||
<td class="num">{{ h.units | number }}</td>
|
<td class="num">{{ h.units | number }}</td>
|
||||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||||
<td class="num">{{ marketPrice(h.scriptCode) || h.purchaseRate }}</td>
|
<td class="num">₹{{ h.bookValue ?? h.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">
|
||||||
|
@if (marketPrice(h.scriptCode); as mp) {
|
||||||
|
{{ mp }} <small>(market {{ marketRateDate(h.scriptCode) }})</small>
|
||||||
|
} @else {
|
||||||
|
{{ h.purchaseRate }} <small>(purchase)</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="pg-empty">
|
<td colspan="7" class="pg-empty">
|
||||||
{{ activeReport === 'shareBalance' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
{{ activeReport === 'shareBalance' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -160,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'bankInvList') {
|
@if (activeReport === 'bankInvList') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Bank Investment List</div>
|
<div class="pos-section-label">Bank Investment List — FDs held with each bank</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -383,30 +391,42 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'pendingReceived') {
|
@if (activeReport === 'pendingReceived') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Received Interest / Dividend</div>
|
<div class="pos-section-label">
|
||||||
|
Interest Received — {{ receivedFrom }} to {{ asOnDate }} (coupons, FD payouts and FD interest paid at closure)
|
||||||
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Investment</th>
|
||||||
<th>Cert No</th>
|
<th>Deal No</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
<th class="num col-in">Amount</th>
|
<th class="num col-in">Received</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (t of receivedInterestTxns; track $index) {
|
@for (x of receivedRows; track x.instrument + ':' + x.refId) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
<td>{{ t.certNo }}</td>
|
<td>
|
||||||
<td>{{ t.particulars }}</td>
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
</td>
|
||||||
|
<td>{{ x.particulars }}</td>
|
||||||
|
<td class="num col-in">₹{{ x.received | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="pg-empty">No interest received yet.</td>
|
<td colspan="4" class="pg-empty">No interest received in this period.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@if (receivedRows.length) {
|
||||||
|
<tfoot>
|
||||||
|
<tr class="cctr-total-row">
|
||||||
|
<td colspan="3">Total</td>
|
||||||
|
<td class="num">₹{{ receivedTotal | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
@@ -514,6 +534,8 @@
|
|||||||
<th>Party</th>
|
<th>Party</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th>Maturity Date</th>
|
<th>Maturity Date</th>
|
||||||
|
<th>Closed As</th>
|
||||||
|
<th>Closed On</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -525,10 +547,12 @@
|
|||||||
<td>{{ h.party }}</td>
|
<td>{{ h.party }}</td>
|
||||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||||
<td>{{ h.maturityDate }}</td>
|
<td>{{ h.maturityDate }}</td>
|
||||||
|
<td>{{ h.closureType || '—' }}</td>
|
||||||
|
<td>{{ h.closedOnDate || '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="pg-empty">No matured FDs.</td>
|
<td colspan="6" class="pg-empty">No matured FDs.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -642,7 +666,7 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'shiftingReport') {
|
@if (activeReport === 'shiftingReport') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Shifting Report — Govt Securities transferred to another script / category</div>
|
<div class="pos-section-label">Shifting Report — securities transferred to another script / category</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import {
|
|||||||
InvestmentDailyRateDto,
|
InvestmentDailyRateDto,
|
||||||
InvestmentFdDto,
|
InvestmentFdDto,
|
||||||
InvestmentFdScheduleDto,
|
InvestmentFdScheduleDto,
|
||||||
|
InvestmentInterestReportRowDto,
|
||||||
InvestmentSecurityTransferDto,
|
InvestmentSecurityTransferDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
|
bookValueSoldByPurchase,
|
||||||
toFdHolding,
|
toFdHolding,
|
||||||
toFdTxns,
|
toFdTxns,
|
||||||
toHolding,
|
toHolding,
|
||||||
@@ -75,8 +77,11 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
/** Saved purchases and FDs as holdings, and their GL entries, newest first. */
|
/** Saved purchases and FDs as holdings, and their GL entries, newest first. */
|
||||||
holdings: InvestmentHolding[] = [];
|
holdings: InvestmentHolding[] = [];
|
||||||
txns: InvestmentTxn[] = [];
|
txns: InvestmentTxn[] = [];
|
||||||
/** Coupons and FD interest paid out — interest actually received. */
|
/** Interest received in the financial year to the as-on date, per deal / FD (Interest Received & Receivable report). */
|
||||||
receivedTxns: InvestmentTxn[] = [];
|
receivedRows: InvestmentInterestReportRowDto[] = [];
|
||||||
|
receivedFrom = '';
|
||||||
|
/** Every purchase deal, pending ones included — the Deal Slip register (the ledgers hold only what reached the GL). */
|
||||||
|
dealSlipTxns: InvestmentTxn[] = [];
|
||||||
/** GOI category transfers (security shifted to another script / category). */
|
/** GOI category transfers (security shifted to another script / category). */
|
||||||
transfers: InvestmentSecurityTransferDto[] = [];
|
transfers: InvestmentSecurityTransferDto[] = [];
|
||||||
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
||||||
@@ -85,6 +90,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
private receivable = new Map<string, number>();
|
private receivable = new Map<string, number>();
|
||||||
private fdMaturity = new Map<number, number>();
|
private fdMaturity = new Map<number, number>();
|
||||||
private latestRate = new Map<string, number>();
|
private latestRate = new Map<string, number>();
|
||||||
|
private latestRateDate = new Map<string, string>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
@@ -108,18 +114,24 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
next: (r) => {
|
next: (r) => {
|
||||||
this.fds = r.fds;
|
this.fds = r.fds;
|
||||||
this.transfers = r.transfers;
|
this.transfers = r.transfers;
|
||||||
|
// A holding is shown at the book value still held: cost less what settled sales and write-offs took off.
|
||||||
|
const sold = bookValueSoldByPurchase(r.sales);
|
||||||
this.holdings = [
|
this.holdings = [
|
||||||
...r.purchases.map(toHolding),
|
...r.purchases.map((p) => ({ ...toHolding(p), bookValue: p.bookValue - (sold.get(p.id) ?? 0) })),
|
||||||
...r.fds.map(toFdHolding)
|
...r.fds.map(toFdHolding)
|
||||||
];
|
];
|
||||||
|
// Ledgers show what reached the GL: settled purchases and sales only.
|
||||||
this.txns = this.byDateDesc([
|
this.txns = this.byDateDesc([
|
||||||
...r.purchases.map(toPurchaseTxn),
|
...r.purchases.filter((p) => !!p.glTransactionId).map(toPurchaseTxn),
|
||||||
...r.postings.map(toInterestTxn),
|
...r.postings.map(toInterestTxn),
|
||||||
...r.sales.map(toSaleTxn),
|
...r.sales.filter((x) => x.status === 'SETTLED').map(toSaleTxn),
|
||||||
...r.transfers.map(toTransferTxn),
|
...r.transfers.map(toTransferTxn),
|
||||||
...r.fds.flatMap((f) => toFdTxns(f, []))
|
...r.fds.flatMap((f) => toFdTxns(f, []))
|
||||||
]);
|
]);
|
||||||
this.receivedTxns = this.byDateDesc(r.postings.filter((x) => x.lineType === 'COUPON').map(toInterestTxn));
|
this.dealSlipTxns = this.byDateDesc(r.purchases.map(toPurchaseTxn));
|
||||||
|
// Received = coupons, FD payouts and FD interest paid at closure, as the interest report counts them.
|
||||||
|
this.receivedRows = r.interest.rows.filter((x) => x.received > 0);
|
||||||
|
this.receivedFrom = r.interest.fromDate;
|
||||||
this.receivable = new Map(
|
this.receivable = new Map(
|
||||||
r.interest.rows.map((x) => [
|
r.interest.rows.map((x) => [
|
||||||
x.instrument + ':' + x.refId,
|
x.instrument + ':' + x.refId,
|
||||||
@@ -140,9 +152,12 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
[...r.rates]
|
[...r.rates]
|
||||||
.filter((x) => parseDDMMYYYY(x.rateDate).getTime() <= asOn)
|
.filter((x) => parseDDMMYYYY(x.rateDate).getTime() <= asOn)
|
||||||
.sort((a, b) => parseDDMMYYYY(a.rateDate).getTime() - parseDDMMYYYY(b.rateDate).getTime())
|
.sort((a, b) => parseDDMMYYYY(a.rateDate).getTime() - parseDDMMYYYY(b.rateDate).getTime())
|
||||||
.forEach((x) => this.latestRate.set(x.scriptCode, x.closeRate));
|
.forEach((x) => {
|
||||||
|
this.latestRate.set(x.scriptCode, x.closeRate);
|
||||||
|
this.latestRateDate.set(x.scriptCode, x.rateDate);
|
||||||
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (this.activeReport === 'fdLedger' || this.activeReport === 'pendingReceived') {
|
if (this.activeReport === 'fdLedger') {
|
||||||
this.loadFdInterest();
|
this.loadFdInterest();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,23 +183,15 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
).subscribe((schedules) => {
|
).subscribe((schedules) => {
|
||||||
const lines: InvestmentTxn[] = [];
|
const lines: InvestmentTxn[] = [];
|
||||||
const received: InvestmentTxn[] = [];
|
|
||||||
schedules.forEach((s, i) => {
|
schedules.forEach((s, i) => {
|
||||||
if (!s) {
|
if (s) {
|
||||||
return;
|
lines.push(...toFdTxns(withId[i], s.rows).filter((t) => t.txnType === 'INTEREST'));
|
||||||
}
|
}
|
||||||
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 = this.byDateDesc([
|
||||||
...this.txns,
|
...this.txns,
|
||||||
...lines
|
...lines
|
||||||
]);
|
]);
|
||||||
this.receivedTxns = this.byDateDesc([
|
|
||||||
...this.receivedTxns,
|
|
||||||
...received
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +208,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
|
|
||||||
setReport(report: string): void {
|
setReport(report: string): void {
|
||||||
this.activeReport = report;
|
this.activeReport = report;
|
||||||
if (report === 'fdLedger' || report === 'pendingReceived') {
|
if (report === 'fdLedger') {
|
||||||
this.loadFdInterest();
|
this.loadFdInterest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,6 +245,11 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
return this.latestRate.get(scriptCode) ?? null;
|
return this.latestRate.get(scriptCode) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Date of that latest close. */
|
||||||
|
marketRateDate(scriptCode: string): string {
|
||||||
|
return this.latestRateDate.get(scriptCode) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
private active(type?: InstrumentType): InvestmentHolding[] {
|
private active(type?: InstrumentType): InvestmentHolding[] {
|
||||||
return this.holdings.filter((h) => h.status === 'ACTIVE' && (!type || h.instrumentType === type));
|
return this.holdings.filter((h) => h.status === 'ACTIVE' && (!type || h.instrumentType === type));
|
||||||
}
|
}
|
||||||
@@ -256,9 +268,10 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
get fdHoldings(): InvestmentHolding[] {
|
get fdHoldings(): InvestmentHolding[] {
|
||||||
return this.active('FD');
|
return this.active('FD');
|
||||||
}
|
}
|
||||||
|
/** FDs held with each bank (principal) — securities bought from a dealer are not 'with' that bank. */
|
||||||
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 this.active()) {
|
for (const h of this.active('FD')) {
|
||||||
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;
|
||||||
@@ -286,7 +299,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
||||||
}
|
}
|
||||||
get dealSlips(): InvestmentTxn[] {
|
get dealSlips(): InvestmentTxn[] {
|
||||||
return this.txns.filter((t) => t.txnType === 'PURCHASE' && t.instrumentType !== 'FD');
|
return this.dealSlipTxns;
|
||||||
}
|
}
|
||||||
get slrBondRegister(): InvestmentHolding[] {
|
get slrBondRegister(): InvestmentHolding[] {
|
||||||
return this.active('GOVT_SECURITY');
|
return this.active('GOVT_SECURITY');
|
||||||
@@ -297,8 +310,8 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
.map((h) => ({ ...h, dueAmount: this.accrued(h) }))
|
.map((h) => ({ ...h, dueAmount: this.accrued(h) }))
|
||||||
.filter((h) => h.dueAmount > 0);
|
.filter((h) => h.dueAmount > 0);
|
||||||
}
|
}
|
||||||
get receivedInterestTxns(): InvestmentTxn[] {
|
get receivedTotal(): number {
|
||||||
return this.receivedTxns;
|
return this.receivedRows.reduce((t, x) => t + x.received, 0);
|
||||||
}
|
}
|
||||||
get pendingInterestHoldings(): InvestmentHolding[] {
|
get pendingInterestHoldings(): InvestmentHolding[] {
|
||||||
return this.active().filter((h) => this.accrued(h) > 0);
|
return this.active().filter((h) => this.accrued(h) > 0);
|
||||||
@@ -406,7 +419,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
h.purchaseRate,
|
h.purchaseRate,
|
||||||
h.interestRate,
|
h.interestRate,
|
||||||
h.purchaseDate,
|
h.purchaseDate,
|
||||||
h.maturityDate,
|
h.instrumentType === 'MUTUAL_FUND' ? '' : h.maturityDate,
|
||||||
this.accrued(h),
|
this.accrued(h),
|
||||||
this.maturity(h),
|
this.maturity(h),
|
||||||
h.status
|
h.status
|
||||||
@@ -539,7 +552,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
case 'intDueRegister':
|
case 'intDueRegister':
|
||||||
return one(
|
return one(
|
||||||
'Interest Due Register',
|
'Interest Due Register',
|
||||||
'Interest Due Register (Non-Cumulative Holdings)',
|
'Interest Due Register (interest earned, not yet received)',
|
||||||
[
|
[
|
||||||
...this.holdingsExportHeaders,
|
...this.holdingsExportHeaders,
|
||||||
'Due Amount'
|
'Due Amount'
|
||||||
@@ -565,11 +578,34 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sheetName: 'Received Interest',
|
sheetName: 'Received Interest',
|
||||||
title: 'Received Interest / Dividend',
|
title: 'Interest Received',
|
||||||
subtitle,
|
subtitle: `${this.receivedFrom} to ${this.asOnDate}`,
|
||||||
headers: this.txnExportHeaders,
|
headers: [
|
||||||
widths: this.txnExportWidths,
|
'Investment',
|
||||||
rows: this.receivedInterestTxns.map((t) => this.txnExportRow(t))
|
'Deal No',
|
||||||
|
'Particulars',
|
||||||
|
'Received'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
14,
|
||||||
|
16,
|
||||||
|
44,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...this.receivedRows.map((x) => [
|
||||||
|
x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond',
|
||||||
|
x.dealNo,
|
||||||
|
x.particulars,
|
||||||
|
x.received
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
this.receivedTotal
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sheetName: 'Pending Interest',
|
sheetName: 'Pending Interest',
|
||||||
@@ -631,7 +667,7 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
case 'shiftingReport':
|
case 'shiftingReport':
|
||||||
return one(
|
return one(
|
||||||
'Shifting Report',
|
'Shifting Report',
|
||||||
'Shifting Report — Govt Securities transferred to another script / category',
|
'Shifting Report — securities transferred to another script / category',
|
||||||
[
|
[
|
||||||
'Date',
|
'Date',
|
||||||
'Deal No',
|
'Deal No',
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpContext, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Observable, forkJoin } from 'rxjs';
|
import { Observable, forkJoin } from 'rxjs';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { SILENT_HTTP_ERROR } from '../core/http/silent-error.token';
|
||||||
import {
|
import {
|
||||||
DailyScriptRate,
|
DailyScriptRate,
|
||||||
HoldingStatus,
|
HoldingStatus,
|
||||||
@@ -23,6 +24,13 @@ import {
|
|||||||
SlrType
|
SlrType
|
||||||
} from './investment-data';
|
} from './investment-data';
|
||||||
|
|
||||||
|
/** Non-SLR (bonds, mutual funds): always AFS, no interest scheme; income only on sale / redemption (Jitendra Sir). */
|
||||||
|
export const NON_SLR_TYPES: string[] = [
|
||||||
|
'NON_SLR',
|
||||||
|
'BOND',
|
||||||
|
'MUTUAL_FUND'
|
||||||
|
];
|
||||||
|
|
||||||
/** Investment party as returned by /investment/parties. */
|
/** Investment party as returned by /investment/parties. */
|
||||||
export interface InvestmentPartyDto {
|
export interface InvestmentPartyDto {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -511,8 +519,13 @@ export class InvestmentService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createDailyRate(body: object): Observable<{ resourceId: number }> {
|
/** `silent`: the caller shows the error itself (the rate file upload lists it against its row), so no dialog. */
|
||||||
return this.http.post<{ resourceId: number }>('/investment/daily-rates', body);
|
createDailyRate(body: object, silent = false): Observable<{ resourceId: number }> {
|
||||||
|
return this.http.post<{ resourceId: number }>(
|
||||||
|
'/investment/daily-rates',
|
||||||
|
body,
|
||||||
|
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDailyRate(rateId: number): Observable<unknown> {
|
deleteDailyRate(rateId: number): Observable<unknown> {
|
||||||
@@ -757,6 +770,17 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Book value each deal has given up through settled sales, redemptions, write-offs and amortisation, by purchase id. */
|
||||||
|
export function bookValueSoldByPurchase(sales: InvestmentSaleDto[]): Map<number, number> {
|
||||||
|
const sold = new Map<number, number>();
|
||||||
|
for (const x of sales) {
|
||||||
|
if (x.status === 'SETTLED') {
|
||||||
|
sold.set(x.purchaseId, (sold.get(x.purchaseId) ?? 0) + (x.bookValueSold ?? 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sold;
|
||||||
|
}
|
||||||
|
|
||||||
/** A security transfer shows the book value carried to the new security. */
|
/** A security transfer shows the book value carried to the new security. */
|
||||||
export function toTransferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
|
export function toTransferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
|
||||||
return {
|
return {
|
||||||
@@ -803,10 +827,11 @@ export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const redeem = x.saleType === 'REDEEM';
|
const redeem = x.saleType === 'REDEEM';
|
||||||
|
// A Govt Security redeemed below book writes off its premium; a non-SLR shortfall is a loss (500605).
|
||||||
const pl =
|
const pl =
|
||||||
x.profitLoss >= 0
|
x.profitLoss >= 0
|
||||||
? `profit ₹${x.profitLoss}`
|
? `profit ₹${x.profitLoss}`
|
||||||
: redeem
|
: redeem && !NON_SLR_TYPES.includes(x.bondType)
|
||||||
? `premium written off ₹${-x.profitLoss}`
|
? `premium written off ₹${-x.profitLoss}`
|
||||||
: `loss ₹${-x.profitLoss}`;
|
: `loss ₹${-x.profitLoss}`;
|
||||||
return {
|
return {
|
||||||
@@ -850,10 +875,11 @@ export function toPurchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
|
|||||||
certNo: d.certNo,
|
certNo: d.certNo,
|
||||||
instrumentType: d.bondType,
|
instrumentType: d.bondType,
|
||||||
txnType: 'PURCHASE',
|
txnType: 'PURCHASE',
|
||||||
voucherNo: `INV/PUR/${d.dealNo}`,
|
// The settlement JV's transaction id; a pending deal has no voucher yet.
|
||||||
|
voucherNo: posted ? `TXN ${d.glTransactionId}` : `PENDING/${d.dealNo}`,
|
||||||
txnDate: d.settlementDate,
|
txnDate: d.settlementDate,
|
||||||
amount: d.settlementAmount,
|
amount: d.settlementAmount,
|
||||||
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
|
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]${posted ? '' : ' - PENDING SETTLEMENT'}`,
|
||||||
glCode: posted ? d.investmentGlCode : '',
|
glCode: posted ? d.investmentGlCode : '',
|
||||||
glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
|
glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
|
||||||
};
|
};
|
||||||
@@ -883,6 +909,8 @@ export function toFdHolding(f: InvestmentFdDto): InvestmentHolding {
|
|||||||
crToAc: f.bankGlCode ?? '',
|
crToAc: f.bankGlCode ?? '',
|
||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: f.status === 'ACTIVE' ? 'ACTIVE' : 'MATURED',
|
status: f.status === 'ACTIVE' ? 'ACTIVE' : 'MATURED',
|
||||||
|
closureType: f.closureType,
|
||||||
|
closedOnDate: f.closedOnDate,
|
||||||
bookValue: f.principal
|
bookValue: f.principal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -917,7 +945,12 @@ export function toFdTxns(f: InvestmentFdDto, rows: InvestmentFdInterestRowDto[])
|
|||||||
if (f.closedOnDate) {
|
if (f.closedOnDate) {
|
||||||
out.push({
|
out.push({
|
||||||
...base,
|
...base,
|
||||||
txnType: 'REDEEM',
|
// The ledger's Type column shows how the FD ended: maturity, premature closure or renewal.
|
||||||
|
txnType: (f.closureType === 'PREMATURE'
|
||||||
|
? 'PREMATURE'
|
||||||
|
: f.closureType === 'RENEWAL'
|
||||||
|
? 'RENEWAL'
|
||||||
|
: 'REDEEM') as InvestmentTxn['txnType'],
|
||||||
voucherNo: f.closureVoucherNo ?? `FDC/${f.dealNo}`,
|
voucherNo: f.closureVoucherNo ?? `FDC/${f.dealNo}`,
|
||||||
txnDate: f.closedOnDate,
|
txnDate: f.closedOnDate,
|
||||||
amount: f.closureAmount ?? 0,
|
amount: f.closureAmount ?? 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user