Compare commits

...
6 changed files with 1184 additions and 556 deletions
@@ -136,6 +136,7 @@
<th>Party</th>
<th class="num">Units</th>
<th class="num">Face Value</th>
<th class="num">Book Value</th>
<th class="num">Rate</th>
<th>Settlement Date</th>
<th>Maturity Date</th>
@@ -155,6 +156,7 @@
<td>{{ h.party }}</td>
<td class="num">{{ h.units | number }}</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>{{ h.purchaseDate }}</td>
<td>{{ h.maturityDate }}</td>
@@ -177,7 +179,7 @@
</tr>
} @empty {
<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>
}
</tbody>
@@ -650,40 +652,79 @@
<!-- ===================== 02 REDEMPTION ENTRY ===================== -->
@if (activeTab === 'redemption') {
<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">
<label class="inv-form-field">
<span>Cert No / Deal No *</span>
<select [(ngModel)]="redeemCertNo">
<option value="">-- Select --</option>
@for (h of activeHoldingsForType; track h.certNo) {
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
<span>* Deal No</span>
<select [(ngModel)]="redeemPurchaseId" (ngModelChange)="onRedeemDealChange()">
<option [ngValue]="null">-- Select --</option>
@for (h of saleableHoldings; track h.certNo) {
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptName }} (matures {{ h.maturityDate }})</option>
}
</select>
</label>
@if (redeemHolding; as h) {
<label class="inv-form-field"><span>Script</span><input type="text" [value]="h.scriptName" readonly /></label>
@if (redeemQuote; as q) {
<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 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 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 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>
@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">
<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>
</div>
</div>
@@ -853,7 +894,7 @@
<tr [style.opacity]="r.due ? 1 : 0.6">
<td>
<span class="pg-chip">{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}</span>
@if (r.due) {
@if (r.due && !r.posted) {
<span class="pg-chip">Due</span>
}
</td>
@@ -920,175 +961,240 @@
<!-- ===================== 04 SELL ENTRY ===================== -->
@if (activeTab === 'sell') {
<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">
<label class="inv-form-field">
<span>Mode</span>
<select [(ngModel)]="sellForm.mode">
<option value="SELL">SELL</option>
<option value="REDEEM">REDEEM</option>
<option value="MUTUAL_FUND_SELL">MUTUAL FUND SELL</option>
</select>
</label>
<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>&nbsp;</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>
<span>* Deal No</span>
<select [(ngModel)]="sale.purchaseId" (ngModelChange)="onSaleDealChange()">
<option [ngValue]="null">-- Select --</option>
@for (h of saleableHoldings; track h.certNo) {
<option [ngValue]="h.id">
{{ h.dealNo }} — {{ h.scriptName }} ({{ h.units | number: '1.0-6' }} units)
</option>
}
</select>
</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">
<span>Buyer Code</span>
<select [(ngModel)]="sellForm.buyerCode">
<span>* Buyer (Party)</span>
<select [(ngModel)]="sale.partyCode">
<option value="">-- Select --</option>
@for (p of partyOptions; track p.partyCode) {
<option [value]="p.partyName">{{ p.partyName }}</option>
@for (p of activePartyOptions; track p.partyCode) {
<option [value]="p.partyCode">{{ p.partyCode }} — {{ p.partyName }}</option>
}
</select>
</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"
><span>TBill GL.A/C.</span><input type="text" [(ngModel)]="sellForm.tbillGlAc"
/></label>
<label class="inv-form-field"
><span>* Deal Date</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.dealDate"></mifosx-date-ddmmyyyy
><span>* Deal Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="sale.dealDate" (ngModelChange)="refreshSaleQuote()"></mifosx-date-ddmmyyyy
></label>
<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 class="inv-form-field">
<span>Settlement Type</span>
<select [(ngModel)]="sellForm.settlementType">
<span>* Settlement Type</span>
<select [(ngModel)]="sale.settlementType" (ngModelChange)="refreshSaleQuote()">
<option value="T+0">T+0</option>
<option value="T+1">T+1</option>
<option value="T+2">T+2</option>
</select>
</label>
<label class="inv-form-field"
><span>Settlement Dt</span><mifosx-date-ddmmyyyy [(ngModel)]="sellForm.settlementDt"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>* Face Value</span><input type="number" [(ngModel)]="sellForm.faceValue" min="0"
><span>* Units to Sell</span
><input type="number" min="0" [(ngModel)]="sale.units" (change)="refreshSaleQuote()"
/></label>
<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 class="inv-form-field"
><span>* Unit Val</span><input type="number" [(ngModel)]="sellForm.unitVal" min="0"
/></label>
<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
><span>Brokerage</span
><input type="number" min="0" [(ngModel)]="sale.brokerage" (change)="refreshSaleQuote()"
/></label>
<label class="inv-form-field">
<span>Recomm. Authority</span>
<select [(ngModel)]="sellForm.recommAuthority">
<option value="">-- Select --</option>
@for (a of authorityOptions; track a) {
<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>
<span>* Amount received into (Bank)</span>
<select [(ngModel)]="sale.bankGlCode" (ngModelChange)="refreshSaleQuote()">
@for (g of settlementBankOptions; track g.glCode) {
<option [value]="g.glCode">{{ g.glCode }} — {{ g.glName }}</option>
}
</select>
</label>
<label class="inv-form-field"
><span>CALL MONEY</span
><input type="checkbox" [(ngModel)]="sellForm.callMoney" style="height: 18px; width: 18px"
><span>{{ saleIsHtm ? '* ' : '' }}Sanction Authority</span
><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>
</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">
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">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)="cancelSell()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="exitSell()">Exit</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)="resetSale()">Cancel</button>
<button type="button" class="pg-btn pg-btn--ghost" (click)="setTab('holdings')">Exit</button>
</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>
}
<!-- ===================== 05 WRITE OFF ENTRY ===================== -->
@if (activeTab === 'writeoff') {
<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">
<label class="inv-form-field">
<span>Cert No *</span>
<select [(ngModel)]="writeoffCertNo">
<option value="">-- Select --</option>
@for (h of activeHoldingsForType; track h.certNo) {
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
<span>* Deal No</span>
<select [(ngModel)]="writeoffPurchaseId">
<option [ngValue]="null">-- Select --</option>
@for (h of saleableHoldings; track h.certNo) {
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
}
</select>
</label>
@if (writeoffHolding; as h) {
<label class="inv-form-field"
><span>Face Value to be Written Off</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 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 class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
</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">
<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>
</div>
</div>
@@ -1097,33 +1203,104 @@
<!-- ===================== 06 AMORTIZE ENTRY ===================== -->
@if (activeTab === 'amortize') {
<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">
<label class="inv-form-field">
<span>Cert No *</span>
<select [(ngModel)]="amortizeCertNo">
<option value="">-- Select --</option>
@for (h of activeHoldingsForType; track h.certNo) {
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
<span>* Deal No</span>
<select [(ngModel)]="amortizePurchaseId" (ngModelChange)="onAmortizeDealChange()">
<option [ngValue]="null">-- Select --</option>
@for (h of premiumHoldings; track h.certNo) {
<option [ngValue]="h.id">
{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }} &#64; {{ h.purchaseRate }}
</option>
}
</select>
</label>
@if (amortizeHolding; as h) {
@if (amortSchedule; as d) {
<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 class="inv-form-field"
><span>Suggested Yearly Amortization</span
><input type="text" [value]="'₹' + (suggestedAmortizeAmt | number: '1.2-2')" readonly
><span>Premium Paid</span><input type="text" [value]="'₹' + (d.premium | 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 class="inv-form-field"
><span>Amortize Amount</span
><input type="number" [(ngModel)]="amortizeAmt" min="0" [placeholder]="suggestedAmortizeAmt"
/></label>
</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">
<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>
</div>
</div>
@@ -1132,49 +1309,65 @@
<!-- ===================== 07 AMORTIZE YEARLY PROCESS ===================== -->
@if (activeTab === 'amortizeYearly') {
<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">
<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 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 class="inv-form-field"
><span>Total Amortization</span
><input type="text" [value]="'₹' + (amortizeYearlyTotal | number: '1.2-2')" readonly
><span>Total Amortisation</span
><input type="text" [value]="'₹' + (amortizeDueTotal | number: '1.2-2')" readonly
/></label>
</div>
<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>
</div>
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Deal No</th>
<th>Script</th>
<th class="num">Face Value</th>
<th class="num">Purchase Rate</th>
<th>Maturity Date</th>
<th class="num col-out">Yearly Amortization</th>
<th>Period</th>
<th class="num">Days</th>
<th class="num">Premium Left</th>
<th class="num col-out">Amortisation</th>
</tr>
</thead>
<tbody>
@for (t of amortizeYearlyTargets; track t.holding.certNo) {
@for (d of amortizeDue; track d.purchaseId) {
<tr>
<td>
<span class="pg-chip">{{ t.holding.certNo }}</span>
<span class="pg-chip">{{ d.dealNo }}</span>
</td>
<td>{{ t.holding.scriptName }}</td>
<td class="num">₹{{ t.holding.faceValue | number: '1.2-2' }}</td>
<td class="num">{{ t.holding.purchaseRate }}</td>
<td>{{ t.holding.maturityDate }}</td>
<td class="num col-out">₹{{ t.amount | number: '1.2-2' }}</td>
<td>{{ d.scriptCode }} {{ d.scriptDesc }}</td>
<td>{{ d.fromDate }} → {{ d.fyEnd }}</td>
<td class="num">{{ d.days }}</td>
<td class="num">₹{{ d.unamortisedPremium | number: '1.2-2' }}</td>
<td class="num col-out">₹{{ d.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<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>
}
</tbody>
@@ -1185,47 +1378,74 @@
<!-- ===================== 08 GOI CATEGORY TRANSFER ===================== -->
@if (activeTab === 'goiTransfer') {
<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">
<label class="inv-form-field">
<span>Cert No *</span>
<select [(ngModel)]="goiCertNo">
<option value="">-- Select --</option>
@for (h of holdings; track h.certNo) {
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
<span>* Deal No</span>
<select [(ngModel)]="goiPurchaseId" (ngModelChange)="onGoiDealChange()">
<option [ngValue]="null">-- Select --</option>
@for (h of saleableHoldings; track h.certNo) {
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptCode }} {{ h.scriptName }}</option>
}
</select>
</label>
@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"
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
><span>Current GL</span><input type="text" [value]="h.glCode + ' — ' + h.glName" readonly
/></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>
}
<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"
><span>Transfer Date *</span><mifosx-date-ddmmyyyy [(ngModel)]="goiTransferDate"></mifosx-date-ddmmyyyy
></label>
</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">
<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>
</div>
</div>
File diff suppressed because it is too large Load Diff
+5
View File
@@ -28,6 +28,7 @@ export type TxnType =
| 'REDEEM'
| 'WRITE_OFF'
| 'AMORTIZE'
| 'TRANSFER'
| 'PART_PAYMENT';
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
@@ -135,6 +136,8 @@ export interface InvestmentHolding {
settlementAmount?: number;
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
settlementDate?: string;
/** Last coupon date before the purchase; coupons fall every six months from it. */
lastIntDate?: string;
/** Journal entry transaction posted when the deal was settled. */
glTransactionId?: string;
}
@@ -1524,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 BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE 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. */
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
@@ -286,7 +286,7 @@
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.issueDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
><span>Redum / Mat Date</span
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Redum / Mat Date</span
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.maturityDate"></mifosx-date-ddmmyyyy
></label>
<label class="inv-form-field"
@@ -53,6 +53,10 @@ type MasterTab =
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
* lists, each with an inline Add-row form so the demo stays interactive without a backend.
*/
/** Detail GLs under the 100700 INVESTMENTS head; 100707 holds premium only, never a party's or script's GL. */
const INVESTMENT_GL_HEAD_PREFIX = '1007';
const PREMIUM_ON_INVESTMENTS_GL_CODE = '100707';
@Component({
selector: 'mifosx-investment-masters',
templateUrl: './investment-masters.component.html',
@@ -120,7 +124,7 @@ export class InvestmentMastersComponent implements OnInit {
saving = false;
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
/** Enabled ASSET detail GLs from the chart of accounts. */
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
investmentGlOptions: InvestmentGlAccount[] = [];
newSubType: Partial<SubTypeMaster> = {};
newBroker: Partial<BrokerMaster> = {};
@@ -133,7 +137,10 @@ export class InvestmentMastersComponent implements OnInit {
ngOnInit(): void {
this.loadDailyRates();
this.investmentService.getAssetGlAccounts().subscribe({
next: (gls) => (this.investmentGlOptions = gls),
next: (gls) =>
(this.investmentGlOptions = gls.filter(
(g) => g.glCode.startsWith(INVESTMENT_GL_HEAD_PREFIX) && g.glCode !== PREMIUM_ON_INVESTMENTS_GL_CODE
)),
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
this.reloadMasters();
@@ -287,8 +294,8 @@ export class InvestmentMastersComponent implements OnInit {
this.toast('Enter Bond Type, Script Desc and GL A/c No.');
return;
}
if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate)) {
this.toast('Enter ISIN, Int Rate and Issue Date for this security.');
if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate || !f.maturityDate)) {
this.toast('Enter ISIN, Int Rate, Issue Date and Maturity Date for this security.');
return;
}
const body = {
+207 -2
View File
@@ -175,6 +175,73 @@ export class InvestmentService {
}
/** 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. */
getInterestPostings(): Observable<InvestmentInterestPostingDto[]> {
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
}
/** Interest lines of all holdings that are due now or within the next few days. */
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
@@ -329,7 +396,8 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
party: p.partyName,
partyCode: p.partyCode,
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,
units: p.balanceUnits,
purchaseDate: p.settlementDate,
@@ -340,8 +408,9 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
glName: p.investmentGlName,
crToAc: p.crGlCode,
brokerCode: '',
status: p.status as HoldingStatus,
status: (p.status === 'REDEEMED' ? 'MATURED' : p.status) as HoldingStatus,
settlementDate: p.settlementDate,
lastIntDate: p.lastIntDate,
glTransactionId: p.glTransactionId,
category: p.category,
bookValue: p.bookValue,
@@ -372,6 +441,142 @@ export interface InvestmentInterestRowDto {
}
/** 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. */
export interface InvestmentInterestPostingDto {
id: number;
purchaseId: number;
dealNo: string;
certNo: string;
bondType: string;
scriptDesc: string;
lineType: 'COUPON' | 'PROVISION';
intDate: string;
fromDate: string;
toDate: string;
grossInterest: number;
alreadyBooked: number;
amount: number;
drGlCode: string;
drGlName: string;
incomeGlCode: string;
incomeGlName: string;
glTransactionId: string;
voucherNo: string;
postedOnDate: string;
}
export interface InvestmentInterestDueDto {
purchaseId: number;
dealNo: string;