feat(investment): sell, redemption, write-off, GOI transfer and amortize screens
This commit is contained in:
@@ -136,6 +136,7 @@
|
|||||||
<th>Party</th>
|
<th>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">Book Value</th>
|
||||||
<th class="num">Rate</th>
|
<th class="num">Rate</th>
|
||||||
<th>Settlement Date</th>
|
<th>Settlement Date</th>
|
||||||
<th>Maturity Date</th>
|
<th>Maturity Date</th>
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
<td>{{ h.party }}</td>
|
<td>{{ 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">₹{{ 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.maturityDate }}</td>
|
||||||
@@ -177,7 +179,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="12" class="pg-empty">No {{ instrumentLabel(bondType) }} holdings yet.</td>
|
<td colspan="13" class="pg-empty">No {{ instrumentLabel(bondType) }} holdings yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -650,40 +652,79 @@
|
|||||||
<!-- ===================== 02 REDEMPTION ENTRY ===================== -->
|
<!-- ===================== 02 REDEMPTION ENTRY ===================== -->
|
||||||
@if (activeTab === 'redemption') {
|
@if (activeTab === 'redemption') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Redemption Entry Gov/Bond</div>
|
<div class="pos-section-label">Investment - Redemption Entry Gov/Bond (repayment at maturity)</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No / Deal No *</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="redeemCertNo">
|
<select [(ngModel)]="redeemPurchaseId" (ngModelChange)="onRedeemDealChange()">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldingsForType; track h.certNo) {
|
@for (h of saleableHoldings; track h.certNo) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptName }} (matures {{ h.maturityDate }})</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (redeemHolding; as h) {
|
@if (redeemQuote; as q) {
|
||||||
<label class="inv-form-field"><span>Script</span><input type="text" [value]="h.scriptName" readonly /></label>
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
><span>Script</span><input type="text" [value]="q.scriptCode + ' — ' + q.scriptDesc" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Maturity Date</span><input type="text" [value]="h.maturityDate" readonly
|
><span>Maturity / Redemption Date</span><input type="text" [value]="q.settlementDate" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Days to Maturity</span><input type="text" [value]="daysLeft(h)" readonly
|
><span>Units</span><input type="text" [value]="q.units | number: '1.0-6'" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Face Value Received</span
|
||||||
|
><input type="text" [value]="'₹' + (q.netAmount | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Book Value</span><input type="text" [value]="'₹' + (q.bookValueSold | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>{{
|
||||||
|
q.profitLoss >= 0 ? 'Discount realised (profit 400405)' : 'Premium written off (expense 500604)'
|
||||||
|
}}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[value]="'₹' + ((q.profitLoss < 0 ? -q.profitLoss : q.profitLoss) | number: '1.2-2')"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Amount received into (Bank)</span>
|
||||||
|
<select [(ngModel)]="redeemBankGlCode">
|
||||||
|
@for (g of settlementBankOptions; track g.glCode) {
|
||||||
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Remarks</span><input type="text" [(ngModel)]="redeemRemarks" maxlength="500"
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Redemption Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="redeemDate"></mifosx-date-ddmmyyyy
|
|
||||||
></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Redemption Proceeds (computed)</span
|
|
||||||
><input type="text" [value]="'₹' + (redeemAmount | number: '1.2-2')" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"><span>Remarks</span><input type="text" [(ngModel)]="redeemRemarks" /></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (redeemError) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px; color: #b42318">{{ redeemError }}</p>
|
||||||
|
}
|
||||||
|
@if (redeemQuote; as q) {
|
||||||
|
<div class="pos-section-label">Accounting (dated {{ q.settlementDate }})</div>
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Dr {{ redeemBankGlCode }} Bank ₹{{ q.netAmount | number: '1.2-2' }} · Cr {{ q.investmentGlCode }} Investment
|
||||||
|
₹{{ q.bookValueSold - (splitPremium(q) ? q.premiumCleared! : 0) | number: '1.2-2' }}
|
||||||
|
@if (splitPremium(q)) {
|
||||||
|
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
||||||
|
}
|
||||||
|
@if (q.profitLoss > 0) {
|
||||||
|
· Cr 400405 Profit on Redemption ₹{{ q.profitLoss | number: '1.2-2' }}
|
||||||
|
} @else if (q.profitLoss < 0) {
|
||||||
|
· Dr 500604 Premium Amortisation ₹{{ -q.profitLoss | number: '1.2-2' }}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveRedemption()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="!redeemQuote || redeeming" (click)="redeem()">
|
||||||
|
Redeem
|
||||||
|
</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -920,175 +961,240 @@
|
|||||||
<!-- ===================== 04 SELL ENTRY ===================== -->
|
<!-- ===================== 04 SELL ENTRY ===================== -->
|
||||||
@if (activeTab === 'sell') {
|
@if (activeTab === 'sell') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Govt.Security/Bond Sell Entry</div>
|
<div class="pos-section-label">Investment - Sell Entry Gov/Bond (all or part of the units, before maturity)</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Mode</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="sellForm.mode">
|
<select [(ngModel)]="sale.purchaseId" (ngModelChange)="onSaleDealChange()">
|
||||||
<option value="SELL">SELL</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
<option value="REDEEM">REDEEM</option>
|
@for (h of saleableHoldings; track h.certNo) {
|
||||||
<option value="MUTUAL_FUND_SELL">MUTUAL FUND SELL</option>
|
<option [ngValue]="h.id">
|
||||||
</select>
|
{{ h.dealNo }} — {{ h.scriptName }} ({{ h.units | number: '1.0-6' }} units)
|
||||||
</label>
|
</option>
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>Purchase Search By</span>
|
|
||||||
<select [(ngModel)]="sellForm.searchBy">
|
|
||||||
<option value="DEAL_NO">DEAL NO</option>
|
|
||||||
<option value="ISIN_NO">ISIN NO</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Search Value</span><input type="text" [(ngModel)]="sellForm.searchValue"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span> </span>
|
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchPurchaseForSell()">Load Purchase</button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="inv-form-grid">
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>* Bond Type</span>
|
|
||||||
<select [(ngModel)]="sellForm.bondType">
|
|
||||||
@for (o of bondTypeOptions; track o.value) {
|
|
||||||
<option [value]="o.value">{{ o.label }}</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>* Script Code</span><input type="text" [(ngModel)]="sellForm.scriptCode"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"><span>* Cert No</span><input type="text" [(ngModel)]="sellForm.certNo" /></label>
|
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Buyer Code</span>
|
<span>* Buyer (Party)</span>
|
||||||
<select [(ngModel)]="sellForm.buyerCode">
|
<select [(ngModel)]="sale.partyCode">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
@for (p of partyOptions; track p.partyCode) {
|
@for (p of activePartyOptions; track p.partyCode) {
|
||||||
<option [value]="p.partyName">{{ p.partyName }}</option>
|
<option [value]="p.partyCode">{{ p.partyCode }} — {{ p.partyName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"><span>Sub Type</span><input type="text" [(ngModel)]="sellForm.subType" /></label>
|
|
||||||
<label class="inv-form-field"><span>* ISIN</span><input type="text" [(ngModel)]="sellForm.isin" /></label>
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="sellForm.tbillGlAc"
|
><span>* Deal Date</span
|
||||||
/></label>
|
><mifosx-date-ddmmyyyy [(ngModel)]="sale.dealDate" (ngModelChange)="refreshSaleQuote()"></mifosx-date-ddmmyyyy
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.dealDate"></mifosx-date-ddmmyyyy
|
|
||||||
></label>
|
></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>* Deal Time</span><input type="text" [(ngModel)]="sellForm.dealTime" placeholder="HH:mm:ss"
|
><span>Deal Time</span><input type="time" step="1" [(ngModel)]="sale.dealTime"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Settlement Type</span>
|
<span>* Settlement Type</span>
|
||||||
<select [(ngModel)]="sellForm.settlementType">
|
<select [(ngModel)]="sale.settlementType" (ngModelChange)="refreshSaleQuote()">
|
||||||
<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>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Settlement Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.settlementDt"></mifosx-date-ddmmyyyy
|
><span>* Units to Sell</span
|
||||||
></label>
|
><input type="number" min="0" [(ngModel)]="sale.units" (change)="refreshSaleQuote()"
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>* Face Value</span><input type="number" [(ngModel)]="sellForm.faceValue" min="0"
|
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Sell Rate *</span><input type="number" [(ngModel)]="sellForm.sellRate" min="0" step="0.0001"
|
><span>* Sale Rate (per ₹100)</span
|
||||||
|
><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"
|
||||||
><span>* Unit Val</span><input type="number" [(ngModel)]="sellForm.unitVal" min="0"
|
><span>Brokerage</span
|
||||||
/></label>
|
><input type="number" min="0" [(ngModel)]="sale.brokerage" (change)="refreshSaleQuote()"
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>* Sell Unit</span><input type="number" [(ngModel)]="sellForm.sellUnit" min="0"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>TDS Amt.</span><input type="number" [(ngModel)]="sellForm.tdsAmt" min="0"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Brokerage</span><input type="number" [(ngModel)]="sellForm.brokerage" min="0"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>* DB. To A/c</span><input type="text" [(ngModel)]="sellForm.dbToAc"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Amortize Amt.</span><input type="number" [(ngModel)]="sellForm.amortizeAmt"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Sale Diff (computed)</span><input type="text" [value]="sellSaleDiff | number: '1.2-2'" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Premium Loss (computed)</span><input type="text" [value]="sellPremiumLoss | number: '1.2-2'" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Accrued Int. (computed)</span
|
|
||||||
><input type="text" [value]="sellAccruedInterest | number: '1.2-2'" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Days (computed)</span><input type="text" [value]="sellAccruedDays" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Consideration Amt. (computed)</span
|
|
||||||
><input type="text" [value]="sellConsideration | number: '1.2-2'" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Pay Amt (computed)</span><input type="text" [value]="sellPayAmount | number: '1.2-2'" readonly
|
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Recomm. Authority</span>
|
<span>* Amount received into (Bank)</span>
|
||||||
<select [(ngModel)]="sellForm.recommAuthority">
|
<select [(ngModel)]="sale.bankGlCode" (ngModelChange)="refreshSaleQuote()">
|
||||||
<option value="">-- Select --</option>
|
@for (g of settlementBankOptions; track g.glCode) {
|
||||||
@for (a of authorityOptions; track a) {
|
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
|
||||||
<option [value]="a">{{ a }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>Sanction Authority</span>
|
|
||||||
<select [(ngModel)]="sellForm.sanctionAuthority">
|
|
||||||
<option value="">-- Select --</option>
|
|
||||||
@for (a of authorityOptions; track a) {
|
|
||||||
<option [value]="a">{{ a }}</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>CALL MONEY</span
|
><span>{{ saleIsHtm ? '* ' : '' }}Sanction Authority</span
|
||||||
><input type="checkbox" [(ngModel)]="sellForm.callMoney" style="height: 18px; width: 18px"
|
><input
|
||||||
|
type="text"
|
||||||
|
maxlength="100"
|
||||||
|
[(ngModel)]="sale.sanctionAuthority"
|
||||||
|
placeholder="e.g. Board resolution no."
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ saleIsHtm ? '* Reason for sale' : 'Remarks' }}</span
|
||||||
|
><input type="text" maxlength="500" [(ngModel)]="sale.remarks"
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
|
@if (saleIsHtm) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
HTM securities are not sold except under liquidity stress, with the Board's permission and the reason
|
||||||
|
recorded.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
@if (saleQuoteError) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px; color: #b42318">{{ saleQuoteError }}</p>
|
||||||
|
}
|
||||||
|
@if (saleQuote; as q) {
|
||||||
|
<div class="pos-section-label">Sale Calculation — settlement date {{ q.settlementDate }}</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Face Value Sold</span><input type="text" [value]="'₹' + (q.faceValue | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Sale Price</span><input type="text" [value]="'₹' + (q.consideration | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Cost (Book Value)</span
|
||||||
|
><input type="text" [value]="'₹' + (q.bookValueSold | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>{{ q.profitLoss >= 0 ? 'Profit on Sale' : 'Loss on Sale' }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[value]="'₹' + ((q.profitLoss < 0 ? -q.profitLoss : q.profitLoss) | number: '1.2-2')"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Net Amount to Bank</span><input type="text" [value]="'₹' + (q.netAmount | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
On Settle: Dr Bank ₹{{ q.netAmount | number: '1.2-2' }}
|
||||||
|
@if (q.brokerage) {
|
||||||
|
· Dr 500602 Brokerage ₹{{ q.brokerage | number: '1.2-2' }}
|
||||||
|
}
|
||||||
|
· Cr {{ q.investmentGlCode }} Investment ₹{{
|
||||||
|
q.bookValueSold - (splitPremium(q) ? q.premiumCleared! : 0) | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
@if (splitPremium(q)) {
|
||||||
|
· Cr {{ q.premiumGlCode }} Premium ₹{{ q.premiumCleared | number: '1.2-2' }}
|
||||||
|
}
|
||||||
|
@if (q.profitLoss > 0) {
|
||||||
|
· Cr 400405 Profit ₹{{ q.profitLoss | number: '1.2-2' }}
|
||||||
|
} @else if (q.profitLoss < 0) {
|
||||||
|
· Dr 500605 Loss ₹{{ -q.profitLoss | number: '1.2-2' }}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
@if (q.accruedInterest) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
<strong>Interest — manual entry:</strong> the buyer also pays interest for {{ q.accruedDays }} days since
|
||||||
|
the last coupon, ₹{{ q.accruedInterest | number: '1.2-2' }}. It is not in this voucher; when it reaches the
|
||||||
|
bank, pass a journal entry: Dr Bank ₹{{ q.accruedInterest | number: '1.2-2' }}
|
||||||
|
@if (q.receivableCleared) {
|
||||||
|
· Cr 100603 Int. Receivable ₹{{ q.receivableCleared | number: '1.2-2' }} (already provisioned)
|
||||||
|
}
|
||||||
|
· Cr Int. Income ₹{{ q.interestIncome | number: '1.2-2' }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="savingSale" (click)="saveSale()">Save</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchPurchaseForSell()">Retrieve</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="resetSale()">Cancel</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelSell()">Cancel</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="exitSell()">Exit</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pos-section-label">Pending Sales — settle on or after the settlement date</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Script</th>
|
||||||
|
<th>Buyer</th>
|
||||||
|
<th class="num">Units</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
|
<th>Settlement Date</th>
|
||||||
|
<th class="num">Net Amount</th>
|
||||||
|
<th class="num">Profit / Loss</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (s of pendingSalesForType; track s.id) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ s.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.scriptCode }} — {{ s.scriptDesc }}</td>
|
||||||
|
<td>{{ s.partyName }}</td>
|
||||||
|
<td class="num">{{ s.units | number: '1.0-6' }}</td>
|
||||||
|
<td class="num">{{ s.saleRate }}</td>
|
||||||
|
<td>{{ s.settlementDate }}</td>
|
||||||
|
<td class="num">₹{{ s.netAmount | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ s.profitLoss | number: '1.2-2' }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" (click)="settleSale(s)">Settle</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropSale(s)">Drop</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="pg-empty">No pending sales.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- ===================== 05 WRITE OFF ENTRY ===================== -->
|
<!-- ===================== 05 WRITE OFF ENTRY ===================== -->
|
||||||
@if (activeTab === 'writeoff') {
|
@if (activeTab === 'writeoff') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Write Off Entry Gov/Bond</div>
|
<div class="pos-section-label">
|
||||||
|
Investment - Write Off Entry Gov/Bond (book the loss when the value has fallen)
|
||||||
|
</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="writeoffCertNo">
|
<select [(ngModel)]="writeoffPurchaseId">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldingsForType; track h.certNo) {
|
@for (h of saleableHoldings; track h.certNo) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (writeoffHolding; as h) {
|
@if (writeoffHolding; as h) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Face Value to be Written Off</span
|
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Book Value (now)</span
|
||||||
|
><input type="text" [value]="'₹' + (carryingValue(h) | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Loss to Write Off (₹)</span><input type="number" min="0" [(ngModel)]="writeoffAmount"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Book Value after Write-off</span
|
||||||
|
><input type="text" [value]="'₹' + (carryingValue(h) - (writeoffAmount || 0) | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Reason</span><input type="text" maxlength="500" [(ngModel)]="writeoffReason"
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (writeoffHolding && writeoffAmount) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Voucher: Dr 500605 Loss on Investments ₹{{ writeoffAmount | number: '1.2-2' }} / Cr
|
||||||
|
{{ writeoffHolding!.glCode }} Investment ₹{{ writeoffAmount | number: '1.2-2' }}. The loss is an expense of
|
||||||
|
the year; the units stay held and later sales or the redemption work from the lower book value.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveWriteoff()">Save</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="!writeoffHolding || !writeoffAmount || writingOff"
|
||||||
|
(click)="saveWriteoff()"
|
||||||
|
>
|
||||||
|
Write Off
|
||||||
|
</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1097,33 +1203,104 @@
|
|||||||
<!-- ===================== 06 AMORTIZE ENTRY ===================== -->
|
<!-- ===================== 06 AMORTIZE ENTRY ===================== -->
|
||||||
@if (activeTab === 'amortize') {
|
@if (activeTab === 'amortize') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Amortize Entry (Premium / Discount)</div>
|
<div class="pos-section-label">
|
||||||
|
Investment - Amortize Entry (premium of HTM securities written off evenly to maturity, every 31 March)
|
||||||
|
</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="amortizeCertNo">
|
<select [(ngModel)]="amortizePurchaseId" (ngModelChange)="onAmortizeDealChange()">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldingsForType; track h.certNo) {
|
@for (h of premiumHoldings; track h.certNo) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="h.id">
|
||||||
|
{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }} @ {{ h.purchaseRate }}
|
||||||
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (amortizeHolding; as h) {
|
@if (amortSchedule; as d) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Purchase Rate</span><input type="text" [value]="h.purchaseRate" readonly
|
><span>Face Value</span><input type="text" [value]="'₹' + (d.faceValue | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Suggested Yearly Amortization</span
|
><span>Premium Paid</span><input type="text" [value]="'₹' + (d.premium | number: '1.2-2')" readonly
|
||||||
><input type="text" [value]="'₹' + (suggestedAmortizeAmt | number: '1.2-2')" readonly
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Premium Still to Amortise</span
|
||||||
|
><input type="text" [value]="'₹' + (d.unamortisedPremium | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Book Value (now)</span><input type="text" [value]="'₹' + (d.bookValue | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Settlement → Maturity</span
|
||||||
|
><input type="text" [value]="d.settlementDate + ' → ' + d.maturityDate" readonly
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Amortize Amount</span
|
|
||||||
><input type="number" [(ngModel)]="amortizeAmt" min="0" [placeholder]="suggestedAmortizeAmt"
|
|
||||||
/></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (!premiumHoldings.length) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
No active HTM holding was bought above face value, so there is no premium to amortise.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
@if (amortSchedule; as d) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Each 31 March: premium still to amortise × days in the period ÷ days left to maturity. Voucher: Dr 500604
|
||||||
|
Amortisation of Premium / Cr {{ d.premiumGlCode }}. The last part is written off with the redemption.
|
||||||
|
</p>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Year End</th>
|
||||||
|
<th>Period</th>
|
||||||
|
<th class="num">Days</th>
|
||||||
|
<th class="num col-out">Amortisation</th>
|
||||||
|
<th class="num">Premium Left</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of d.rows; track r.fyEnd) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.kind === 'MATURITY' ? 'Maturity ' + r.fyEnd : r.fyEnd }}</td>
|
||||||
|
<td>{{ r.fromDate }} → {{ r.toDate }}</td>
|
||||||
|
<td class="num">{{ r.days }}</td>
|
||||||
|
<td class="num col-out">₹{{ r.amount | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">
|
||||||
|
{{
|
||||||
|
r.premiumAfter === null || r.premiumAfter === undefined
|
||||||
|
? ''
|
||||||
|
: '₹' + (r.premiumAfter | number: '1.2-2')
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.posted) {
|
||||||
|
<span class="pg-chip">Posted {{ r.voucherNo }}</span>
|
||||||
|
} @else if (r.kind === 'MATURITY') {
|
||||||
|
With redemption
|
||||||
|
} @else {
|
||||||
|
Projected
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.postable) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="amortizing"
|
||||||
|
(click)="amortizeRow(r.fyEnd)"
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveAmortize()">Save</button>
|
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1132,49 +1309,65 @@
|
|||||||
<!-- ===================== 07 AMORTIZE YEARLY PROCESS ===================== -->
|
<!-- ===================== 07 AMORTIZE YEARLY PROCESS ===================== -->
|
||||||
@if (activeTab === 'amortizeYearly') {
|
@if (activeTab === 'amortizeYearly') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Amortize Yearly Process</div>
|
<div class="pos-section-label">
|
||||||
|
Investment - Amortize Yearly Process (all HTM premium holdings for a 31 March)
|
||||||
|
</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Financial Year</span><input type="text" [(ngModel)]="amortizeYear"
|
><span>* Year End (31-03-YYYY)</span><input type="text" maxlength="10" [(ngModel)]="amortizeFyEnd"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Holdings to Process</span><input type="text" [value]="amortizeYearlyTargets.length" readonly
|
><span>Holdings Due</span><input type="text" [value]="amortizeDue.length" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Total Amortization</span
|
><span>Total Amortisation</span
|
||||||
><input type="text" [value]="'₹' + (amortizeYearlyTotal | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (amortizeDueTotal | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="runAmortizeYearly()">Process</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="loadAmortizeDue()">Show Due</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="!amortizeDue.length || amortizing"
|
||||||
|
(click)="runAmortizeYearly()"
|
||||||
|
>
|
||||||
|
Process
|
||||||
|
</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Deal No</th>
|
||||||
<th>Script</th>
|
<th>Script</th>
|
||||||
<th class="num">Face Value</th>
|
<th>Period</th>
|
||||||
<th class="num">Purchase Rate</th>
|
<th class="num">Days</th>
|
||||||
<th>Maturity Date</th>
|
<th class="num">Premium Left</th>
|
||||||
<th class="num col-out">Yearly Amortization</th>
|
<th class="num col-out">Amortisation</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (t of amortizeYearlyTargets; track t.holding.certNo) {
|
@for (d of amortizeDue; track d.purchaseId) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ t.holding.certNo }}</span>
|
<span class="pg-chip">{{ d.dealNo }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.holding.scriptName }}</td>
|
<td>{{ d.scriptCode }} {{ d.scriptDesc }}</td>
|
||||||
<td class="num">₹{{ t.holding.faceValue | number: '1.2-2' }}</td>
|
<td>{{ d.fromDate }} → {{ d.fyEnd }}</td>
|
||||||
<td class="num">{{ t.holding.purchaseRate }}</td>
|
<td class="num">{{ d.days }}</td>
|
||||||
<td>{{ t.holding.maturityDate }}</td>
|
<td class="num">₹{{ d.unamortisedPremium | number: '1.2-2' }}</td>
|
||||||
<td class="num col-out">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num col-out">₹{{ d.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="pg-empty">No premium-carrying holdings require amortization.</td>
|
<td colspan="6" class="pg-empty">
|
||||||
|
{{
|
||||||
|
amortizeDueLoaded
|
||||||
|
? 'No holding has amortisation due for ' + amortizeFyEnd + '.'
|
||||||
|
: 'Enter the year end and click Show Due.'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -1185,47 +1378,74 @@
|
|||||||
<!-- ===================== 08 GOI CATEGORY TRANSFER ===================== -->
|
<!-- ===================== 08 GOI CATEGORY TRANSFER ===================== -->
|
||||||
@if (activeTab === 'goiTransfer') {
|
@if (activeTab === 'goiTransfer') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - GOI Category Transfer (HTM / AFS / HFT shift)</div>
|
<div class="pos-section-label">
|
||||||
|
Investment - GOI Category Transfer (replace with a security of the same terms)
|
||||||
|
</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="goiCertNo">
|
<select [(ngModel)]="goiPurchaseId" (ngModelChange)="onGoiDealChange()">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of holdings; track h.certNo) {
|
@for (h of saleableHoldings; track h.certNo) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (goiHolding; as h) {
|
@if (goiHolding; as h) {
|
||||||
<label class="inv-form-field"><span>Script</span><input type="text" [value]="h.scriptName" readonly /></label>
|
<label class="inv-form-field"
|
||||||
|
><span>Units</span><input type="text" [value]="h.units | number: '1.0-6'" readonly
|
||||||
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
}
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>From Category *</span>
|
|
||||||
<select [(ngModel)]="goiFromCategory">
|
|
||||||
<option value="">-- Select --</option>
|
|
||||||
@for (c of categoryOptions; track c) {
|
|
||||||
<option [value]="c">{{ c }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="inv-form-field">
|
|
||||||
<span>To Category *</span>
|
|
||||||
<select [(ngModel)]="goiToCategory">
|
|
||||||
<option value="">-- Select --</option>
|
|
||||||
@for (c of categoryOptions; track c) {
|
|
||||||
<option [value]="c">{{ c }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="goiTransferDate"></mifosx-date-ddmmyyyy
|
><span>Current GL</span><input type="text" [value]="h.glCode + ' — ' + h.glName" readonly
|
||||||
></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Transfer to Security</span>
|
||||||
|
<select [(ngModel)]="goiToScriptCode">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (x of goiTargetScripts; track x.scriptCode) {
|
||||||
|
<option [value]="x.scriptCode">{{ x.scriptCode }} — {{ x.scriptName }} ({{ x.isin }})</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@if (goiToScript; as to) {
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>New GL</span><input type="text" [value]="to.glCode + ' — ' + to.glName" readonly
|
||||||
|
/></label>
|
||||||
|
}
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="goiRemarks"
|
||||||
|
/></label>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (goiHolding && !goiTargetScripts.length) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
No other active security with the same coupon rate, maturity date and unit value. Add it in Script Master
|
||||||
|
first.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
@if (goiHolding && goiToScript; as to) {
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
@if (to.glCode === goiHolding!.glCode) {
|
||||||
|
Same GL — the deal moves to {{ to.scriptCode }}; no accounting entry is needed.
|
||||||
|
} @else {
|
||||||
|
Voucher on transfer: Dr {{ to.glCode }} / Cr {{ goiHolding!.glCode }} at the book value of the holding; any
|
||||||
|
premium not yet amortised stays in 100707.
|
||||||
|
}
|
||||||
|
Units, book value and the interest schedule stay the same.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveGoiTransfer()">Save</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="!goiToScript || goiSaving"
|
||||||
|
(click)="saveGoiTransfer()"
|
||||||
|
>
|
||||||
|
Transfer
|
||||||
|
</button>
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ 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 { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { forkJoin, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
SCRIPT_MASTER,
|
SCRIPT_MASTER,
|
||||||
|
ScriptMaster,
|
||||||
PARTY_MASTER,
|
PARTY_MASTER,
|
||||||
SUBTYPE_MASTER,
|
SUBTYPE_MASTER,
|
||||||
BROKER_MASTER,
|
BROKER_MASTER,
|
||||||
@@ -34,6 +37,7 @@ import {
|
|||||||
STAMP_DUTY_GL,
|
STAMP_DUTY_GL,
|
||||||
BROKERAGE_GL,
|
BROKERAGE_GL,
|
||||||
OTHER_CHARGES_GL,
|
OTHER_CHARGES_GL,
|
||||||
|
PREMIUM_ON_INVESTMENTS_GL,
|
||||||
SETTLEMENT_BANK_GL_OPTIONS,
|
SETTLEMENT_BANK_GL_OPTIONS,
|
||||||
periodBetween,
|
periodBetween,
|
||||||
parseDDMMYYYY,
|
parseDDMMYYYY,
|
||||||
@@ -62,6 +66,10 @@ import {
|
|||||||
InvestmentInterestRowDto,
|
InvestmentInterestRowDto,
|
||||||
InvestmentInterestScheduleDto,
|
InvestmentInterestScheduleDto,
|
||||||
InvestmentPurchaseDto,
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentAmortisationDto,
|
||||||
|
InvestmentAmortisationDueDto,
|
||||||
|
InvestmentSaleDto,
|
||||||
|
InvestmentSecurityTransferDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
toHolding
|
toHolding
|
||||||
@@ -156,37 +164,6 @@ interface AccountingPreviewLine {
|
|||||||
amount: number;
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
* 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,
|
* "03 Govt./Bond Entry" submenu in full: Purchase, Redemption, Interest Calculation, Sell,
|
||||||
@@ -252,18 +229,103 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** 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 {
|
||||||
this.investmentService.getInterestPostings().subscribe({
|
forkJoin({
|
||||||
next: (postings) => {
|
postings: this.investmentService
|
||||||
const others = this.txns.filter((t) => !(t.txnType === 'INTEREST' && t.voucherNo.startsWith('JV/')));
|
.getInterestPostings()
|
||||||
|
.pipe(catchError(() => of([] as InvestmentInterestPostingDto[]))),
|
||||||
|
sales: this.investmentService.getSales().pipe(catchError(() => of([] as InvestmentSaleDto[]))),
|
||||||
|
transfers: this.investmentService.getTransfers().pipe(catchError(() => of([] as InvestmentSecurityTransferDto[])))
|
||||||
|
}).subscribe(({ postings, sales, transfers }) => {
|
||||||
|
this.sales = sales;
|
||||||
|
const others = this.txns.filter(
|
||||||
|
(t) => ![
|
||||||
|
'INTEREST',
|
||||||
|
'SELL',
|
||||||
|
'REDEEM',
|
||||||
|
'TRANSFER',
|
||||||
|
'AMORTIZE',
|
||||||
|
'WRITE_OFF'
|
||||||
|
].includes(t.txnType)
|
||||||
|
);
|
||||||
this.txns = [
|
this.txns = [
|
||||||
...others,
|
...others,
|
||||||
...postings.map((x) => this.interestTxn(x))
|
...postings.map((x) => this.interestTxn(x)),
|
||||||
|
...sales.map((x) => this.saleTxn(x)),
|
||||||
|
...transfers.map((x) => this.transferTxn(x))
|
||||||
].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
||||||
},
|
|
||||||
error: () => undefined
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A security transfer shows the book value carried to the new security. */
|
||||||
|
private transferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'TRANSFER',
|
||||||
|
voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.transferDate,
|
||||||
|
amount: x.bookValue,
|
||||||
|
particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`,
|
||||||
|
glCode: x.toGlCode,
|
||||||
|
glName: x.glTransactionId
|
||||||
|
? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})`
|
||||||
|
: `${x.toGlName} (same GL, no entry)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */
|
||||||
|
private saleTxn(x: InvestmentSaleDto): InvestmentTxn {
|
||||||
|
if (x.saleType === 'AMORTISE') {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'AMORTIZE',
|
||||||
|
voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.bookValueSold,
|
||||||
|
particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`,
|
||||||
|
glCode: '500604',
|
||||||
|
glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (x.saleType === 'WRITEOFF') {
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: 'WRITE_OFF',
|
||||||
|
voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.bookValueSold,
|
||||||
|
particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`,
|
||||||
|
glCode: '500605',
|
||||||
|
glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const redeem = x.saleType === 'REDEEM';
|
||||||
|
const pl =
|
||||||
|
x.profitLoss >= 0
|
||||||
|
? `profit ₹${x.profitLoss}`
|
||||||
|
: redeem
|
||||||
|
? `premium written off ₹${-x.profitLoss}`
|
||||||
|
: `loss ₹${-x.profitLoss}`;
|
||||||
|
return {
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: x.bondType as InstrumentType,
|
||||||
|
txnType: x.saleType,
|
||||||
|
voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.settlementDate,
|
||||||
|
amount: x.netAmount,
|
||||||
|
particulars: redeem
|
||||||
|
? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`
|
||||||
|
: `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`,
|
||||||
|
glCode: x.status === 'SETTLED' ? x.bankGlCode : '',
|
||||||
|
glName:
|
||||||
|
x.status === 'SETTLED'
|
||||||
|
? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
|
||||||
|
: 'Not posted (pending settlement)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
|
/** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
|
||||||
private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
|
private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
|
||||||
const coupon = x.lineType === 'COUPON';
|
const coupon = x.lineType === 'COUPON';
|
||||||
@@ -331,13 +393,9 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
|
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
|
||||||
sellForm: BondSellForm = this.blankSellForm();
|
|
||||||
readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS;
|
readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS;
|
||||||
|
|
||||||
// Redemption Entry
|
// Redemption Entry
|
||||||
redeemCertNo = '';
|
|
||||||
redeemDate = this.asOnDate;
|
|
||||||
redeemRemarks = '';
|
|
||||||
|
|
||||||
// 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every
|
// 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every
|
||||||
// half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only:
|
// half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only:
|
||||||
@@ -358,21 +416,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
|
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
|
||||||
|
|
||||||
// Write-off
|
// Write-off
|
||||||
writeoffCertNo = '';
|
|
||||||
writeoffReason = '';
|
|
||||||
|
|
||||||
// Amortize
|
|
||||||
amortizeCertNo = '';
|
|
||||||
amortizeAmt: number | null = null;
|
|
||||||
|
|
||||||
// Amortize Yearly Process
|
|
||||||
amortizeYear = String(new Date().getFullYear());
|
|
||||||
|
|
||||||
// GOI Category Transfer
|
// GOI Category Transfer
|
||||||
goiCertNo = '';
|
|
||||||
goiFromCategory = '';
|
|
||||||
goiToCategory = '';
|
|
||||||
goiTransferDate = this.asOnDate;
|
|
||||||
|
|
||||||
// Investment Depreciation
|
// Investment Depreciation
|
||||||
depreciationDate = this.asOnDate;
|
depreciationDate = this.asOnDate;
|
||||||
@@ -412,8 +457,13 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return this.txns.filter((t) => t.instrumentType === this.bondType);
|
return this.txns.filter((t) => t.instrumentType === this.bondType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scripts offered for a new deal — inactive ones are hidden, except the one a retrieved deal already uses. */
|
||||||
get scriptsForType(): typeof SCRIPT_MASTER {
|
get scriptsForType(): typeof SCRIPT_MASTER {
|
||||||
return this.scriptOptions.filter((s) => s.instrumentType === this.purchaseForm.bondType);
|
return this.scriptOptions.filter(
|
||||||
|
(s) =>
|
||||||
|
s.instrumentType === this.purchaseForm.bondType &&
|
||||||
|
(s.active !== false || s.scriptCode === this.purchaseForm.scriptCode)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
instrumentLabel(type: InstrumentType): string {
|
instrumentLabel(type: InstrumentType): string {
|
||||||
@@ -512,37 +562,6 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
get isMutualFund(): boolean {
|
||||||
return this.purchaseForm.bondType === 'MUTUAL_FUND';
|
return this.purchaseForm.bondType === 'MUTUAL_FUND';
|
||||||
}
|
}
|
||||||
@@ -800,8 +819,18 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const invGl = this.purchaseInvestmentGlCode;
|
const invGl = this.purchaseInvestmentGlCode;
|
||||||
|
// Premium over face value sits in its own GL until it is amortised (Jitendra Sir).
|
||||||
|
const premium = Math.max(0, Math.min(this.purchasePremiumDiscount, book));
|
||||||
const lines: AccountingPreviewLine[] = [
|
const lines: AccountingPreviewLine[] = [
|
||||||
{ side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }];
|
{ side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book - premium }];
|
||||||
|
if (premium > 0) {
|
||||||
|
lines.push({
|
||||||
|
side: 'Dr',
|
||||||
|
glCode: PREMIUM_ON_INVESTMENTS_GL.glCode,
|
||||||
|
glName: PREMIUM_ON_INVESTMENTS_GL.glName,
|
||||||
|
amount: premium
|
||||||
|
});
|
||||||
|
}
|
||||||
// Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir).
|
// Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir).
|
||||||
if (accrued > 0) {
|
if (accrued > 0) {
|
||||||
lines.push({
|
lines.push({
|
||||||
@@ -1031,38 +1060,62 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Redemption Entry --------------------
|
// -------------------- Redemption Entry --------------------
|
||||||
get redeemHolding(): InvestmentHolding | undefined {
|
redeemPurchaseId: number | null = null;
|
||||||
return this.holdingsForType.find((h) => h.certNo === this.redeemCertNo);
|
redeemQuote: InvestmentSaleDto | null = null;
|
||||||
|
redeemError = '';
|
||||||
|
redeemBankGlCode = '';
|
||||||
|
redeemRemarks = '';
|
||||||
|
redeeming = false;
|
||||||
|
|
||||||
|
/** Settled holdings of this type still held — the ones that can be sold or redeemed. */
|
||||||
|
get saleableHoldings(): InvestmentHolding[] {
|
||||||
|
return this.activeHoldingsForType.filter((h) => !!h.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get redeemAmount(): number {
|
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
|
||||||
const h = this.redeemHolding;
|
onRedeemDealChange(): void {
|
||||||
return h ? maturityAmount(h) + accruedInterest(h, this.redeemDate) - h.faceValue + h.faceValue : 0;
|
this.redeemQuote = null;
|
||||||
}
|
this.redeemError = '';
|
||||||
|
if (!this.redeemPurchaseId) {
|
||||||
saveRedemption(): void {
|
|
||||||
const h = this.redeemHolding;
|
|
||||||
if (!h) {
|
|
||||||
this.toast('Select a certificate to redeem.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
h.status = 'MATURED';
|
this.investmentService.redemptionQuote(this.redeemPurchaseId).subscribe({
|
||||||
this.txns = [
|
next: (q) => {
|
||||||
{
|
this.redeemQuote = q;
|
||||||
certNo: h.certNo,
|
this.redeemBankGlCode = q.bankGlCode;
|
||||||
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
|
error: (err) => (this.redeemError = apiErrorMessage(err))
|
||||||
];
|
});
|
||||||
this.toast(`${h.certNo} redeemed. Proceeds ₹${this.redeemAmount.toLocaleString('en-IN')}.`);
|
}
|
||||||
this.redeemCertNo = '';
|
|
||||||
|
/** Posts the face value received at maturity, dated the maturity date, and closes the holding. */
|
||||||
|
redeem(): void {
|
||||||
|
const q = this.redeemQuote;
|
||||||
|
if (!q || this.redeeming) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bankGlAccountId = this.glIdByCode.get(this.redeemBankGlCode) ?? q.bankGlAccountId;
|
||||||
|
this.redeeming = true;
|
||||||
|
this.investmentService
|
||||||
|
.redeem(q.purchaseId, {
|
||||||
|
bankGlAccountId,
|
||||||
|
...(this.redeemRemarks ? { remarks: this.redeemRemarks } : {}),
|
||||||
|
locale: 'en'
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.redeeming = false;
|
||||||
|
this.toast(`Deal ${q.dealNo} redeemed — voucher ${res.resourceIdentifier ?? ''}.`);
|
||||||
|
this.redeemPurchaseId = null;
|
||||||
|
this.redeemQuote = null;
|
||||||
this.redeemRemarks = '';
|
this.redeemRemarks = '';
|
||||||
this.activeTab = 'holdings';
|
this.loadPurchases();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.redeeming = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Interest calculation --------------------
|
// -------------------- Interest calculation --------------------
|
||||||
@@ -1174,274 +1227,367 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
|
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Sell / Redeem Entry --------------------
|
// -------------------- Sell Entry --------------------
|
||||||
/** Locates the original purchase by Deal No / Cert No or ISIN and fills the sell form. */
|
sale = this.blankSale();
|
||||||
searchPurchaseForSell(): void {
|
saleQuote: InvestmentSaleDto | null = null;
|
||||||
const val = this.sellForm.searchValue.trim();
|
saleQuoteError = '';
|
||||||
if (!val) {
|
savingSale = false;
|
||||||
this.toast('Enter a Deal No / Cert No or ISIN to search.');
|
sales: InvestmentSaleDto[] = [];
|
||||||
return;
|
|
||||||
}
|
private blankSale() {
|
||||||
const h =
|
return {
|
||||||
this.sellForm.searchBy === 'DEAL_NO'
|
purchaseId: null as number | null,
|
||||||
? this.holdings.find((x) => x.certNo === val)
|
partyCode: '',
|
||||||
: this.holdings.find((x) => this.scriptOptions.find((s) => s.scriptCode === x.scriptCode)?.isin === val);
|
dealDate: this.asOnDate,
|
||||||
if (!h) {
|
dealTime: '',
|
||||||
this.toast('No matching purchase found.');
|
settlementType: 'T+0',
|
||||||
return;
|
units: null as number | null,
|
||||||
}
|
saleRate: null as number | null,
|
||||||
this.sellForm = {
|
brokerage: null as number | null,
|
||||||
...this.sellForm,
|
bankGlCode: '',
|
||||||
bondType: h.instrumentType as BondType,
|
sanctionAuthority: '',
|
||||||
scriptCode: h.scriptCode,
|
remarks: ''
|
||||||
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 {
|
resetSale(): void {
|
||||||
return this.holdings.find((h) => h.certNo === this.sellForm.certNo);
|
this.sale = this.blankSale();
|
||||||
|
this.saleQuote = null;
|
||||||
|
this.saleQuoteError = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Consideration = units sold × sell rate, scaled by the instrument's unit convention. */
|
get saleHolding(): InvestmentHolding | undefined {
|
||||||
get sellConsideration(): number {
|
return this.holdings.find((h) => h.id === this.sale.purchaseId);
|
||||||
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. */
|
/** An HTM security is sold only with the Board's permission, recorded with its reason. */
|
||||||
get sellSaleDiff(): number {
|
get saleIsHtm(): boolean {
|
||||||
const h = this.sellHolding;
|
return (this.saleHolding?.category ?? '').startsWith('HTM');
|
||||||
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 pendingSalesForType(): InvestmentSaleDto[] {
|
||||||
get sellPremiumLoss(): number {
|
return this.sales.filter((s) => s.status === 'PENDING_SETTLEMENT' && s.bondType === this.bondType);
|
||||||
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 {
|
onSaleDealChange(): void {
|
||||||
const h = this.sellHolding;
|
const h = this.saleHolding;
|
||||||
return h ? accruedInterest(h, this.sellForm.dealDate || this.asOnDate) : 0;
|
this.sale.units = h ? h.units : null;
|
||||||
|
this.sale.bankGlCode = h?.crToAc ?? '';
|
||||||
|
this.refreshSaleQuote();
|
||||||
}
|
}
|
||||||
|
|
||||||
get sellAccruedDays(): number {
|
/** Price, interest from the buyer, cost and profit / loss worked out by the backend, as Settle will post them. */
|
||||||
const h = this.sellHolding;
|
refreshSaleQuote(): void {
|
||||||
if (!h) {
|
const f = this.sale;
|
||||||
return 0;
|
this.saleQuote = null;
|
||||||
}
|
this.saleQuoteError = '';
|
||||||
return Math.max(0, Math.round(yearsBetween(h.purchaseDate, this.sellForm.dealDate || this.asOnDate) * 365));
|
if (!f.purchaseId || !f.units || !f.saleRate || !f.dealDate) {
|
||||||
}
|
|
||||||
|
|
||||||
/** 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;
|
return;
|
||||||
}
|
}
|
||||||
if (f.sellUnit > h.units) {
|
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
this.toast(`Sell Unit cannot exceed units held (${h.units}).`);
|
this.investmentService
|
||||||
return;
|
.quoteSale({
|
||||||
}
|
purchaseId: f.purchaseId,
|
||||||
h.units -= f.sellUnit;
|
dealDate: f.dealDate,
|
||||||
h.faceValue = Math.round(h.units * (f.unitVal || 100));
|
settlementType: f.settlementType,
|
||||||
if (h.units <= 0) {
|
units: f.units,
|
||||||
h.status = f.mode === 'REDEEM' ? 'MATURED' : 'SOLD';
|
saleRate: f.saleRate,
|
||||||
}
|
...(f.brokerage ? { brokerage: f.brokerage } : {}),
|
||||||
this.txns = [
|
...(bankGlAccountId ? { bankGlAccountId } : {})
|
||||||
{
|
})
|
||||||
certNo: h.certNo,
|
.subscribe({
|
||||||
instrumentType: h.instrumentType,
|
next: (q) => (this.saleQuote = q),
|
||||||
txnType: f.mode === 'REDEEM' ? 'REDEEM' : 'SELL',
|
error: (err) => (this.saleQuoteError = apiErrorMessage(err))
|
||||||
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 {
|
/** Saves the sale pending; it reaches the GL when settled on its settlement date. */
|
||||||
return this.amortizeYearlyTargets.reduce((s, t) => s + t.amount, 0);
|
saveSale(): void {
|
||||||
}
|
const f = this.sale;
|
||||||
|
const party = this.partyOptions.find((p) => p.partyCode === f.partyCode);
|
||||||
runAmortizeYearly(): void {
|
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
const targets = this.amortizeYearlyTargets;
|
if (!f.purchaseId || !party?.id || !f.units || !f.saleRate || !bankGlAccountId) {
|
||||||
if (targets.length === 0) {
|
this.toast('Select the deal, buyer and bank, and enter units and sale rate.');
|
||||||
this.toast('No premium-carrying holdings to amortise.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
|
if (this.saleIsHtm && (!f.sanctionAuthority.trim() || !f.remarks.trim())) {
|
||||||
certNo: t.holding.certNo,
|
this.toast('HTM sale needs the Board sanction authority and the reason in Remarks.');
|
||||||
instrumentType: t.holding.instrumentType,
|
return;
|
||||||
txnType: 'AMORTIZE',
|
}
|
||||||
voucherNo: `INV/AMZ-YR/${this.amortizeYear}-${i + 1}`,
|
const body = {
|
||||||
txnDate: this.asOnDate,
|
purchaseId: f.purchaseId,
|
||||||
amount: t.amount,
|
partyId: party.id,
|
||||||
particulars: `Yearly amortization process ${this.amortizeYear} - ${t.holding.scriptName}`
|
dealDate: f.dealDate,
|
||||||
}));
|
dealTime: f.dealTime || '',
|
||||||
this.txns = [
|
settlementType: f.settlementType,
|
||||||
...newTxns,
|
units: f.units,
|
||||||
...this.txns
|
saleRate: f.saleRate,
|
||||||
];
|
...(f.brokerage ? { brokerage: f.brokerage } : {}),
|
||||||
|
bankGlAccountId,
|
||||||
|
...(f.sanctionAuthority.trim() ? { sanctionAuthority: f.sanctionAuthority.trim() } : {}),
|
||||||
|
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
this.savingSale = true;
|
||||||
|
this.investmentService.createSale(body).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.savingSale = false;
|
||||||
|
this.toast(`Sale saved — settle it on ${this.saleQuote?.settlementDate ?? 'the settlement date'} to post it.`);
|
||||||
|
this.sale = this.blankSale();
|
||||||
|
this.saleQuote = null;
|
||||||
|
this.loadInterestLog();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.savingSale = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
settleSale(s: InvestmentSaleDto): void {
|
||||||
|
if (!s.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.settleSale(s.id).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.toast(`Sale of deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`);
|
||||||
|
this.loadPurchases();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dropSale(s: InvestmentSaleDto): void {
|
||||||
|
if (!s.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.deleteSale(s.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toast(`Pending sale of deal ${s.dealNo} dropped.`);
|
||||||
|
this.loadInterestLog();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Write-off --------------------
|
||||||
|
writeoffPurchaseId: number | null = null;
|
||||||
|
writeoffAmount: number | null = null;
|
||||||
|
writeoffReason = '';
|
||||||
|
writingOff = false;
|
||||||
|
|
||||||
|
get writeoffHolding(): InvestmentHolding | undefined {
|
||||||
|
return this.holdings.find((h) => h.id === this.writeoffPurchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when a sale or redemption credits the unamortised premium to its own GL (100707), apart from the investment GL. */
|
||||||
|
splitPremium(q: InvestmentSaleDto): boolean {
|
||||||
|
return !!q.premiumCleared && q.premiumCleared > 0 && !!q.premiumGlCode && q.premiumGlCode !== q.investmentGlCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Book value of a holding as it stands: cost less what sales and write-offs have taken off. */
|
||||||
|
carryingValue(h: InvestmentHolding): number {
|
||||||
|
const taken = this.sales
|
||||||
|
.filter((x) => x.purchaseId === h.id && x.status === 'SETTLED')
|
||||||
|
.reduce((sum, x) => sum + x.bookValueSold, 0);
|
||||||
|
return Math.round(((h.bookValue ?? 0) - taken) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes the loss off at once: Dr 500605 Loss on Investments / Cr Investment; the holding stays at the lower value. */
|
||||||
|
saveWriteoff(): void {
|
||||||
|
const h = this.writeoffHolding;
|
||||||
|
const amount = this.writeoffAmount;
|
||||||
|
if (!h?.id || !amount || amount <= 0 || !this.writeoffReason.trim()) {
|
||||||
|
this.toast('Select the deal, enter the loss amount and the reason.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount >= this.carryingValue(h)) {
|
||||||
|
this.toast('The write-off must be less than the book value; sell or redeem the security to close it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.writingOff = true;
|
||||||
|
this.investmentService.writeOff(h.id, { amount, remarks: this.writeoffReason.trim(), locale: 'en' }).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.writingOff = false;
|
||||||
|
this.toast(`Deal ${h.dealNo}: ₹${amount} written off — voucher ${res.resourceIdentifier ?? ''}.`);
|
||||||
|
this.writeoffPurchaseId = null;
|
||||||
|
this.writeoffAmount = null;
|
||||||
|
this.writeoffReason = '';
|
||||||
|
this.loadPurchases();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.writingOff = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Amortize --------------------
|
||||||
|
amortizePurchaseId: number | null = null;
|
||||||
|
amortSchedule: InvestmentAmortisationDto | null = null;
|
||||||
|
amortizing = false;
|
||||||
|
|
||||||
|
/** Active HTM holdings bought above face value — the ones whose premium is amortised. */
|
||||||
|
get premiumHoldings(): InvestmentHolding[] {
|
||||||
|
return this.saleableHoldings.filter((h) => (h.category ?? '').startsWith('HTM') && h.purchaseRate > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAmortizeDealChange(): void {
|
||||||
|
this.amortSchedule = null;
|
||||||
|
if (this.amortizePurchaseId) {
|
||||||
|
this.investmentService.getAmortisation(this.amortizePurchaseId).subscribe({
|
||||||
|
next: (d) => (this.amortSchedule = d),
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Posts the next due 31 March of the selected holding: Dr 500604 / Cr premium GL. */
|
||||||
|
amortizeRow(fyEnd: string): void {
|
||||||
|
const d = this.amortSchedule;
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.amortizing = true;
|
||||||
|
this.investmentService
|
||||||
|
.amortise({ purchaseId: d.purchaseId, fyEnd, locale: 'en', dateFormat: 'dd-MM-yyyy' })
|
||||||
|
.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.amortizing = false;
|
||||||
|
this.toast(`Deal ${d.dealNo}: premium amortised for ${fyEnd} — voucher ${res.resourceIdentifier ?? ''}.`);
|
||||||
|
this.onAmortizeDealChange();
|
||||||
|
this.loadPurchases();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.amortizing = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Amortize Yearly Process --------------------
|
||||||
|
/** Latest 31 March on or before today. */
|
||||||
|
amortizeFyEnd = (() => {
|
||||||
|
const t = new Date();
|
||||||
|
const y = t.getMonth() > 2 || (t.getMonth() === 2 && t.getDate() === 31) ? t.getFullYear() : t.getFullYear() - 1;
|
||||||
|
return `31-03-${y}`;
|
||||||
|
})();
|
||||||
|
amortizeDue: InvestmentAmortisationDueDto[] = [];
|
||||||
|
amortizeDueLoaded = false;
|
||||||
|
|
||||||
|
get amortizeDueTotal(): number {
|
||||||
|
return Math.round(this.amortizeDue.reduce((sum, d) => sum + d.amount, 0) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAmortizeDue(): void {
|
||||||
|
this.amortizeDueLoaded = false;
|
||||||
|
if (!/^31-03-\d{4}$/.test(this.amortizeFyEnd)) {
|
||||||
|
this.toast('Amortisation is done as on 31 March: enter the year end as 31-03-YYYY.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.getAmortisationDue(this.amortizeFyEnd).subscribe({
|
||||||
|
next: (d) => {
|
||||||
|
this.amortizeDue = d;
|
||||||
|
this.amortizeDueLoaded = true;
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Posts the year's amortisation for every holding listed; one voucher per holding. */
|
||||||
|
runAmortizeYearly(): void {
|
||||||
|
if (!this.amortizeDue.length) {
|
||||||
|
this.toast('No holding has premium amortisation due for this year end.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.amortizing = true;
|
||||||
|
this.investmentService.amortise({ fyEnd: this.amortizeFyEnd, locale: 'en', dateFormat: 'dd-MM-yyyy' }).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.amortizing = false;
|
||||||
this.toast(
|
this.toast(
|
||||||
`Yearly amortization run for ${targets.length} holding(s). Total ₹${this.amortizeYearlyTotal.toLocaleString('en-IN')}.`
|
`Amortisation for ${this.amortizeFyEnd} posted for ${this.amortizeDue.length} holding(s) — vouchers ${res.resourceIdentifier ?? ''}.`
|
||||||
);
|
);
|
||||||
|
this.loadAmortizeDue();
|
||||||
|
this.loadPurchases();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.amortizing = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- GOI Category Transfer --------------------
|
// -------------------- GOI Category Transfer --------------------
|
||||||
|
// -------------------- GOI Category Transfer --------------------
|
||||||
|
goiPurchaseId: number | null = null;
|
||||||
|
goiToScriptCode = '';
|
||||||
|
goiRemarks = '';
|
||||||
|
goiSaving = false;
|
||||||
|
|
||||||
get goiHolding(): InvestmentHolding | undefined {
|
get goiHolding(): InvestmentHolding | undefined {
|
||||||
return this.holdings.find((h) => h.certNo === this.goiCertNo);
|
return this.holdings.find((h) => h.id === this.goiPurchaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get goiFromScript(): ScriptMaster | undefined {
|
||||||
|
return this.scriptOptions.find((x) => x.scriptCode === this.goiHolding?.scriptCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Securities the holding can move to: same type, coupon rate, maturity and unit value — only the security changes. */
|
||||||
|
get goiTargetScripts(): ScriptMaster[] {
|
||||||
|
const from = this.goiFromScript;
|
||||||
|
if (!from) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.scriptOptions.filter(
|
||||||
|
(x) =>
|
||||||
|
!!x.id &&
|
||||||
|
x.active !== false &&
|
||||||
|
x.scriptCode !== from.scriptCode &&
|
||||||
|
x.instrumentType === from.instrumentType &&
|
||||||
|
x.couponRate === from.couponRate &&
|
||||||
|
x.maturityDate === from.maturityDate &&
|
||||||
|
x.faceValuePerUnit === from.faceValuePerUnit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get goiToScript(): ScriptMaster | undefined {
|
||||||
|
return this.scriptOptions.find((x) => x.scriptCode === this.goiToScriptCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
onGoiDealChange(): void {
|
||||||
|
this.goiToScriptCode = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replaces the holding's security with the new one; the voucher moves the book value only if the GL differs. */
|
||||||
saveGoiTransfer(): void {
|
saveGoiTransfer(): void {
|
||||||
const h = this.goiHolding;
|
const h = this.goiHolding;
|
||||||
if (!h || !this.goiFromCategory || !this.goiToCategory) {
|
const to = this.goiToScript;
|
||||||
this.toast('Select a certificate and both From / To categories.');
|
if (!h?.id || !to?.id || this.goiSaving) {
|
||||||
|
this.toast('Select the deal and the security to transfer to.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.goiFromCategory === this.goiToCategory) {
|
this.goiSaving = true;
|
||||||
this.toast('From and To categories must differ.');
|
this.investmentService
|
||||||
return;
|
.transferSecurity(h.id, {
|
||||||
}
|
scriptId: to.id,
|
||||||
this.txns = [
|
...(this.goiRemarks ? { remarks: this.goiRemarks } : {}),
|
||||||
{
|
locale: 'en'
|
||||||
certNo: h.certNo,
|
})
|
||||||
instrumentType: h.instrumentType,
|
.subscribe({
|
||||||
txnType: 'AMORTIZE',
|
next: (res) => {
|
||||||
voucherNo: `INV/GOI-TRF/${h.certNo}-${this.txns.length + 1}`,
|
this.goiSaving = false;
|
||||||
txnDate: this.goiTransferDate,
|
this.toast(`Deal ${h.dealNo} transferred to ${to.scriptCode} — ${res.resourceIdentifier ?? ''}.`);
|
||||||
amount: h.faceValue,
|
this.goiPurchaseId = null;
|
||||||
particulars: `GOI category transfer ${this.goiFromCategory} → ${this.goiToCategory} - ${h.scriptName}`
|
this.goiToScriptCode = '';
|
||||||
|
this.goiRemarks = '';
|
||||||
|
this.loadPurchases();
|
||||||
},
|
},
|
||||||
...this.txns
|
error: (err) => {
|
||||||
];
|
this.goiSaving = false;
|
||||||
this.toast(`${h.certNo} transferred from ${this.goiFromCategory} to ${this.goiToCategory}.`);
|
this.toast(apiErrorMessage(err));
|
||||||
this.goiCertNo = '';
|
}
|
||||||
this.goiFromCategory = '';
|
});
|
||||||
this.goiToCategory = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Investment Depreciation --------------------
|
// -------------------- Investment Depreciation --------------------
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type TxnType =
|
|||||||
| 'REDEEM'
|
| 'REDEEM'
|
||||||
| 'WRITE_OFF'
|
| 'WRITE_OFF'
|
||||||
| 'AMORTIZE'
|
| 'AMORTIZE'
|
||||||
|
| 'TRANSFER'
|
||||||
| 'PART_PAYMENT';
|
| 'PART_PAYMENT';
|
||||||
|
|
||||||
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
||||||
@@ -1526,6 +1527,8 @@ export const BROKEN_PERIOD_INTEREST_GL: InvestmentGlOption = {
|
|||||||
export const STAMP_DUTY_GL: InvestmentGlOption = { glCode: '500601', glName: 'STAMP DUTY ON INVESTMENTS' };
|
export const STAMP_DUTY_GL: InvestmentGlOption = { glCode: '500601', glName: 'STAMP DUTY ON INVESTMENTS' };
|
||||||
export const BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE ON INVESTMENTS' };
|
export const BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE ON INVESTMENTS' };
|
||||||
export const OTHER_CHARGES_GL: InvestmentGlOption = { glCode: '500603', glName: 'OTHER CHARGES ON INVESTMENTS' };
|
export const OTHER_CHARGES_GL: InvestmentGlOption = { glCode: '500603', glName: 'OTHER CHARGES ON INVESTMENTS' };
|
||||||
|
/** Premium paid over face value, held until amortised to maturity (Jitendra Sir). */
|
||||||
|
export const PREMIUM_ON_INVESTMENTS_GL: InvestmentGlOption = { glCode: '100707', glName: 'PREMIUM ON INVESTMENTS' };
|
||||||
|
|
||||||
/** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */
|
/** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */
|
||||||
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
|
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
|
||||||
|
|||||||
@@ -175,6 +175,68 @@ export class InvestmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
|
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
|
||||||
|
/** GOI Category Transfers, newest first. */
|
||||||
|
getTransfers(): Observable<InvestmentSecurityTransferDto[]> {
|
||||||
|
return this.http.get<InvestmentSecurityTransferDto[]>('/investment/transfers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Moves a holding to another security of the same terms; a voucher is raised only when the GL changes. */
|
||||||
|
transferSecurity(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/transfer`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sales and redemptions, newest first (pending sales included). */
|
||||||
|
getSales(): Observable<InvestmentSaleDto[]> {
|
||||||
|
return this.http.get<InvestmentSaleDto[]>('/investment/sales');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a sale would come to before it is saved (dealDate dd-MM-yyyy). */
|
||||||
|
quoteSale(params: Record<string, string | number>): Observable<InvestmentSaleDto> {
|
||||||
|
return this.http.get<InvestmentSaleDto>('/investment/sales/quote', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
createSale(body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/sales', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Posts a pending sale on or after its settlement date; the voucher number comes back as resourceIdentifier. */
|
||||||
|
settleSale(saleId: number): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>(`/investment/sales/${saleId}/settle`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSale(saleId: number): Observable<unknown> {
|
||||||
|
return this.http.delete(`/investment/sales/${saleId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Premium amortisation schedule of a holding (06 Amortize Entry). */
|
||||||
|
getAmortisation(purchaseId: number): Observable<InvestmentAmortisationDto> {
|
||||||
|
return this.http.get<InvestmentAmortisationDto>(`/investment/amortisation/${purchaseId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Holdings due for amortisation on a 31 March (07 Amortize Yearly Process). */
|
||||||
|
getAmortisationDue(fyEnd: string): Observable<InvestmentAmortisationDueDto[]> {
|
||||||
|
return this.http.get<InvestmentAmortisationDueDto[]>(`/investment/amortisation/due?fyEnd=${fyEnd}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Posts amortisation for a 31 March: one holding when purchaseId is given, else every holding due. */
|
||||||
|
amortise(body: object): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>('/investment/amortisation', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes part of a holding's book value off as a loss (Dr Loss / Cr Investment); the units stay held. */
|
||||||
|
writeOff(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/write-off`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the redemption at maturity will post; refused before maturity. */
|
||||||
|
redemptionQuote(purchaseId: number): Observable<InvestmentSaleDto> {
|
||||||
|
return this.http.get<InvestmentSaleDto>(`/investment/purchases/${purchaseId}/redemption-quote`);
|
||||||
|
}
|
||||||
|
|
||||||
|
redeem(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>(`/investment/purchases/${purchaseId}/redeem`, body);
|
||||||
|
}
|
||||||
|
|
||||||
/** Interest lines already posted (coupons and provisions), newest first. */
|
/** Interest lines already posted (coupons and provisions), newest first. */
|
||||||
getInterestPostings(): Observable<InvestmentInterestPostingDto[]> {
|
getInterestPostings(): Observable<InvestmentInterestPostingDto[]> {
|
||||||
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
|
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
|
||||||
@@ -334,7 +396,8 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
party: p.partyName,
|
party: p.partyName,
|
||||||
partyCode: p.partyCode,
|
partyCode: p.partyCode,
|
||||||
subType: p.subType ?? '',
|
subType: p.subType ?? '',
|
||||||
faceValue: p.faceValue,
|
// Face value still held after any part sale.
|
||||||
|
faceValue: p.units ? Math.round((p.faceValue * p.balanceUnits * 100) / p.units) / 100 : p.faceValue,
|
||||||
purchaseRate: p.purchaseRate,
|
purchaseRate: p.purchaseRate,
|
||||||
units: p.balanceUnits,
|
units: p.balanceUnits,
|
||||||
purchaseDate: p.settlementDate,
|
purchaseDate: p.settlementDate,
|
||||||
@@ -345,7 +408,7 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
glName: p.investmentGlName,
|
glName: p.investmentGlName,
|
||||||
crToAc: p.crGlCode,
|
crToAc: p.crGlCode,
|
||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: p.status as HoldingStatus,
|
status: (p.status === 'REDEEMED' ? 'MATURED' : p.status) as HoldingStatus,
|
||||||
settlementDate: p.settlementDate,
|
settlementDate: p.settlementDate,
|
||||||
lastIntDate: p.lastIntDate,
|
lastIntDate: p.lastIntDate,
|
||||||
glTransactionId: p.glTransactionId,
|
glTransactionId: p.glTransactionId,
|
||||||
@@ -378,6 +441,118 @@ export interface InvestmentInterestRowDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
|
/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
|
||||||
|
/** A sale (before maturity) or redemption (at maturity) of a holding; also the shape of a quote before saving. */
|
||||||
|
/** One line of a holding's premium amortisation schedule (31 March, or the remainder at maturity). */
|
||||||
|
export interface InvestmentAmortisationRowDto {
|
||||||
|
kind: 'YEAR' | 'MATURITY';
|
||||||
|
fyEnd: string;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
days: number;
|
||||||
|
amount: number;
|
||||||
|
premiumAfter?: number;
|
||||||
|
posted: boolean;
|
||||||
|
voucherNo?: string;
|
||||||
|
glTransactionId?: string;
|
||||||
|
postable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvestmentAmortisationDto {
|
||||||
|
purchaseId: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo: string;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
category: string;
|
||||||
|
settlementDate: string;
|
||||||
|
maturityDate: string;
|
||||||
|
faceValue: number;
|
||||||
|
premium: number;
|
||||||
|
unamortisedPremium: number;
|
||||||
|
bookValue: number;
|
||||||
|
premiumGlCode: string;
|
||||||
|
rows: InvestmentAmortisationRowDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A holding whose premium amortisation falls due on a 31 March. */
|
||||||
|
export interface InvestmentAmortisationDueDto {
|
||||||
|
purchaseId: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo: string;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
fyEnd: string;
|
||||||
|
fromDate: string;
|
||||||
|
days: number;
|
||||||
|
unamortisedPremium: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvestmentSaleDto {
|
||||||
|
id?: number;
|
||||||
|
purchaseId: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo: string;
|
||||||
|
bondType: string;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
saleType: 'SELL' | 'REDEEM' | 'WRITEOFF' | 'AMORTISE';
|
||||||
|
partyId?: number;
|
||||||
|
partyName?: string;
|
||||||
|
dealDate: string;
|
||||||
|
dealTime?: string;
|
||||||
|
settlementType?: string;
|
||||||
|
settlementDate: string;
|
||||||
|
units: number;
|
||||||
|
faceValue: number;
|
||||||
|
saleRate: number;
|
||||||
|
consideration: number;
|
||||||
|
accruedDays: number;
|
||||||
|
accruedInterest: number;
|
||||||
|
receivableCleared: number;
|
||||||
|
interestIncome: number;
|
||||||
|
brokerage: number;
|
||||||
|
bookValueSold: number;
|
||||||
|
/** Part of the book value taken off that was unamortised premium. */
|
||||||
|
premiumCleared?: number;
|
||||||
|
/** GL the premium share is credited to (100707, or the investment GL on older deals). */
|
||||||
|
premiumGlCode?: string;
|
||||||
|
profitLoss: number;
|
||||||
|
netAmount: number;
|
||||||
|
bankGlAccountId: number;
|
||||||
|
bankGlCode: string;
|
||||||
|
bankGlName: string;
|
||||||
|
investmentGlCode: string;
|
||||||
|
sanctionAuthority?: string;
|
||||||
|
remarks?: string;
|
||||||
|
status: 'PENDING_SETTLEMENT' | 'SETTLED';
|
||||||
|
glTransactionId?: string;
|
||||||
|
voucherNo?: string;
|
||||||
|
settledOnDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A GOI Category Transfer: a holding moved to another security of the same terms. */
|
||||||
|
export interface InvestmentSecurityTransferDto {
|
||||||
|
id: number;
|
||||||
|
purchaseId: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo: string;
|
||||||
|
bondType: string;
|
||||||
|
fromScriptCode: string;
|
||||||
|
fromScriptDesc: string;
|
||||||
|
toScriptCode: string;
|
||||||
|
toScriptDesc: string;
|
||||||
|
fromGlCode: string;
|
||||||
|
toGlCode: string;
|
||||||
|
toGlName: string;
|
||||||
|
transferDate: string;
|
||||||
|
units: number;
|
||||||
|
bookValue: number;
|
||||||
|
remarks?: string;
|
||||||
|
glTransactionId?: string;
|
||||||
|
voucherNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** An interest line already posted to GL with its JV voucher. */
|
/** An interest line already posted to GL with its JV voucher. */
|
||||||
export interface InvestmentInterestPostingDto {
|
export interface InvestmentInterestPostingDto {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user