- Correct Investment portfolio allocation and Loan Origination product-mix targets against RBI's UCB SLR/Non-SLR framework and PSL circulars, with a Target Basis column distinguishing RBI-mandated vs board-policy rows (Excel export included). - Expand Investment holdings dataset with more near-term maturities so the Due-Date Diary reflects a realistic spread across FD/Govt Security/Non-SLR/Bond. - Add From/To period-based Interest Calculation & Post (with GL selection for both posting and receiving) across the Investment Interest module, FD Entry and Bond Entry screens; fix a bug where a posted period kept re-appearing as due since accrual wasn't tracked per-holding. - Add a reusable DD-MM-YYYY date-picker component and roll it out across all Investment date fields. - NPA Summary Report: drop SMA-0/1/2 from the stage-wise table (kept in Day-wise Overdue only). - NPA Recovery Report: add Account Type / NPA Category / Target-vs-Actual columns, a shared Account Type filter driving both the account picker and the register, and a searchable (fixed-position, not clipped by card overflow) account select for long lists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1212 lines
38 KiB
TypeScript
1212 lines
38 KiB
TypeScript
/**
|
||
* Copyright since 2025 Mifos Initiative
|
||
*
|
||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
*/
|
||
|
||
import { Component } from '@angular/core';
|
||
import { CommonModule } from '@angular/common';
|
||
import { FormsModule } from '@angular/forms';
|
||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||
import {
|
||
GOVT_SECURITY_HOLDINGS,
|
||
NON_SLR_HOLDINGS,
|
||
BOND_HOLDINGS,
|
||
MUTUAL_FUND_HOLDINGS,
|
||
ALL_TXNS,
|
||
SCRIPT_MASTER,
|
||
PARTY_MASTER,
|
||
SUBTYPE_MASTER,
|
||
BROKER_MASTER,
|
||
SGL_BANK_MASTER,
|
||
MUTUAL_FUND_PRODUCTS,
|
||
LIEN_REASON_MASTER,
|
||
CATEGORY_OPTIONS,
|
||
DEAL_TYPE_OPTIONS,
|
||
MARKET_OPTIONS,
|
||
PAYMENT_MODE_OPTIONS,
|
||
DELIVERY_MODE_OPTIONS,
|
||
BOND_RATING_OPTIONS,
|
||
INT_TYPE_OPTIONS,
|
||
AUTHORITY_OPTIONS,
|
||
INTEREST_INCOME_GL_MASTER,
|
||
INTEREST_INCOME_GL_BY_TYPE,
|
||
InvestmentHolding,
|
||
InvestmentTxn,
|
||
InstrumentType,
|
||
InterestFreq,
|
||
INSTRUMENT_LABELS,
|
||
accruedInterest,
|
||
accruedInterestBetween,
|
||
canAccrueInterest,
|
||
daysToMaturity,
|
||
maturityAmount,
|
||
addPeriod,
|
||
approxYtm,
|
||
yearsBetween,
|
||
todayDDMMYYYY
|
||
} from '../investment-data';
|
||
import { exportWorkbook } from '../investment-export';
|
||
|
||
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
|
||
type BondTab =
|
||
| 'holdings'
|
||
| 'purchase'
|
||
| 'redemption'
|
||
| 'interest'
|
||
| 'sell'
|
||
| 'writeoff'
|
||
| 'amortize'
|
||
| 'amortizeYearly'
|
||
| 'goiTransfer'
|
||
| 'depreciation'
|
||
| 'mfTransfer';
|
||
|
||
interface BondPurchaseForm {
|
||
bondType: BondType;
|
||
dealNo: string;
|
||
dealDate: string;
|
||
dealTime: string;
|
||
isin: string;
|
||
scriptCode: string;
|
||
certNo: string;
|
||
settlementType: 'T+0' | 'T+1' | 'T+2';
|
||
settlementDt: string;
|
||
faceValue: number | null;
|
||
purchaseRate: number | null;
|
||
partyCode: string;
|
||
subType: string;
|
||
tbillGlAc: string;
|
||
crToAc: string;
|
||
premiumAmt: number | null;
|
||
brokerage: number | null;
|
||
brokerName: string;
|
||
tradeNo: string;
|
||
orderNo: string;
|
||
recommAuthority: string;
|
||
sanctionAuthority: string;
|
||
issueDate: string;
|
||
periodYear: number | null;
|
||
periodMonth: number | null;
|
||
periodDays: number | null;
|
||
descInv: string;
|
||
folioNo: string;
|
||
callOption: string;
|
||
meetDt: string;
|
||
resoNo: string;
|
||
fromDist: string;
|
||
toDist: string;
|
||
redumMatDt: string;
|
||
category: string;
|
||
intRate: number | null;
|
||
intType: InterestFreq;
|
||
intDate: string;
|
||
lastIntDt: string;
|
||
dealType: string;
|
||
market: string;
|
||
markCblo: 'YES' | 'NO';
|
||
amortizeAmt: number | null;
|
||
remarks: string;
|
||
lien: 'YES' | 'NO';
|
||
lienAmount: number | null;
|
||
lienReason: string;
|
||
deliveryMode: string;
|
||
bondRating: string;
|
||
corpBond: 'YES' | 'NO';
|
||
paymentMode: string;
|
||
sglWithBank: string;
|
||
description: string;
|
||
registerAdd: string;
|
||
regiCity: string;
|
||
regiPin: string;
|
||
// Mutual Fund specific
|
||
productName: string;
|
||
considerAmt: number | null;
|
||
tradeAmt: number | null;
|
||
nav: number | null;
|
||
holdingDays: number | null;
|
||
stampDuty: number | null;
|
||
stampDutyAc: string;
|
||
}
|
||
|
||
interface BondSellForm {
|
||
/** Legacy screen has SELL / REDEEM / MUTUAL FUND SELL radio at the top. */
|
||
mode: 'SELL' | 'REDEEM' | 'MUTUAL_FUND_SELL';
|
||
/** Purchase Search — locate the original purchase by Deal No or ISIN No. */
|
||
searchBy: 'DEAL_NO' | 'ISIN_NO';
|
||
searchValue: string;
|
||
bondType: BondType;
|
||
scriptCode: string;
|
||
certNo: string;
|
||
buyerCode: string;
|
||
subType: string;
|
||
isin: string;
|
||
tbillGlAc: string;
|
||
dealDate: string;
|
||
dealTime: string;
|
||
settlementType: 'T+0' | 'T+1' | 'T+2';
|
||
settlementDt: string;
|
||
faceValue: number | null;
|
||
sellRate: number | null;
|
||
tdsAmt: number | null;
|
||
unitVal: number | null;
|
||
payAmt: number | null;
|
||
sellUnit: number | null;
|
||
brokerage: number | null;
|
||
dbToAc: string;
|
||
amortizeAmt: number | null;
|
||
recommAuthority: string;
|
||
sanctionAuthority: string;
|
||
callMoney: boolean;
|
||
}
|
||
|
||
/**
|
||
* Combined Govt Security / Non-SLR / Bond / Mutual Fund treasury screen, mirroring the legacy
|
||
* "03 Govt./Bond Entry" submenu in full: Purchase, Redemption, Interest Calculation, Sell,
|
||
* Write-off, Amortize, Amortize Yearly Process, GOI Category Transfer, Investment Depreciation
|
||
* and Mutual Fund Transfer. Dummy in-memory data; Save appends to the local arrays.
|
||
*/
|
||
@Component({
|
||
selector: 'mifosx-bond-investment',
|
||
templateUrl: './bond-investment.component.html',
|
||
styleUrls: [
|
||
'../../cashier/cashier-shared.scss',
|
||
'../investment-shared.scss'
|
||
],
|
||
standalone: true,
|
||
imports: [
|
||
CommonModule,
|
||
FormsModule,
|
||
DateDdmmyyyyPickerComponent
|
||
]
|
||
})
|
||
export class BondInvestmentComponent {
|
||
activeTab: BondTab = 'holdings';
|
||
bondType: BondType = 'GOVT_SECURITY';
|
||
asOnDate = todayDDMMYYYY();
|
||
|
||
holdings: InvestmentHolding[] = [
|
||
...GOVT_SECURITY_HOLDINGS,
|
||
...NON_SLR_HOLDINGS,
|
||
...BOND_HOLDINGS,
|
||
...MUTUAL_FUND_HOLDINGS
|
||
];
|
||
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => ([
|
||
'GOVT_SECURITY',
|
||
'NON_SLR',
|
||
'BOND',
|
||
'MUTUAL_FUND'
|
||
] as InstrumentType[]).includes(t.instrumentType));
|
||
|
||
// Master-driven dropdown sources
|
||
readonly scriptOptions = SCRIPT_MASTER;
|
||
readonly partyOptions = PARTY_MASTER;
|
||
readonly subTypeOptions = SUBTYPE_MASTER;
|
||
readonly brokerOptions = BROKER_MASTER;
|
||
readonly sglBankOptions = SGL_BANK_MASTER;
|
||
readonly mfProductOptions = MUTUAL_FUND_PRODUCTS;
|
||
readonly lienReasonOptions = LIEN_REASON_MASTER;
|
||
readonly categoryOptions = CATEGORY_OPTIONS;
|
||
readonly dealTypeOptions = DEAL_TYPE_OPTIONS;
|
||
readonly marketOptions = MARKET_OPTIONS;
|
||
readonly paymentModeOptions = PAYMENT_MODE_OPTIONS;
|
||
readonly deliveryModeOptions = DELIVERY_MODE_OPTIONS;
|
||
readonly bondRatingOptions = BOND_RATING_OPTIONS;
|
||
readonly intTypeOptions = INT_TYPE_OPTIONS;
|
||
readonly authorityOptions = AUTHORITY_OPTIONS;
|
||
|
||
generating = false;
|
||
toastMsg = '';
|
||
toastVisible = false;
|
||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||
|
||
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
|
||
sellForm: BondSellForm = this.blankSellForm();
|
||
|
||
// Redemption Entry
|
||
redeemCertNo = '';
|
||
redeemDate = this.asOnDate;
|
||
redeemRemarks = '';
|
||
|
||
// Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) —
|
||
// posts interest accrued strictly within [intCalcFromDate, intCalcToDate], same period-based
|
||
// basis as the Investment Interest module's Govt./Bond Interest Calculation.
|
||
intCalcFromDate = addPeriod(this.asOnDate, 0, -3);
|
||
intCalcToDate = this.asOnDate;
|
||
intCalcType: BondType | '' = '';
|
||
intCalcScript = '';
|
||
intCalcDealNo = '';
|
||
intCalcAll = false;
|
||
intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY;
|
||
readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
|
||
|
||
// Write-off
|
||
writeoffCertNo = '';
|
||
writeoffReason = '';
|
||
|
||
// Amortize
|
||
amortizeCertNo = '';
|
||
amortizeAmt: number | null = null;
|
||
|
||
// Amortize Yearly Process
|
||
amortizeYear = String(new Date().getFullYear());
|
||
|
||
// GOI Category Transfer
|
||
goiCertNo = '';
|
||
goiFromCategory = '';
|
||
goiToCategory = '';
|
||
goiTransferDate = this.asOnDate;
|
||
|
||
// Investment Depreciation
|
||
depreciationDate = this.asOnDate;
|
||
depreciationCategory = '';
|
||
|
||
// Mutual Fund Transfer
|
||
mfFromCertNo = '';
|
||
mfToProduct = '';
|
||
mfTransferUnits: number | null = null;
|
||
mfTransferDate = this.asOnDate;
|
||
|
||
readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [
|
||
{ value: 'GOVT_SECURITY', label: 'GOVT SECURITY' },
|
||
{ value: 'NON_SLR', label: 'NON SLR' },
|
||
{ value: 'BOND', label: 'BOND' },
|
||
{ value: 'MUTUAL_FUND', label: 'MUTUAL FUND' }
|
||
];
|
||
|
||
setTab(tab: BondTab): void {
|
||
this.activeTab = tab;
|
||
}
|
||
|
||
setBondType(type: BondType): void {
|
||
this.bondType = type;
|
||
this.purchaseForm.bondType = type;
|
||
}
|
||
|
||
get holdingsForType(): InvestmentHolding[] {
|
||
return this.holdings.filter((h) => h.instrumentType === this.bondType);
|
||
}
|
||
|
||
get activeHoldingsForType(): InvestmentHolding[] {
|
||
return this.holdingsForType.filter((h) => h.status === 'ACTIVE');
|
||
}
|
||
|
||
get txnsForType(): InvestmentTxn[] {
|
||
return this.txns.filter((t) => t.instrumentType === this.bondType);
|
||
}
|
||
|
||
get scriptsForType(): typeof SCRIPT_MASTER {
|
||
return this.scriptOptions.filter((s) => s.instrumentType === this.purchaseForm.bondType);
|
||
}
|
||
|
||
instrumentLabel(type: InstrumentType): string {
|
||
return INSTRUMENT_LABELS[type];
|
||
}
|
||
|
||
private nextDealNo(): string {
|
||
return `2026${String(27000039 + this.holdings.length).padStart(9, '0')}`;
|
||
}
|
||
|
||
private blankPurchaseForm(): BondPurchaseForm {
|
||
const type = this.bondType ?? 'GOVT_SECURITY';
|
||
return {
|
||
bondType: type,
|
||
dealNo: this.nextDealNo(),
|
||
dealDate: this.asOnDate,
|
||
dealTime: '',
|
||
isin: '',
|
||
scriptCode: '',
|
||
certNo: '',
|
||
settlementType: 'T+0',
|
||
settlementDt: this.asOnDate,
|
||
faceValue: null,
|
||
purchaseRate: 100,
|
||
partyCode: '',
|
||
subType: type === 'GOVT_SECURITY' ? 'SLR' : type === 'NON_SLR' ? 'NON SLR' : 'CORPORATE BOND',
|
||
tbillGlAc: '',
|
||
crToAc: '310070',
|
||
premiumAmt: null,
|
||
brokerage: null,
|
||
brokerName: '',
|
||
tradeNo: '',
|
||
orderNo: '',
|
||
recommAuthority: '',
|
||
sanctionAuthority: '',
|
||
issueDate: this.asOnDate,
|
||
periodYear: 3,
|
||
periodMonth: 0,
|
||
periodDays: 0,
|
||
descInv: '',
|
||
folioNo: '',
|
||
callOption: '',
|
||
meetDt: '',
|
||
resoNo: '',
|
||
fromDist: '',
|
||
toDist: '',
|
||
redumMatDt: '',
|
||
category: '',
|
||
intRate: null,
|
||
intType: 'HALF_YEARLY',
|
||
intDate: this.asOnDate,
|
||
lastIntDt: this.asOnDate,
|
||
dealType: 'OUTRIGHT',
|
||
market: 'NDS-OM',
|
||
markCblo: 'NO',
|
||
amortizeAmt: null,
|
||
remarks: '',
|
||
lien: 'NO',
|
||
lienAmount: null,
|
||
lienReason: '',
|
||
deliveryMode: '',
|
||
bondRating: type === 'GOVT_SECURITY' ? 'SOVEREIGN' : 'AAA',
|
||
corpBond: type === 'BOND' ? 'YES' : 'NO',
|
||
paymentMode: '',
|
||
sglWithBank: '',
|
||
description: '',
|
||
registerAdd: '',
|
||
regiCity: '',
|
||
regiPin: '',
|
||
productName: '',
|
||
considerAmt: null,
|
||
tradeAmt: null,
|
||
nav: null,
|
||
holdingDays: null,
|
||
stampDuty: null,
|
||
stampDutyAc: ''
|
||
};
|
||
}
|
||
|
||
private blankSellForm(): BondSellForm {
|
||
return {
|
||
mode: 'SELL',
|
||
searchBy: 'DEAL_NO',
|
||
searchValue: '',
|
||
bondType: this.bondType ?? 'GOVT_SECURITY',
|
||
scriptCode: '',
|
||
certNo: '',
|
||
buyerCode: '',
|
||
subType: '',
|
||
isin: '',
|
||
tbillGlAc: '',
|
||
dealDate: this.asOnDate,
|
||
dealTime: '',
|
||
settlementType: 'T+0',
|
||
settlementDt: this.asOnDate,
|
||
faceValue: null,
|
||
sellRate: null,
|
||
tdsAmt: null,
|
||
unitVal: 100,
|
||
payAmt: null,
|
||
sellUnit: null,
|
||
brokerage: null,
|
||
dbToAc: '310070',
|
||
amortizeAmt: null,
|
||
recommAuthority: '',
|
||
sanctionAuthority: '',
|
||
callMoney: false
|
||
};
|
||
}
|
||
|
||
get isMutualFund(): boolean {
|
||
return this.purchaseForm.bondType === 'MUTUAL_FUND';
|
||
}
|
||
|
||
/** Face value for the purchase — Mutual Fund screens label it "Consider Amt" instead. */
|
||
get purchaseBaseAmount(): number {
|
||
const f = this.purchaseForm;
|
||
return (this.isMutualFund ? f.considerAmt : f.faceValue) || 0;
|
||
}
|
||
|
||
get purchaseMaturityDate(): string {
|
||
const f = this.purchaseForm;
|
||
if (f.redumMatDt) {
|
||
return f.redumMatDt;
|
||
}
|
||
if (!f.issueDate) {
|
||
return '';
|
||
}
|
||
return addPeriod(f.issueDate, f.periodYear || 0, f.periodMonth || 0, f.periodDays || 0);
|
||
}
|
||
|
||
get purchaseUnits(): number {
|
||
const f = this.purchaseForm;
|
||
const amt = this.purchaseBaseAmount;
|
||
if (this.isMutualFund) {
|
||
return f.nav ? +(amt / f.nav).toFixed(3) : 0;
|
||
}
|
||
return f.purchaseRate ? Math.round((amt / f.purchaseRate) * (f.purchaseRate / 100)) : 0;
|
||
}
|
||
|
||
/** Unit Val — the per-unit face value used by the legacy form (100 for G-Sec/Bonds, NAV for MF). */
|
||
get purchaseUnitVal(): number {
|
||
return this.isMutualFund ? this.purchaseForm.nav || 0 : 100;
|
||
}
|
||
|
||
/** Book Value — what the investment is carried at, i.e. amount actually paid. */
|
||
get purchaseBookValue(): number {
|
||
const f = this.purchaseForm;
|
||
if (this.isMutualFund) {
|
||
return this.purchaseBaseAmount + (f.stampDuty || 0) + (f.brokerage || 0);
|
||
}
|
||
const rate = f.purchaseRate || 100;
|
||
return Math.round((this.purchaseBaseAmount * rate) / 100) + (f.brokerage || 0);
|
||
}
|
||
|
||
get purchaseYtm(): number {
|
||
const f = this.purchaseForm;
|
||
if (!f.intRate || !this.purchaseMaturityDate) {
|
||
return 0;
|
||
}
|
||
return approxYtm(f.intRate, f.purchaseRate || 100, yearsBetween(f.issueDate, this.purchaseMaturityDate));
|
||
}
|
||
|
||
/** Accrued Interest at purchase — interest earned by the seller since the last coupon date. */
|
||
get purchaseAccruedInterest(): number {
|
||
const f = this.purchaseForm;
|
||
if (!f.intRate || !f.lastIntDt || !f.dealDate) {
|
||
return 0;
|
||
}
|
||
const yrs = Math.max(0, yearsBetween(f.lastIntDt, f.dealDate));
|
||
return Math.round(this.purchaseBaseAmount * (f.intRate / 100) * yrs);
|
||
}
|
||
|
||
/** Settlement Amt of Purchase — book value plus accrued interest payable to the seller. */
|
||
get purchaseSettlementAmount(): number {
|
||
return this.purchaseBookValue + this.purchaseAccruedInterest;
|
||
}
|
||
|
||
get purchaseMaturityAmount(): number {
|
||
const f = this.purchaseForm;
|
||
if (!this.purchaseBaseAmount || !this.purchaseMaturityDate) {
|
||
return this.purchaseBaseAmount;
|
||
}
|
||
return maturityAmount({
|
||
certNo: f.certNo,
|
||
instrumentType: f.bondType,
|
||
scriptCode: f.scriptCode,
|
||
scriptName: '',
|
||
party: f.partyCode,
|
||
subType: f.subType,
|
||
faceValue: this.purchaseBaseAmount,
|
||
purchaseRate: f.purchaseRate || 100,
|
||
units: this.purchaseUnits,
|
||
purchaseDate: f.issueDate,
|
||
maturityDate: this.purchaseMaturityDate,
|
||
interestRate: f.intRate || 0,
|
||
interestFreq: this.isMutualFund ? 'CUMULATIVE' : f.intType,
|
||
glCode: '',
|
||
glName: '',
|
||
crToAc: f.crToAc,
|
||
brokerCode: f.brokerName,
|
||
status: 'ACTIVE'
|
||
});
|
||
}
|
||
|
||
/** Fills ISIN + interest rate when a script is picked, mirroring the legacy form's lookup. */
|
||
onScriptChange(): void {
|
||
const s = this.scriptOptions.find((x) => x.scriptCode === this.purchaseForm.scriptCode);
|
||
if (s) {
|
||
this.purchaseForm.isin = s.isin;
|
||
}
|
||
}
|
||
|
||
savePurchase(): void {
|
||
const f = this.purchaseForm;
|
||
if (!f.certNo || !this.purchaseBaseAmount || !f.partyCode) {
|
||
this.toast('Please fill Cert No, Face Value / Consider Amt and Party Code.');
|
||
return;
|
||
}
|
||
const holding: InvestmentHolding = {
|
||
certNo: f.certNo,
|
||
instrumentType: f.bondType,
|
||
scriptCode: f.scriptCode || f.isin || f.productName,
|
||
scriptName: this.isMutualFund
|
||
? f.productName
|
||
: this.scriptOptions.find((s) => s.scriptCode === f.scriptCode)?.scriptName || f.scriptCode || f.isin,
|
||
party: f.partyCode,
|
||
subType: f.subType,
|
||
faceValue: this.purchaseBaseAmount,
|
||
purchaseRate: this.isMutualFund ? f.nav || 0 : f.purchaseRate || 100,
|
||
units: this.purchaseUnits,
|
||
purchaseDate: f.issueDate,
|
||
maturityDate: this.purchaseMaturityDate || f.issueDate,
|
||
interestRate: f.intRate || 0,
|
||
interestFreq: this.isMutualFund ? 'CUMULATIVE' : f.intType,
|
||
glCode:
|
||
'31' +
|
||
(f.bondType === 'GOVT_SECURITY'
|
||
? '0020'
|
||
: f.bondType === 'NON_SLR'
|
||
? '0030'
|
||
: f.bondType === 'BOND'
|
||
? '0040'
|
||
: '0050'),
|
||
glName: `INVESTMENT IN ${INSTRUMENT_LABELS[f.bondType].toUpperCase()}`,
|
||
crToAc: f.crToAc,
|
||
brokerCode: f.brokerName,
|
||
status: 'ACTIVE'
|
||
};
|
||
this.holdings = [
|
||
holding,
|
||
...this.holdings
|
||
];
|
||
this.txns = [
|
||
{
|
||
certNo: holding.certNo,
|
||
instrumentType: f.bondType,
|
||
txnType: 'PURCHASE',
|
||
voucherNo: `INV/PUR/${f.dealNo}`,
|
||
txnDate: f.dealDate,
|
||
amount: this.purchaseSettlementAmount,
|
||
particulars: `Purchase Deal ${f.dealNo} - ${holding.scriptName} - ${holding.party}${f.category ? ' [' + f.category + ']' : ''}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`${INSTRUMENT_LABELS[f.bondType]} Certificate ${holding.certNo} saved successfully.`);
|
||
this.purchaseForm = this.blankPurchaseForm();
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
cancelPurchase(): void {
|
||
this.purchaseForm = this.blankPurchaseForm();
|
||
this.toast('Entry cancelled — form reset.');
|
||
}
|
||
|
||
/** Legacy "Retrieve" button — loads an existing deal back into the form by Cert No. */
|
||
retrievePurchase(): void {
|
||
const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo.trim());
|
||
if (!h) {
|
||
this.toast(`Cert No ${this.purchaseForm.certNo} not found.`);
|
||
return;
|
||
}
|
||
this.purchaseForm = {
|
||
...this.blankPurchaseForm(),
|
||
bondType: h.instrumentType as BondType,
|
||
certNo: h.certNo,
|
||
scriptCode: h.scriptCode,
|
||
isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '',
|
||
partyCode: h.party,
|
||
subType: h.subType,
|
||
faceValue: h.faceValue,
|
||
considerAmt: h.faceValue,
|
||
purchaseRate: h.purchaseRate,
|
||
nav: h.purchaseRate,
|
||
issueDate: h.purchaseDate,
|
||
redumMatDt: h.maturityDate,
|
||
intRate: h.interestRate,
|
||
intType: h.interestFreq,
|
||
brokerName: h.brokerCode,
|
||
crToAc: h.crToAc,
|
||
productName: h.scriptName
|
||
};
|
||
this.bondType = h.instrumentType as BondType;
|
||
this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
|
||
}
|
||
|
||
dropPurchase(): void {
|
||
const certNo = this.purchaseForm.certNo;
|
||
if (!this.holdings.some((h) => h.certNo === certNo)) {
|
||
this.toast('Nothing to drop — this deal is not yet saved.');
|
||
return;
|
||
}
|
||
this.holdings = this.holdings.filter((h) => h.certNo !== certNo);
|
||
this.txns = this.txns.filter((t) => t.certNo !== certNo);
|
||
this.purchaseForm = this.blankPurchaseForm();
|
||
this.toast(`Cert No ${certNo} dropped.`);
|
||
}
|
||
|
||
exitPurchase(): void {
|
||
this.purchaseForm = this.blankPurchaseForm();
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
// -------------------- Redemption Entry --------------------
|
||
get redeemHolding(): InvestmentHolding | undefined {
|
||
return this.holdingsForType.find((h) => h.certNo === this.redeemCertNo);
|
||
}
|
||
|
||
get redeemAmount(): number {
|
||
const h = this.redeemHolding;
|
||
return h ? maturityAmount(h) + accruedInterest(h, this.redeemDate) - h.faceValue + h.faceValue : 0;
|
||
}
|
||
|
||
saveRedemption(): void {
|
||
const h = this.redeemHolding;
|
||
if (!h) {
|
||
this.toast('Select a certificate to redeem.');
|
||
return;
|
||
}
|
||
h.status = 'MATURED';
|
||
this.txns = [
|
||
{
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: 'REDEEM',
|
||
voucherNo: `INV/RED/${h.certNo}`,
|
||
txnDate: this.redeemDate,
|
||
amount: this.redeemAmount,
|
||
particulars: `Redemption on maturity - ${h.scriptName}${this.redeemRemarks ? ' - ' + this.redeemRemarks : ''}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`${h.certNo} redeemed. Proceeds ₹${this.redeemAmount.toLocaleString('en-IN')}.`);
|
||
this.redeemCertNo = '';
|
||
this.redeemRemarks = '';
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
// -------------------- Interest calculation --------------------
|
||
/** Holdings the Interest Calculation "Process" run would cover, given the filters. A
|
||
* Non-Performing holding is always excluded — same accrual-basis rule as the Investment
|
||
* Interest module (it's still collectible on cash basis via Interest Receive there). */
|
||
get intCalcTargets(): InvestmentHolding[] {
|
||
const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
|
||
if (this.intCalcAll) {
|
||
return base;
|
||
}
|
||
return base.filter((h) => {
|
||
if (this.intCalcType && h.instrumentType !== this.intCalcType) return false;
|
||
if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false;
|
||
if (this.intCalcDealNo && h.certNo !== this.intCalcDealNo.trim()) return false;
|
||
return true;
|
||
});
|
||
}
|
||
|
||
get intCalcExcludedNpi(): InvestmentHolding[] {
|
||
return this.holdings.filter((h) => h.status === 'ACTIVE' && !canAccrueInterest(h));
|
||
}
|
||
|
||
/** Re-suggests the Interest Income GL when the Type filter changes — still overridable. */
|
||
onIntCalcTypeChange(): void {
|
||
if (this.intCalcType) {
|
||
this.intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE[this.intCalcType];
|
||
}
|
||
}
|
||
|
||
glName(glCode: string): string {
|
||
return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
|
||
}
|
||
|
||
/** Interest accrued strictly within [intCalcFromDate, intCalcToDate] — a period-based
|
||
* Interest Calculation & Post run, same basis as the Investment Interest module. */
|
||
interestAccrued(h: InvestmentHolding): number {
|
||
return accruedInterestBetween(h, this.intCalcFromDate || h.purchaseDate, this.intCalcToDate || this.asOnDate);
|
||
}
|
||
|
||
processInterest(): void {
|
||
const targets = this.intCalcTargets.filter((h) => this.interestAccrued(h) > 0);
|
||
if (targets.length === 0) {
|
||
this.toast('No holdings have interest due to be posted for this period.');
|
||
return;
|
||
}
|
||
const toDate = this.intCalcToDate || this.asOnDate;
|
||
const fromDate = this.intCalcFromDate || toDate;
|
||
const glCode = this.intCalcGlCode;
|
||
const glName = this.glName(glCode);
|
||
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
|
||
const amt = this.interestAccrued(h);
|
||
h.interestPosted = (h.interestPosted ?? 0) + amt;
|
||
h.interestPostedTillDate = toDate;
|
||
return {
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: 'INTEREST',
|
||
voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`,
|
||
txnDate: toDate,
|
||
amount: amt,
|
||
particulars: `Interest calculated (accrual) for period ${fromDate} to ${toDate} - ${h.scriptName}`,
|
||
glCode,
|
||
glName
|
||
};
|
||
});
|
||
this.txns = [
|
||
...newTxns,
|
||
...this.txns
|
||
];
|
||
this.intCalcFromDate = toDate;
|
||
const total = newTxns.reduce((s, t) => s + t.amount, 0);
|
||
this.toast(
|
||
`Interest processed for ${targets.length} holding(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.`
|
||
);
|
||
}
|
||
|
||
// -------------------- Sell / Redeem Entry --------------------
|
||
/** Locates the original purchase by Deal No / Cert No or ISIN and fills the sell form. */
|
||
searchPurchaseForSell(): void {
|
||
const val = this.sellForm.searchValue.trim();
|
||
if (!val) {
|
||
this.toast('Enter a Deal No / Cert No or ISIN to search.');
|
||
return;
|
||
}
|
||
const h =
|
||
this.sellForm.searchBy === 'DEAL_NO'
|
||
? this.holdings.find((x) => x.certNo === val)
|
||
: this.holdings.find((x) => this.scriptOptions.find((s) => s.scriptCode === x.scriptCode)?.isin === val);
|
||
if (!h) {
|
||
this.toast('No matching purchase found.');
|
||
return;
|
||
}
|
||
this.sellForm = {
|
||
...this.sellForm,
|
||
bondType: h.instrumentType as BondType,
|
||
scriptCode: h.scriptCode,
|
||
certNo: h.certNo,
|
||
subType: h.subType,
|
||
isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '',
|
||
faceValue: h.faceValue,
|
||
unitVal: h.instrumentType === 'MUTUAL_FUND' ? h.purchaseRate : 100,
|
||
sellUnit: h.units
|
||
};
|
||
this.toast(`Purchase ${h.certNo} loaded.`);
|
||
}
|
||
|
||
get sellHolding(): InvestmentHolding | undefined {
|
||
return this.holdings.find((h) => h.certNo === this.sellForm.certNo);
|
||
}
|
||
|
||
/** Consideration = units sold × sell rate, scaled by the instrument's unit convention. */
|
||
get sellConsideration(): number {
|
||
const f = this.sellForm;
|
||
if (!f.sellUnit || !f.sellRate) {
|
||
return 0;
|
||
}
|
||
const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100;
|
||
return Math.round((f.sellUnit * f.sellRate) / perUnitDivisor);
|
||
}
|
||
|
||
/** Sale Diff — profit/loss versus what the holding was bought at. */
|
||
get sellSaleDiff(): number {
|
||
const h = this.sellHolding;
|
||
const f = this.sellForm;
|
||
if (!h || !f.sellUnit || !f.sellRate) {
|
||
return 0;
|
||
}
|
||
const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100;
|
||
const cost = Math.round((f.sellUnit * h.purchaseRate) / perUnitDivisor);
|
||
return this.sellConsideration - cost;
|
||
}
|
||
|
||
/** Premium Loss — the unamortised premium written off when a holding is sold early. */
|
||
get sellPremiumLoss(): number {
|
||
const h = this.sellHolding;
|
||
if (!h || h.purchaseRate <= 100 || h.instrumentType === 'MUTUAL_FUND') {
|
||
return 0;
|
||
}
|
||
return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100);
|
||
}
|
||
|
||
get sellAccruedInterest(): number {
|
||
const h = this.sellHolding;
|
||
return h ? accruedInterest(h, this.sellForm.dealDate || this.asOnDate) : 0;
|
||
}
|
||
|
||
get sellAccruedDays(): number {
|
||
const h = this.sellHolding;
|
||
if (!h) {
|
||
return 0;
|
||
}
|
||
return Math.max(0, Math.round(yearsBetween(h.purchaseDate, this.sellForm.dealDate || this.asOnDate) * 365));
|
||
}
|
||
|
||
/** Pay Amt — net amount receivable: consideration + accrued interest − TDS − brokerage. */
|
||
get sellPayAmount(): number {
|
||
const f = this.sellForm;
|
||
return this.sellConsideration + this.sellAccruedInterest - (f.tdsAmt || 0) - (f.brokerage || 0);
|
||
}
|
||
|
||
saveSell(): void {
|
||
const f = this.sellForm;
|
||
const h = this.sellHolding;
|
||
if (!h || !f.sellRate || !f.sellUnit) {
|
||
this.toast('Load a purchase, then enter Sell Rate and Sell Unit.');
|
||
return;
|
||
}
|
||
if (f.sellUnit > h.units) {
|
||
this.toast(`Sell Unit cannot exceed units held (${h.units}).`);
|
||
return;
|
||
}
|
||
h.units -= f.sellUnit;
|
||
h.faceValue = Math.round(h.units * (f.unitVal || 100));
|
||
if (h.units <= 0) {
|
||
h.status = f.mode === 'REDEEM' ? 'MATURED' : 'SOLD';
|
||
}
|
||
this.txns = [
|
||
{
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: f.mode === 'REDEEM' ? 'REDEEM' : 'SELL',
|
||
voucherNo: `INV/${f.mode === 'REDEEM' ? 'RED' : 'SEL'}/${h.certNo}-${this.txns.length + 1}`,
|
||
txnDate: f.dealDate,
|
||
amount: this.sellPayAmount,
|
||
particulars: `${f.mode.replace(/_/g, ' ')} - ${h.scriptName} @ ${f.sellRate}${f.buyerCode ? ' to ' + f.buyerCode : ''}${f.callMoney ? ' [CALL MONEY]' : ''}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(
|
||
`${h.certNo} ${f.mode === 'REDEEM' ? 'redeemed' : 'sold'}. Pay Amt ₹${this.sellPayAmount.toLocaleString('en-IN')}.`
|
||
);
|
||
this.sellForm = this.blankSellForm();
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
cancelSell(): void {
|
||
this.sellForm = this.blankSellForm();
|
||
this.toast('Sell entry cancelled — form reset.');
|
||
}
|
||
|
||
exitSell(): void {
|
||
this.sellForm = this.blankSellForm();
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
// -------------------- Write-off --------------------
|
||
get writeoffHolding(): InvestmentHolding | undefined {
|
||
return this.holdingsForType.find((h) => h.certNo === this.writeoffCertNo);
|
||
}
|
||
|
||
saveWriteoff(): void {
|
||
const h = this.writeoffHolding;
|
||
if (!h || !this.writeoffReason) {
|
||
this.toast('Select a certificate and enter a write-off reason.');
|
||
return;
|
||
}
|
||
h.status = 'WRITTEN_OFF';
|
||
this.txns = [
|
||
{
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: 'WRITE_OFF',
|
||
voucherNo: `INV/WOF/${h.certNo}`,
|
||
txnDate: this.asOnDate,
|
||
amount: h.faceValue,
|
||
particulars: `Written off - ${this.writeoffReason}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`${h.certNo} written off.`);
|
||
this.writeoffCertNo = '';
|
||
this.writeoffReason = '';
|
||
this.activeTab = 'holdings';
|
||
}
|
||
|
||
// -------------------- Amortize --------------------
|
||
get amortizeHolding(): InvestmentHolding | undefined {
|
||
return this.holdingsForType.find((h) => h.certNo === this.amortizeCertNo);
|
||
}
|
||
|
||
/** Straight-line premium amortisation for one year on the selected holding. */
|
||
get suggestedAmortizeAmt(): number {
|
||
const h = this.amortizeHolding;
|
||
if (!h || h.purchaseRate <= 100) {
|
||
return 0;
|
||
}
|
||
const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate));
|
||
return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years);
|
||
}
|
||
|
||
saveAmortize(): void {
|
||
const h = this.amortizeHolding;
|
||
const amt = this.amortizeAmt ?? this.suggestedAmortizeAmt;
|
||
if (!h || !amt) {
|
||
this.toast('Select a certificate and enter the amortization amount.');
|
||
return;
|
||
}
|
||
this.txns = [
|
||
{
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: 'AMORTIZE',
|
||
voucherNo: `INV/AMZ/${h.certNo}-${this.txns.length + 1}`,
|
||
txnDate: this.asOnDate,
|
||
amount: amt,
|
||
particulars: `Premium/discount amortized - ${h.scriptName}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`Amortization of ₹${amt.toLocaleString('en-IN')} booked for ${h.certNo}.`);
|
||
this.amortizeCertNo = '';
|
||
this.amortizeAmt = null;
|
||
}
|
||
|
||
// -------------------- Amortize Yearly Process --------------------
|
||
/** All premium-carrying holdings the yearly batch would amortise. */
|
||
get amortizeYearlyTargets(): Array<{ holding: InvestmentHolding; amount: number }> {
|
||
return this.holdings
|
||
.filter((h) => h.status === 'ACTIVE' && h.purchaseRate > 100 && h.instrumentType !== 'MUTUAL_FUND')
|
||
.map((h) => {
|
||
const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate));
|
||
return { holding: h, amount: Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years) };
|
||
});
|
||
}
|
||
|
||
get amortizeYearlyTotal(): number {
|
||
return this.amortizeYearlyTargets.reduce((s, t) => s + t.amount, 0);
|
||
}
|
||
|
||
runAmortizeYearly(): void {
|
||
const targets = this.amortizeYearlyTargets;
|
||
if (targets.length === 0) {
|
||
this.toast('No premium-carrying holdings to amortise.');
|
||
return;
|
||
}
|
||
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
|
||
certNo: t.holding.certNo,
|
||
instrumentType: t.holding.instrumentType,
|
||
txnType: 'AMORTIZE',
|
||
voucherNo: `INV/AMZ-YR/${this.amortizeYear}-${i + 1}`,
|
||
txnDate: this.asOnDate,
|
||
amount: t.amount,
|
||
particulars: `Yearly amortization process ${this.amortizeYear} - ${t.holding.scriptName}`
|
||
}));
|
||
this.txns = [
|
||
...newTxns,
|
||
...this.txns
|
||
];
|
||
this.toast(
|
||
`Yearly amortization run for ${targets.length} holding(s). Total ₹${this.amortizeYearlyTotal.toLocaleString('en-IN')}.`
|
||
);
|
||
}
|
||
|
||
// -------------------- GOI Category Transfer --------------------
|
||
get goiHolding(): InvestmentHolding | undefined {
|
||
return this.holdings.find((h) => h.certNo === this.goiCertNo);
|
||
}
|
||
|
||
saveGoiTransfer(): void {
|
||
const h = this.goiHolding;
|
||
if (!h || !this.goiFromCategory || !this.goiToCategory) {
|
||
this.toast('Select a certificate and both From / To categories.');
|
||
return;
|
||
}
|
||
if (this.goiFromCategory === this.goiToCategory) {
|
||
this.toast('From and To categories must differ.');
|
||
return;
|
||
}
|
||
this.txns = [
|
||
{
|
||
certNo: h.certNo,
|
||
instrumentType: h.instrumentType,
|
||
txnType: 'AMORTIZE',
|
||
voucherNo: `INV/GOI-TRF/${h.certNo}-${this.txns.length + 1}`,
|
||
txnDate: this.goiTransferDate,
|
||
amount: h.faceValue,
|
||
particulars: `GOI category transfer ${this.goiFromCategory} → ${this.goiToCategory} - ${h.scriptName}`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`${h.certNo} transferred from ${this.goiFromCategory} to ${this.goiToCategory}.`);
|
||
this.goiCertNo = '';
|
||
this.goiFromCategory = '';
|
||
this.goiToCategory = '';
|
||
}
|
||
|
||
// -------------------- Investment Depreciation --------------------
|
||
/** AFS/HFT holdings whose market price is below book value must be marked down (RBI norms). */
|
||
get depreciationTargets(): Array<{ holding: InvestmentHolding; marketPrice: number; depreciation: number }> {
|
||
return this.holdings
|
||
.filter((h) => h.status === 'ACTIVE' && h.instrumentType !== 'MUTUAL_FUND')
|
||
.map((h) => {
|
||
const md = this.scriptOptions.find((s) => s.scriptCode === h.scriptCode);
|
||
const marketPrice = md ? h.purchaseRate * 0.995 : h.purchaseRate;
|
||
const depreciation =
|
||
marketPrice < h.purchaseRate ? Math.round((h.faceValue * (h.purchaseRate - marketPrice)) / 100) : 0;
|
||
return { holding: h, marketPrice: +marketPrice.toFixed(2), depreciation };
|
||
})
|
||
.filter((r) => r.depreciation > 0);
|
||
}
|
||
|
||
get depreciationTotal(): number {
|
||
return this.depreciationTargets.reduce((s, r) => s + r.depreciation, 0);
|
||
}
|
||
|
||
runDepreciation(): void {
|
||
const targets = this.depreciationTargets;
|
||
if (targets.length === 0) {
|
||
this.toast('No holdings require depreciation provisioning.');
|
||
return;
|
||
}
|
||
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
|
||
certNo: t.holding.certNo,
|
||
instrumentType: t.holding.instrumentType,
|
||
txnType: 'AMORTIZE',
|
||
voucherNo: `INV/DEPR/${this.depreciationDate.replace(/-/g, '')}-${i + 1}`,
|
||
txnDate: this.depreciationDate,
|
||
amount: t.depreciation,
|
||
particulars: `Investment depreciation provision - ${t.holding.scriptName} (market ${t.marketPrice})`
|
||
}));
|
||
this.txns = [
|
||
...newTxns,
|
||
...this.txns
|
||
];
|
||
this.toast(
|
||
`Depreciation provisioned for ${targets.length} holding(s). Total ₹${this.depreciationTotal.toLocaleString('en-IN')}.`
|
||
);
|
||
}
|
||
|
||
// -------------------- Mutual Fund Transfer --------------------
|
||
get mfHoldings(): InvestmentHolding[] {
|
||
return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE');
|
||
}
|
||
|
||
get mfFromHolding(): InvestmentHolding | undefined {
|
||
return this.holdings.find((h) => h.certNo === this.mfFromCertNo);
|
||
}
|
||
|
||
get mfTransferValue(): number {
|
||
const h = this.mfFromHolding;
|
||
if (!h || !this.mfTransferUnits) {
|
||
return 0;
|
||
}
|
||
return Math.round(this.mfTransferUnits * h.purchaseRate);
|
||
}
|
||
|
||
saveMfTransfer(): void {
|
||
const h = this.mfFromHolding;
|
||
if (!h || !this.mfToProduct || !this.mfTransferUnits) {
|
||
this.toast('Select source holding, target product and units to transfer.');
|
||
return;
|
||
}
|
||
if (this.mfTransferUnits > h.units) {
|
||
this.toast(`Units cannot exceed units held (${h.units}).`);
|
||
return;
|
||
}
|
||
const value = this.mfTransferValue;
|
||
h.units = +(h.units - this.mfTransferUnits).toFixed(3);
|
||
h.faceValue = Math.round(h.units * h.purchaseRate);
|
||
if (h.units <= 0) {
|
||
h.status = 'SOLD';
|
||
}
|
||
const product = this.mfProductOptions.find((p) => p.productName === this.mfToProduct);
|
||
const newCert = `MF-${5000 + this.holdings.filter((x) => x.instrumentType === 'MUTUAL_FUND').length + 1}`;
|
||
this.holdings = [
|
||
{
|
||
certNo: newCert,
|
||
instrumentType: 'MUTUAL_FUND',
|
||
scriptCode: product?.productCode || this.mfToProduct,
|
||
scriptName: this.mfToProduct,
|
||
party: product?.amcName || '',
|
||
subType: product?.category || 'DEBT',
|
||
faceValue: value,
|
||
purchaseRate: h.purchaseRate,
|
||
units: this.mfTransferUnits,
|
||
purchaseDate: this.mfTransferDate,
|
||
maturityDate: addPeriod(this.mfTransferDate, 1, 0),
|
||
interestRate: h.interestRate,
|
||
interestFreq: 'CUMULATIVE',
|
||
glCode: '310050',
|
||
glName: 'INVESTMENT IN MUTUAL FUNDS',
|
||
crToAc: h.crToAc,
|
||
brokerCode: '',
|
||
status: 'ACTIVE'
|
||
},
|
||
...this.holdings
|
||
];
|
||
this.txns = [
|
||
{
|
||
certNo: newCert,
|
||
instrumentType: 'MUTUAL_FUND',
|
||
txnType: 'PURCHASE',
|
||
voucherNo: `INV/MF-TRF/${newCert}`,
|
||
txnDate: this.mfTransferDate,
|
||
amount: value,
|
||
particulars: `Mutual Fund switch from ${h.scriptName} to ${this.mfToProduct} (${this.mfTransferUnits} units)`
|
||
},
|
||
...this.txns
|
||
];
|
||
this.toast(`Transferred ${this.mfTransferUnits} units to ${this.mfToProduct} as ${newCert}.`);
|
||
this.mfFromCertNo = '';
|
||
this.mfToProduct = '';
|
||
this.mfTransferUnits = null;
|
||
}
|
||
|
||
// -------------------- Helpers --------------------
|
||
accrued(h: InvestmentHolding): number {
|
||
return accruedInterest(h, this.asOnDate);
|
||
}
|
||
|
||
maturity(h: InvestmentHolding): number {
|
||
return maturityAmount(h);
|
||
}
|
||
|
||
daysLeft(h: InvestmentHolding): number {
|
||
return daysToMaturity(h, this.asOnDate);
|
||
}
|
||
|
||
statusClass(h: InvestmentHolding): string {
|
||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||
}
|
||
|
||
// -------------------- Export to Excel (Holdings register) --------------------
|
||
async exportExcel(): Promise<void> {
|
||
this.generating = true;
|
||
try {
|
||
await exportWorkbook(
|
||
[
|
||
{
|
||
sheetName: this.instrumentLabel(this.bondType),
|
||
title: `${this.instrumentLabel(this.bondType)} Holdings Register`,
|
||
subtitle: `As on ${this.asOnDate}`,
|
||
headers: [
|
||
'Cert No',
|
||
'Script',
|
||
'Party',
|
||
'Units',
|
||
'Face Value',
|
||
'Rate',
|
||
'Purchase Date',
|
||
'Maturity Date',
|
||
'Accrued Interest',
|
||
'Maturity Value',
|
||
'Status'
|
||
],
|
||
widths: [
|
||
14,
|
||
26,
|
||
22,
|
||
12,
|
||
16,
|
||
10,
|
||
14,
|
||
14,
|
||
16,
|
||
16,
|
||
12
|
||
],
|
||
rows: this.holdingsForType.map((h) => [
|
||
h.certNo,
|
||
h.scriptName,
|
||
h.party,
|
||
h.units,
|
||
h.faceValue,
|
||
h.interestRate,
|
||
h.purchaseDate,
|
||
h.maturityDate,
|
||
this.accrued(h),
|
||
this.maturity(h),
|
||
h.status
|
||
])
|
||
}
|
||
],
|
||
`${this.instrumentLabel(this.bondType)}-Holdings-${this.asOnDate}`
|
||
);
|
||
this.toast(`${this.instrumentLabel(this.bondType)} Holdings exported.`);
|
||
} catch (err) {
|
||
console.error('Bond Holdings export failed', err);
|
||
this.toast('Could not generate the workbook — please retry.');
|
||
} finally {
|
||
this.generating = false;
|
||
}
|
||
}
|
||
|
||
private toast(msg: string): void {
|
||
this.toastMsg = msg;
|
||
this.toastVisible = true;
|
||
if (this.toastTimer) {
|
||
clearTimeout(this.toastTimer);
|
||
}
|
||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||
}
|
||
}
|