feat(investment): MTM valuation and mutual fund switch screens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1552,60 +1552,204 @@
|
||||
<!-- ===================== 09 INVESTMENT DEPRECIATION ===================== -->
|
||||
@if (activeTab === 'depreciation') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Investment Depreciation (Mark-to-Market Provisioning)</div>
|
||||
<div class="pos-section-label">Investment - Investment Depreciation (Market Valuation of AFS / HFT)</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy
|
||||
><span>* Valuation Date</span><mifosx-date-ddmmyyyy [(ngModel)]="depreciationDate"></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Category</span>
|
||||
<span>* Category</span>
|
||||
<select [(ngModel)]="depreciationCategory">
|
||||
<option value="">-- All Categories --</option>
|
||||
@for (c of categoryOptions; track c) {
|
||||
<option [value]="c">{{ c }}</option>
|
||||
}
|
||||
<option value="AFS">AFS (Available For Sale) — quarterly</option>
|
||||
<option value="HFT">HFT (Held For Trading) — monthly</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Holdings Requiring Provision</span><input type="text" [value]="depreciationTargets.length" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Depreciation</span
|
||||
><input type="text" [value]="'₹' + (depreciationTotal | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<p class="inv-hint">
|
||||
Each security still held is valued at the latest Daily Script Master rate / NAV on or before the date. Gains and
|
||||
losses are netted within Govt Securities, Corporate Bonds and Mutual Funds separately; a net loss is provided in
|
||||
the Investment Depreciation Reserve and a net gain is ignored. The book value does not change. HTM is not
|
||||
valued.
|
||||
</p>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="runDepreciation()">Process</button>
|
||||
<button type="button" class="pg-btn pg-btn--primary" [disabled]="valuationLoading" (click)="previewValuation()">
|
||||
{{ valuationLoading ? 'Valuing…' : 'Preview' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="!valuationPreview?.postable || valuationPosting"
|
||||
(click)="postValuation()"
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||
</div>
|
||||
@if (valuationError) {
|
||||
<p class="pg-sub" style="padding: 0 16px; color: #b42318">{{ valuationError }}</p>
|
||||
}
|
||||
@if (valuationPreview; as v) {
|
||||
@if (v.missingRates.length) {
|
||||
<p class="inv-hint inv-hint--bad">
|
||||
No rate on or before {{ v.valuationDate }} for: {{ v.missingRates.join(', ') }}. Enter it in Daily Script
|
||||
Master (09) first.
|
||||
</p>
|
||||
}
|
||||
@if (v.lastRunDate && !v.postable && !v.missingRates.length) {
|
||||
<p class="inv-hint inv-hint--bad">
|
||||
The last {{ v.category }} valuation is dated {{ v.lastRunDate }}; a new one must be dated after it.
|
||||
</p>
|
||||
}
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th class="num">Book Value</th>
|
||||
<th class="num">Market Value</th>
|
||||
<th class="num">Net Gain / Loss</th>
|
||||
<th class="num col-out">IDR Needed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (c of v.classes; track c.assetClass) {
|
||||
<tr>
|
||||
<td>{{ valuationClassLabels[c.assetClass] }}</td>
|
||||
<td class="num">₹{{ c.bookValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ c.marketValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ c.netDifference | number: '1.2-2' }}</td>
|
||||
<td class="num col-out">₹{{ c.required | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="pg-sub" style="padding: 0 16px">
|
||||
IDR needed ₹{{ v.requiredTotal | number: '1.2-2' }} · held from the last run ₹{{
|
||||
v.previousTotal | number: '1.2-2'
|
||||
}}
|
||||
·
|
||||
@if (v.delta > 0) {
|
||||
Voucher dated {{ v.valuationDate }}: Dr {{ v.provisionGlCode }} Provision for Depreciation / Cr
|
||||
{{ v.reserveGlCode }} IDR ₹{{ v.delta | number: '1.2-2' }}
|
||||
} @else if (v.delta < 0) {
|
||||
Write-back dated {{ v.valuationDate }}: Dr {{ v.reserveGlCode }} IDR / Cr {{ v.provisionGlCode }} Provision
|
||||
for Depreciation ₹{{ -v.delta | number: '1.2-2' }}
|
||||
} @else {
|
||||
No change in the IDR, so no voucher (the valuation is still recorded).
|
||||
}
|
||||
</p>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Deal No</th>
|
||||
<th>Script</th>
|
||||
<th>Class</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Book Value</th>
|
||||
<th class="num">Rate / NAV</th>
|
||||
<th>Rate Date</th>
|
||||
<th class="num">Market Value</th>
|
||||
<th class="num">Gain / Loss</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (l of v.lines; track l.purchaseId) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ l.dealNo }}</span>
|
||||
</td>
|
||||
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
|
||||
<td>{{ valuationClassLabels[l.assetClass] }}</td>
|
||||
<td class="num">{{ l.units | number: '1.0-6' }}</td>
|
||||
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
|
||||
<td>{{ l.rateDate }}</td>
|
||||
<td class="num">₹{{ l.marketValue | number: '1.2-2' }}</td>
|
||||
<td class="num" [class.col-out]="l.difference < 0">₹{{ l.difference | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="9" class="pg-empty">No {{ v.category }} holding with a rate on {{ v.valuationDate }}.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Valuation Runs</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th class="num">Book Rate</th>
|
||||
<th class="num">Market Price</th>
|
||||
<th class="num col-out">Depreciation</th>
|
||||
<th>Date</th>
|
||||
<th>Category</th>
|
||||
<th class="num">Govt Securities</th>
|
||||
<th class="num">Corporate Bonds</th>
|
||||
<th class="num">Mutual Funds</th>
|
||||
<th class="num">IDR Needed</th>
|
||||
<th class="num">Posted</th>
|
||||
<th>Voucher</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of depreciationTargets; track t.holding.certNo) {
|
||||
@for (r of valuationRuns; track r.id) {
|
||||
<tr>
|
||||
<td>{{ r.valuationDate }}</td>
|
||||
<td>{{ r.category }}</td>
|
||||
<td class="num">₹{{ r.classes[0].required | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.classes[1].required | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.classes[2].required | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.requiredTotal | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ r.delta | number: '1.2-2' }}</td>
|
||||
<td>{{ r.voucherNo || '—' }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.holding.certNo }}</span>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="openValuationRun(r.id)">Lines</button>
|
||||
</td>
|
||||
<td>{{ t.holding.scriptName }}</td>
|
||||
<td class="num">{{ t.holding.purchaseRate }}</td>
|
||||
<td class="num">{{ t.marketPrice }}</td>
|
||||
<td class="num col-out">₹{{ t.depreciation | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="pg-empty">No holdings require depreciation provisioning.</td>
|
||||
<td colspan="9" class="pg-empty">No valuation run yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@if (valuationOpened; as o) {
|
||||
<div class="pos-section-label">{{ o.category }} valuation of {{ o.valuationDate }}</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Deal No</th>
|
||||
<th>Script</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Book Value</th>
|
||||
<th class="num">Rate / NAV</th>
|
||||
<th>Rate Date</th>
|
||||
<th class="num">Market Value</th>
|
||||
<th class="num">Gain / Loss</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (l of o.lines; track l.purchaseId) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ l.dealNo }}</span>
|
||||
</td>
|
||||
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
|
||||
<td class="num">{{ l.units | number: '1.0-6' }}</td>
|
||||
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
|
||||
<td>{{ l.rateDate }}</td>
|
||||
<td class="num">₹{{ l.marketValue | number: '1.2-2' }}</td>
|
||||
<td class="num" [class.col-out]="l.difference < 0">₹{{ l.difference | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="8" class="pg-empty">No holding was valued (the run only wrote the reserve back).</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1615,40 +1759,87 @@
|
||||
<div class="pos-section-label">Investment - Mutual Fund Transfer (Switch between schemes)</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>From Holding *</span>
|
||||
<select [(ngModel)]="mfFromCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of mfHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
<span>* From Fund (Deal No)</span>
|
||||
<select [(ngModel)]="mfSwitchPurchaseId" (ngModelChange)="onMfSwitchDealChange()">
|
||||
<option [ngValue]="null">-- Select --</option>
|
||||
@for (h of mfHoldings; track h.id) {
|
||||
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (mfFromHolding; as h) {
|
||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
||||
<label class="inv-form-field"><span>NAV</span><input type="text" [value]="h.purchaseRate" readonly /></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Units Held</span><input type="text" [value]="h.units | number: '1.0-6'" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Book Value Held</span
|
||||
><input type="text" [value]="'₹' + (carryingValue(h) | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>* Switch Date</span
|
||||
><mifosx-date-ddmmyyyy [(ngModel)]="mfSwitchDate" (ngModelChange)="loadMfSwitchRates()"></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
<label class="inv-form-field"
|
||||
><span>* Units to Switch</span><input type="number" [(ngModel)]="mfSwitchUnits" min="0" step="0.001"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>To Product *</span>
|
||||
<select [(ngModel)]="mfToProduct">
|
||||
<span>* NAV of this fund</span>
|
||||
<input type="number" [(ngModel)]="mfSwitchFromNav" min="0" step="0.0001" />
|
||||
@if (mfSwitchFromRate; as r) {
|
||||
<small>Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}</small>
|
||||
}
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>* Switch into Scheme</span>
|
||||
<select [(ngModel)]="mfSwitchToScriptCode" (ngModelChange)="onMfSwitchTargetChange()">
|
||||
<option value="">-- Select --</option>
|
||||
@for (p of mfProductOptions; track p.productCode) {
|
||||
<option [value]="p.productName">{{ p.productName }} ({{ p.amcName }})</option>
|
||||
@for (x of mfSwitchTargets; track x.scriptCode) {
|
||||
<option [value]="x.scriptCode">{{ x.scriptCode }} — {{ x.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>* NAV of the new scheme</span>
|
||||
<input type="number" [(ngModel)]="mfSwitchToNav" min="0" step="0.0001" />
|
||||
@if (mfSwitchToRate; as r) {
|
||||
<small>Daily Script Master {{ r.rateDate }}: {{ r.closeRate | number: '1.2-4' }}</small>
|
||||
}
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>* Sub Type of the new scheme</span>
|
||||
<select [(ngModel)]="mfSwitchSubType">
|
||||
@for (s of mfSwitchSubTypes; track s.subTypeCode) {
|
||||
<option [value]="s.subTypeName">{{ s.subTypeName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Units to Transfer *</span><input type="number" [(ngModel)]="mfTransferUnits" min="0" step="0.001"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="mfTransferDate"></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Transfer Value (computed)</span
|
||||
><input type="text" [value]="'₹' + (mfTransferValue | number: '1.2-2')" readonly
|
||||
><span>Remarks</span><input type="text" maxlength="400" [(ngModel)]="mfSwitchRemarks"
|
||||
/></label>
|
||||
</div>
|
||||
@if (mfFromHolding && mfSwitchToScript && mfSwitchValue) {
|
||||
<div class="pos-section-label">Accounting (dated {{ mfSwitchDate }}, no bank entry)</div>
|
||||
<p class="pg-sub" style="padding: 0 16px">
|
||||
Switch value ₹{{ mfSwitchValue | number: '1.2-2' }} · cost of the units ₹{{
|
||||
mfSwitchCost | number: '1.2-2'
|
||||
}}
|
||||
· new units {{ mfSwitchNewUnits | number: '1.0-3' }} of {{ mfSwitchToScript.scriptCode }}
|
||||
</p>
|
||||
<p class="pg-sub" style="padding: 0 16px">
|
||||
Dr {{ mfSwitchToScript.glCode }} Investment (new scheme) ₹{{ mfSwitchValue | number: '1.2-2' }} · Cr
|
||||
{{ mfFromHolding.glCode }} Investment (this fund) ₹{{ mfSwitchCost | number: '1.2-2' }}
|
||||
@if (mfSwitchGain > 0) {
|
||||
· Cr 400402 Interest on Bonds ₹{{ mfSwitchGain | number: '1.2-2' }}
|
||||
} @else if (mfSwitchGain < 0) {
|
||||
· Dr 500605 Loss on Investments ₹{{ -mfSwitchGain | number: '1.2-2' }}
|
||||
}
|
||||
</p>
|
||||
}
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveMfTransfer()">Save</button>
|
||||
<button type="button" class="pg-btn pg-btn--primary" [disabled]="mfSwitchSaving" (click)="saveMfSwitch()">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
SUBTYPE_MASTER,
|
||||
BROKER_MASTER,
|
||||
SGL_BANK_MASTER,
|
||||
MUTUAL_FUND_PRODUCTS,
|
||||
LIEN_REASON_MASTER,
|
||||
CATEGORY_OPTIONS,
|
||||
DEAL_TYPE_OPTIONS,
|
||||
@@ -73,6 +72,7 @@ import {
|
||||
InvestmentSecurityTransferDto,
|
||||
InvestmentNonSlrLimitDto,
|
||||
InvestmentService,
|
||||
InvestmentValuationDto,
|
||||
NON_SLR_TYPES,
|
||||
apiErrorMessage,
|
||||
toHolding,
|
||||
@@ -175,7 +175,7 @@ interface AccountingPreviewLine {
|
||||
* 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.
|
||||
* (market valuation) and Mutual Fund Transfer (switch), all on the backend.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-bond-investment',
|
||||
@@ -278,7 +278,8 @@ export class BondInvestmentComponent implements OnInit {
|
||||
'REDEEM',
|
||||
'TRANSFER',
|
||||
'AMORTIZE',
|
||||
'WRITE_OFF'
|
||||
'WRITE_OFF',
|
||||
'SWITCH'
|
||||
].includes(t.txnType)
|
||||
);
|
||||
this.txns = [
|
||||
@@ -311,7 +312,6 @@ export class BondInvestmentComponent implements OnInit {
|
||||
}
|
||||
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;
|
||||
@@ -354,15 +354,9 @@ export class BondInvestmentComponent implements OnInit {
|
||||
|
||||
// GOI Category Transfer
|
||||
|
||||
// Investment Depreciation
|
||||
// Investment Depreciation (market valuation of AFS / HFT)
|
||||
depreciationDate = this.asOnDate;
|
||||
depreciationCategory = '';
|
||||
|
||||
// Mutual Fund Transfer
|
||||
mfFromCertNo = '';
|
||||
mfToProduct = '';
|
||||
mfTransferUnits: number | null = null;
|
||||
mfTransferDate = this.asOnDate;
|
||||
depreciationCategory: 'AFS' | 'HFT' = 'AFS';
|
||||
|
||||
readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [
|
||||
{ value: 'GOVT_SECURITY', label: 'GOVT SECURITY' },
|
||||
@@ -373,6 +367,9 @@ export class BondInvestmentComponent implements OnInit {
|
||||
|
||||
setTab(tab: BondTab): void {
|
||||
this.activeTab = tab;
|
||||
if (tab === 'depreciation') {
|
||||
this.loadValuationRuns();
|
||||
}
|
||||
}
|
||||
|
||||
setBondType(type: BondType): void {
|
||||
@@ -1737,123 +1734,245 @@ export class BondInvestmentComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- 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);
|
||||
// -------------------- Investment Depreciation (market valuation, MTM) --------------------
|
||||
/**
|
||||
* RBI: AFS holdings are valued at market quarterly and HFT monthly (or more often); HTM is carried at cost. The backend
|
||||
* values each security at the Daily Script Master rate, nets gains and losses within each class, and posts only the
|
||||
* change in the Investment Depreciation Reserve. The book value of the holdings does not change.
|
||||
*/
|
||||
valuationPreview: InvestmentValuationDto | null = null;
|
||||
valuationError = '';
|
||||
valuationLoading = false;
|
||||
valuationPosting = false;
|
||||
valuationRuns: InvestmentValuationDto[] = [];
|
||||
/** A past run opened from the history, with its lines. */
|
||||
valuationOpened: InvestmentValuationDto | null = null;
|
||||
|
||||
readonly valuationClassLabels: Record<string, string> = {
|
||||
GOVT_SECURITY: 'Govt Securities',
|
||||
CORPORATE_BOND: 'Corporate Bonds',
|
||||
MUTUAL_FUND: 'Mutual Funds'
|
||||
};
|
||||
|
||||
loadValuationRuns(): void {
|
||||
this.investmentService.getValuations().subscribe({
|
||||
next: (runs) => (this.valuationRuns = runs),
|
||||
error: (err) => this.toast(`Could not load valuation runs: ${apiErrorMessage(err)}`)
|
||||
});
|
||||
}
|
||||
|
||||
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.');
|
||||
previewValuation(): void {
|
||||
this.valuationPreview = null;
|
||||
this.valuationError = '';
|
||||
if (!this.depreciationDate || !this.depreciationCategory) {
|
||||
this.valuationError = 'Enter the valuation date and choose AFS or HFT.';
|
||||
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')}.`
|
||||
);
|
||||
this.valuationLoading = true;
|
||||
this.investmentService.previewValuation(this.depreciationDate, this.depreciationCategory).subscribe({
|
||||
next: (v) => {
|
||||
this.valuationLoading = false;
|
||||
this.valuationPreview = v;
|
||||
},
|
||||
error: (err) => {
|
||||
this.valuationLoading = false;
|
||||
this.valuationError = apiErrorMessage(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Mutual Fund Transfer --------------------
|
||||
/** Posts the change in IDR worked out by the preview; the backend values the holdings again with the same rules. */
|
||||
postValuation(): void {
|
||||
const v = this.valuationPreview;
|
||||
if (!v?.postable || this.valuationPosting) {
|
||||
return;
|
||||
}
|
||||
this.valuationPosting = true;
|
||||
this.investmentService
|
||||
.runValuation({ valuationDate: v.valuationDate, category: v.category, dateFormat: 'dd-MM-yyyy', locale: 'en' })
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.valuationPosting = false;
|
||||
this.toast(
|
||||
v.delta === 0
|
||||
? `Valuation of ${v.valuationDate} recorded; the IDR needed is unchanged, so no voucher.`
|
||||
: `Valuation of ${v.valuationDate} posted — ${res.resourceIdentifier ?? ''}.`
|
||||
);
|
||||
this.valuationPreview = null;
|
||||
this.loadValuationRuns();
|
||||
},
|
||||
error: (err) => {
|
||||
this.valuationPosting = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openValuationRun(id: number | undefined): void {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
this.investmentService.getValuation(id).subscribe({
|
||||
next: (v) => (this.valuationOpened = v),
|
||||
error: (err) => this.toast(apiErrorMessage(err))
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Mutual Fund Transfer (switch) --------------------
|
||||
/**
|
||||
* Units of one scheme are redeemed at its NAV and put into another scheme the same day; no money comes to the bank.
|
||||
* Voucher: Dr new scheme's investment GL (value) / Cr old scheme's GL (cost of the units); gain 400402, loss 500605.
|
||||
*/
|
||||
mfSwitchPurchaseId: number | null = null;
|
||||
mfSwitchUnits: number | null = null;
|
||||
mfSwitchFromNav: number | null = null;
|
||||
mfSwitchToScriptCode = '';
|
||||
mfSwitchToNav: number | null = null;
|
||||
mfSwitchSubType = 'DEBT - LIQUID';
|
||||
mfSwitchDate = this.asOnDate;
|
||||
mfSwitchRemarks = '';
|
||||
mfSwitchSaving = false;
|
||||
/** Latest Daily Script Master NAVs on the switch date, as hints. */
|
||||
mfSwitchFromRate: InvestmentDailyRateDto | null = null;
|
||||
mfSwitchToRate: InvestmentDailyRateDto | null = null;
|
||||
|
||||
get mfHoldings(): InvestmentHolding[] {
|
||||
return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE');
|
||||
return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE' && !!h.id);
|
||||
}
|
||||
|
||||
get mfFromHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.mfFromCertNo);
|
||||
return this.holdings.find((h) => h.id === this.mfSwitchPurchaseId);
|
||||
}
|
||||
|
||||
get mfTransferValue(): number {
|
||||
/** Other active mutual fund schemes saved in Script Master. */
|
||||
get mfSwitchTargets(): ScriptMaster[] {
|
||||
const from = this.mfFromHolding;
|
||||
return this.scriptOptions.filter(
|
||||
(x) => !!x.id && x.active !== false && x.instrumentType === 'MUTUAL_FUND' && x.scriptCode !== from?.scriptCode
|
||||
);
|
||||
}
|
||||
|
||||
get mfSwitchToScript(): ScriptMaster | undefined {
|
||||
return this.scriptOptions.find((x) => x.scriptCode === this.mfSwitchToScriptCode);
|
||||
}
|
||||
|
||||
get mfSwitchSubTypes(): typeof SUBTYPE_MASTER {
|
||||
return SUBTYPE_MASTER.filter((s) => s.instrumentType === 'MUTUAL_FUND');
|
||||
}
|
||||
|
||||
/** Units × NAV of the scheme left. */
|
||||
get mfSwitchValue(): number {
|
||||
return this.mfSwitchUnits && this.mfSwitchFromNav
|
||||
? Math.round(this.mfSwitchUnits * this.mfSwitchFromNav * 100) / 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
/** The units' share of the book value still held, as the backend works it out for a sale. */
|
||||
get mfSwitchCost(): number {
|
||||
const h = this.mfFromHolding;
|
||||
if (!h || !this.mfTransferUnits) {
|
||||
if (!h || !this.mfSwitchUnits || !h.units) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(this.mfTransferUnits * h.purchaseRate);
|
||||
const book = this.carryingValue(h);
|
||||
return this.mfSwitchUnits >= h.units ? book : Math.round(((book * this.mfSwitchUnits) / h.units) * 100) / 100;
|
||||
}
|
||||
|
||||
saveMfTransfer(): void {
|
||||
get mfSwitchGain(): number {
|
||||
return Math.round((this.mfSwitchValue - this.mfSwitchCost) * 100) / 100;
|
||||
}
|
||||
|
||||
get mfSwitchNewUnits(): number {
|
||||
return this.mfSwitchToNav ? Math.round((this.mfSwitchValue / this.mfSwitchToNav) * 1000) / 1000 : 0;
|
||||
}
|
||||
|
||||
onMfSwitchDealChange(): void {
|
||||
this.mfSwitchUnits = this.mfFromHolding?.units ?? null;
|
||||
this.mfSwitchFromNav = null;
|
||||
this.loadMfSwitchRates();
|
||||
}
|
||||
|
||||
/** Fills the NAVs from Daily Script Master for the switch date when one has been entered. */
|
||||
loadMfSwitchRates(): void {
|
||||
const fromId = this.scriptOptions.find((x) => x.scriptCode === this.mfFromHolding?.scriptCode)?.id;
|
||||
const toId = this.mfSwitchToScript?.id;
|
||||
this.mfSwitchFromRate = null;
|
||||
this.mfSwitchToRate = null;
|
||||
if (fromId && this.mfSwitchDate) {
|
||||
this.investmentService.getLatestDailyRate(fromId, this.mfSwitchDate).subscribe({
|
||||
next: (r) => {
|
||||
this.mfSwitchFromRate = r;
|
||||
this.mfSwitchFromNav = this.mfSwitchFromNav ?? r?.closeRate ?? null;
|
||||
},
|
||||
error: () => (this.mfSwitchFromRate = null)
|
||||
});
|
||||
}
|
||||
if (toId && this.mfSwitchDate) {
|
||||
this.investmentService.getLatestDailyRate(toId, this.mfSwitchDate).subscribe({
|
||||
next: (r) => {
|
||||
this.mfSwitchToRate = r;
|
||||
this.mfSwitchToNav = this.mfSwitchToNav ?? r?.closeRate ?? null;
|
||||
},
|
||||
error: () => (this.mfSwitchToRate = null)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMfSwitchTargetChange(): void {
|
||||
this.mfSwitchToNav = null;
|
||||
this.loadMfSwitchRates();
|
||||
}
|
||||
|
||||
saveMfSwitch(): void {
|
||||
const h = this.mfFromHolding;
|
||||
if (!h || !this.mfToProduct || !this.mfTransferUnits) {
|
||||
this.toast('Select source holding, target product and units to transfer.');
|
||||
const to = this.mfSwitchToScript;
|
||||
if (
|
||||
!h?.id ||
|
||||
!to?.id ||
|
||||
!this.mfSwitchUnits ||
|
||||
!this.mfSwitchFromNav ||
|
||||
!this.mfSwitchToNav ||
|
||||
!this.mfSwitchDate
|
||||
) {
|
||||
this.toast('Select the fund, the units, both NAVs, the scheme to switch into and the date.');
|
||||
return;
|
||||
}
|
||||
if (this.mfTransferUnits > h.units) {
|
||||
this.toast(`Units cannot exceed units held (${h.units}).`);
|
||||
if (this.mfSwitchSaving) {
|
||||
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;
|
||||
this.mfSwitchSaving = true;
|
||||
this.investmentService
|
||||
.switchFund(h.id, {
|
||||
switchDate: this.mfSwitchDate,
|
||||
units: this.mfSwitchUnits,
|
||||
fromNav: this.mfSwitchFromNav,
|
||||
toScriptId: to.id,
|
||||
toNav: this.mfSwitchToNav,
|
||||
subType: this.mfSwitchSubType,
|
||||
...(this.mfSwitchRemarks ? { remarks: this.mfSwitchRemarks } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.mfSwitchSaving = false;
|
||||
this.toast(
|
||||
`Switched ${this.mfSwitchUnits} units of deal ${h.dealNo} into ${to.scriptCode} — new deal ${res.resourceIdentifier ?? ''}.`
|
||||
);
|
||||
this.mfSwitchPurchaseId = null;
|
||||
this.mfSwitchUnits = null;
|
||||
this.mfSwitchFromNav = null;
|
||||
this.mfSwitchToScriptCode = '';
|
||||
this.mfSwitchToNav = null;
|
||||
this.mfSwitchRemarks = '';
|
||||
this.mfSwitchFromRate = null;
|
||||
this.mfSwitchToRate = null;
|
||||
this.loadPurchases();
|
||||
},
|
||||
error: (err) => {
|
||||
this.mfSwitchSaving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Helpers --------------------
|
||||
|
||||
@@ -29,6 +29,7 @@ export type TxnType =
|
||||
| 'WRITE_OFF'
|
||||
| 'AMORTIZE'
|
||||
| 'TRANSFER'
|
||||
| 'SWITCH'
|
||||
| 'PART_PAYMENT'
|
||||
| 'PREMATURE'
|
||||
| 'RENEWAL';
|
||||
|
||||
@@ -481,6 +481,36 @@ export class InvestmentService {
|
||||
return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/redeem`, body);
|
||||
}
|
||||
|
||||
/** Mutual fund switch: units leave this fund at its NAV and a new deal opens in the other scheme; the new deal no comes back. */
|
||||
switchFund(purchaseId: number, body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
|
||||
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>(
|
||||
`/investment/purchases/${purchaseId}/switch`,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- Market valuation (MTM) --------------------
|
||||
/** What a valuation of AFS / HFT holdings on a date would find and post, without saving. */
|
||||
previewValuation(valuationDate: string, category: string): Observable<InvestmentValuationDto> {
|
||||
return this.http.get<InvestmentValuationDto>('/investment/valuations/preview', {
|
||||
params: { valuationDate, category }
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs the valuation and posts the change in the Investment Depreciation Reserve. */
|
||||
runValuation(body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
|
||||
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/valuations', body);
|
||||
}
|
||||
|
||||
/** Valuation runs, newest first (without their lines). */
|
||||
getValuations(): Observable<InvestmentValuationDto[]> {
|
||||
return this.http.get<InvestmentValuationDto[]>('/investment/valuations');
|
||||
}
|
||||
|
||||
getValuation(id: number): Observable<InvestmentValuationDto> {
|
||||
return this.http.get<InvestmentValuationDto>(`/investment/valuations/${id}`);
|
||||
}
|
||||
|
||||
/** Interest lines already posted (coupons and provisions), newest first. */
|
||||
getInterestPostings(): Observable<InvestmentInterestPostingDto[]> {
|
||||
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
|
||||
@@ -826,6 +856,20 @@ export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
|
||||
glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
||||
};
|
||||
}
|
||||
if (x.saleType === 'SWITCH') {
|
||||
const result = x.profitLoss >= 0 ? `profit ₹${x.profitLoss}` : `loss ₹${-x.profitLoss}`;
|
||||
return {
|
||||
certNo: x.certNo,
|
||||
instrumentType: x.bondType as InstrumentType,
|
||||
txnType: 'SWITCH',
|
||||
voucherNo: x.voucherNo ?? `SWT/${x.dealNo}/${x.id}`,
|
||||
txnDate: x.settlementDate,
|
||||
amount: x.consideration,
|
||||
particulars: `Switched ${x.units} units @ NAV ${x.saleRate} - Deal ${x.dealNo} - ${x.scriptDesc} (${result}) - ${x.remarks ?? ''}`,
|
||||
glCode: x.investmentGlCode,
|
||||
glName: `Switched to another scheme, no bank entry (txn ${x.glTransactionId})`
|
||||
};
|
||||
}
|
||||
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 =
|
||||
@@ -1040,7 +1084,7 @@ export interface InvestmentSaleDto {
|
||||
bondType: string;
|
||||
scriptCode: string;
|
||||
scriptDesc: string;
|
||||
saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE';
|
||||
saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE' | 'SWITCH';
|
||||
partyId?: number;
|
||||
partyName?: string;
|
||||
dealDate: string;
|
||||
@@ -1075,6 +1119,47 @@ export interface InvestmentSaleDto {
|
||||
settledOnDate?: string;
|
||||
}
|
||||
|
||||
/** A market valuation (MTM) of AFS or HFT holdings: a preview (no id) or a posted run. */
|
||||
export interface InvestmentValuationDto {
|
||||
id?: number;
|
||||
valuationDate: string;
|
||||
category: string;
|
||||
requiredTotal: number;
|
||||
/** IDR held from the category's previous run. */
|
||||
previousTotal: number;
|
||||
/** More provision when positive, a write-back when negative. */
|
||||
delta: number;
|
||||
provisionGlCode: string;
|
||||
reserveGlCode: string;
|
||||
glTransactionId?: string;
|
||||
voucherNo?: string;
|
||||
lastRunDate?: string;
|
||||
missingRates: string[];
|
||||
postable: boolean;
|
||||
classes: Array<{
|
||||
assetClass: 'GOVT_SECURITY' | 'CORPORATE_BOND' | 'MUTUAL_FUND';
|
||||
bookValue: number;
|
||||
marketValue: number;
|
||||
netDifference: number;
|
||||
required: number;
|
||||
}>;
|
||||
lines: Array<{
|
||||
purchaseId: number;
|
||||
dealNo: string;
|
||||
certNo: string;
|
||||
bondType: string;
|
||||
scriptCode: string;
|
||||
scriptDesc: string;
|
||||
assetClass: string;
|
||||
units: number;
|
||||
bookValue: number;
|
||||
rate: number;
|
||||
rateDate: string;
|
||||
marketValue: number;
|
||||
difference: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** A GOI Category Transfer: a holding moved to another security of the same terms. */
|
||||
export interface InvestmentSecurityTransferDto {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user