feat(investment): FD TDS fields, GOI interest to the paisa, compact home reminders, deal no in holdings and settle guard

This commit is contained in:
2026-10-20 16:16:12 +05:30
parent d584af8e54
commit 6890584b16
11 changed files with 316 additions and 82 deletions
+54 -34
View File
@@ -47,43 +47,63 @@
</div> </div>
</div> </div>
<!-- Pending investment interest vouchers (coupon / provision due now or within two days) --> <!-- Pending investment entries (GOI / bond interest, FDs with other banks): one line each, details on demand -->
@if (pendingInvestmentInterest.length) { @if (pendingInvestmentInterest.length || pendingFd.length) {
<a class="investment-interest-alert" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }"> <div class="investment-interest-alert">
<span class="iia-icon">🔔</span> <span class="iia-icon">🔔</span>
<span class="iia-text"> <div class="iia-text">
<strong>{{ pendingInvestmentInterest.length }} investment interest voucher(s) pending</strong> <strong>Investment entries pending</strong>
@for (t of pendingInvestmentInterest; track t.purchaseId) { @if (pendingInvestmentInterest.length) {
<span class="iia-item"> <div class="iia-row">
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} · <span class="iia-summary"
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} >GOI / Bonds: {{ pendingInvestmentInterest.length }} interest voucher(s){{
</span> overdueNote(pendingInvestmentInterest)
}}</span
>
<button type="button" class="iia-toggle" (click)="showInterestDetails = !showInterestDetails">
{{ showInterestDetails ? 'Hide' : 'Details' }}
</button>
<a class="iia-action" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }"
>Open Interest Entry →</a
>
</div>
@if (showInterestDetails) {
@for (t of pendingInvestmentInterest.slice(0, detailLimit); track t.purchaseId) {
<a class="iia-item" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }">
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</a>
}
@if (pendingInvestmentInterest.length > detailLimit) {
<span class="iia-more">+{{ pendingInvestmentInterest.length - detailLimit }} more in Interest Entry</span>
}
}
} }
</span> @if (pendingFd.length) {
<span class="iia-action">Open Interest Entry →</span> <div class="iia-row">
</a> <span class="iia-summary"
} >FD with banks: {{ pendingFd.length }} entry(ies){{ overdueNote(pendingFd) }}</span
>
<!-- FDs placed with other banks: interest vouchers and maturities due --> <button type="button" class="iia-toggle" (click)="showFdDetails = !showFdDetails">
@if (pendingFd.length) { {{ showFdDetails ? 'Hide' : 'Details' }}
<a </button>
class="investment-interest-alert" <a class="iia-action" routerLink="/investment/fd">Open FD Investment →</a>
routerLink="/investment/fd" </div>
[queryParams]="{ tab: 'interest', fd: pendingFd[0].fdId }" @if (showFdDetails) {
> @for (t of pendingFd.slice(0, detailLimit); track t.fdId + t.lineType + t.intDate) {
<span class="iia-icon">🔔</span> <a class="iia-item" routerLink="/investment/fd" [queryParams]="{ tab: 'interest', fd: t.fdId }">
<span class="iia-text"> {{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }}
<strong>{{ pendingFd.length }} FD investment entry(ies) pending</strong> · {{ t.partyName }} · FD {{ t.dealNo }} ·
@for (t of pendingFd; track t.fdId) { {{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
<span class="iia-item"> </a>
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }} · }
{{ t.partyName }} · FD {{ t.dealNo }} · @if (pendingFd.length > detailLimit) {
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} <span class="iia-more">+{{ pendingFd.length - detailLimit }} more in FD Investment</span>
</span> }
}
} }
</span> </div>
<span class="iia-action">Open FD Investment →</span> </div>
</a>
} }
<!-- Quick Action Cards --> <!-- Quick Action Cards -->
+40 -1
View File
@@ -447,16 +447,55 @@ $accent: #0ea5e9;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 4px;
min-width: 0;
}
.iia-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 12px;
font-size: 14px;
}
.iia-summary {
flex: 1;
min-width: 0;
}
.iia-toggle {
padding: 2px 10px;
border: 1px solid #e0a43a;
border-radius: 12px;
background: transparent;
color: inherit;
font-size: 12px;
cursor: pointer;
} }
.iia-item { .iia-item {
padding-left: 12px;
font-size: 13px; font-size: 13px;
color: inherit;
text-decoration: none;
}
.iia-more {
padding-left: 12px;
font-size: 12px;
font-style: italic;
}
a.iia-item:hover {
text-decoration: underline;
} }
.iia-action { .iia-action {
align-self: center; align-self: center;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
color: inherit;
text-decoration: none;
} }
} }
+15
View File
@@ -124,8 +124,23 @@ export class HomeComponent implements OnInit, AfterViewInit {
pendingInvestmentInterest: InvestmentInterestDueDto[] = []; pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
/** FDs placed with other banks: interest vouchers and maturities due now or within two days. */ /** FDs placed with other banks: interest vouchers and maturities due now or within two days. */
pendingFd: InvestmentFdDueDto[] = []; pendingFd: InvestmentFdDueDto[] = [];
/** The lists stay folded to one line each; the user opens them when needed. */
showInterestDetails = false;
showFdDetails = false;
readonly detailLimit = 5;
private readonly investmentService = inject(InvestmentService); private readonly investmentService = inject(InvestmentService);
/** " · 3 overdue, oldest 14-08-2025" for a list of due entries (dates DD-MM-YYYY); empty when none is overdue. */
overdueNote(items: Array<{ overdue?: boolean; intDate: string }>): string {
const late = items.filter((t) => t.overdue);
if (!late.length) {
return '';
}
const key = (d: string) => d.split('-').reverse().join('');
const oldest = late.map((t) => t.intDate).sort((a, b) => key(a).localeCompare(key(b)))[0];
return ` · ${late.length} overdue, oldest ${oldest}`;
}
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */ /** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
private loadPendingInvestmentInterest(): void { private loadPendingInvestmentInterest(): void {
this.investmentService.getDueInterest().subscribe({ this.investmentService.getDueInterest().subscribe({
@@ -131,7 +131,7 @@
<table class="pg-table"> <table class="pg-table">
<thead> <thead>
<tr> <tr>
<th>Cert No</th> <th>Deal No / Cert No</th>
<th>Script</th> <th>Script</th>
<th>Party</th> <th>Party</th>
<th class="num">Units</th> <th class="num">Units</th>
@@ -150,7 +150,8 @@
@for (h of holdingsForType; track h.certNo) { @for (h of holdingsForType; track h.certNo) {
<tr> <tr>
<td> <td>
<span class="pg-chip">{{ h.certNo }}</span> <span class="pg-chip">{{ h.dealNo }}</span>
<br /><small>Cert {{ h.certNo }}</small>
</td> </td>
<td>{{ h.scriptName }}</td> <td>{{ h.scriptName }}</td>
<td>{{ h.party }}</td> <td>{{ h.party }}</td>
@@ -169,8 +170,13 @@
</td> </td>
<td> <td>
@if (h.status === 'PENDING_SETTLEMENT') { @if (h.status === 'PENDING_SETTLEMENT') {
<button type="button" class="pg-btn pg-btn--primary" (click)="settleDeal(h)"> <button
Settle ({{ h.settlementDate }}) type="button"
class="pg-btn pg-btn--primary"
[disabled]="settlingDealId !== null"
(click)="settleDeal(h)"
>
{{ settlingDealId === h.id ? 'Settling…' : 'Settle (' + h.settlementDate + ')' }}
</button> </button>
} @else if (h.glTransactionId) { } @else if (h.glTransactionId) {
<span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span> <span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span>
@@ -357,7 +363,7 @@
@if (!isMutualFund) { @if (!isMutualFund) {
<label class="inv-form-field" <label class="inv-form-field"
><span>Premium (+) / Discount (−)</span ><span>Premium (+) / Discount (−)</span
><input type="text" [value]="'₹' + (purchasePremiumDiscount | number: '1.0-0')" readonly ><input type="text" [value]="'₹' + (purchasePremiumDiscount | number: '1.2-2')" readonly
/></label> /></label>
<label class="inv-form-field" <label class="inv-form-field"
><span>Accrued Interest ({{ purchaseAccruedDays }} days)</span ><span>Accrued Interest ({{ purchaseAccruedDays }} days)</span
@@ -908,16 +914,16 @@
<td class="num col-in">₹{{ r.amount | number: '1.2-2' }}</td> <td class="num col-in">₹{{ r.amount | number: '1.2-2' }}</td>
<td style="white-space: nowrap; font-size: 12px"> <td style="white-space: nowrap; font-size: 12px">
@if (r.type === 'COUPON') { @if (r.type === 'COUPON') {
<div>Dr Bank ₹{{ r.grossInterest | number: '1.0-0' }}</div> <div>Dr Bank ₹{{ r.grossInterest | number: '1.2-2' }}</div>
@if (r.alreadyBooked > 0) { @if (r.alreadyBooked > 0) {
<div> <div>
Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.0-0' }} Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.2-2' }}
</div> </div>
} }
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div> <div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.2-2' }}</div>
} @else { } @else {
<div>Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.0-0' }}</div> <div>Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.2-2' }}</div>
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}</div> <div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.2-2' }}</div>
} }
</td> </td>
<td style="white-space: nowrap"> <td style="white-space: nowrap">
@@ -1129,8 +1135,22 @@
<td class="num">₹{{ s.netAmount | number: '1.2-2' }}</td> <td class="num">₹{{ s.netAmount | number: '1.2-2' }}</td>
<td class="num">₹{{ s.profitLoss | number: '1.2-2' }}</td> <td class="num">₹{{ s.profitLoss | number: '1.2-2' }}</td>
<td style="white-space: nowrap"> <td style="white-space: nowrap">
<button type="button" class="pg-btn pg-btn--primary" (click)="settleSale(s)">Settle</button> <button
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropSale(s)">Drop</button> type="button"
class="pg-btn pg-btn--primary"
[disabled]="busySaleId !== null"
(click)="settleSale(s)"
>
{{ busySaleId === s.id ? 'Settling…' : 'Settle' }}
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="busySaleId !== null"
(click)="dropSale(s)"
>
Drop
</button>
</td> </td>
</tr> </tr>
} @empty { } @empty {
@@ -604,7 +604,7 @@ export class BondInvestmentComponent implements OnInit {
return this.purchaseBaseAmount; return this.purchaseBaseAmount;
} }
const rate = f.purchaseRate || 100; const rate = f.purchaseRate || 100;
return Math.round((this.purchaseBaseAmount * rate) / 100); return Math.round(this.purchaseBaseAmount * rate) / 100;
} }
/** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */ /** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */
@@ -627,7 +627,7 @@ export class BondInvestmentComponent implements OnInit {
get purchaseNextCouponAmount(): number { get purchaseNextCouponAmount(): number {
const f = this.purchaseForm; const f = this.purchaseForm;
return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 200) : 0; return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 2) / 100 : 0;
} }
get purchaseDaysToNextCoupon(): number { get purchaseDaysToNextCoupon(): number {
@@ -648,7 +648,7 @@ export class BondInvestmentComponent implements OnInit {
if (!f.intRate || !f.lastIntDt || !f.dealDate) { if (!f.intRate || !f.lastIntDt || !f.dealDate) {
return 0; return 0;
} }
return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * this.purchaseAccruedDays) / 360); return Math.round((this.purchaseBaseAmount * f.intRate * this.purchaseAccruedDays) / 360) / 100;
} }
get purchaseAccruedDays(): number { get purchaseAccruedDays(): number {
@@ -663,7 +663,7 @@ export class BondInvestmentComponent implements OnInit {
if (this.isMutualFund || !f.faceValue || !f.purchaseRate) { if (this.isMutualFund || !f.faceValue || !f.purchaseRate) {
return 0; return 0;
} }
return Math.round((f.faceValue * (f.purchaseRate - 100)) / 100); return Math.round(f.faceValue * (f.purchaseRate - 100)) / 100;
} }
/** Re-suggests the last coupon date when the deal date changes. */ /** Re-suggests the last coupon date when the deal date changes. */
@@ -1022,12 +1022,17 @@ export class BondInvestmentComponent implements OnInit {
} }
/** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */ /** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
settlingDealId: number | null = null;
settleDeal(h: InvestmentHolding): void { settleDeal(h: InvestmentHolding): void {
if (!h.id) { if (!h.id || this.settlingDealId !== null) {
return; return;
} }
this.settlingDealId = h.id;
this.investmentService.settlePurchase(h.id).subscribe({ this.investmentService.settlePurchase(h.id).subscribe({
next: (res) => { next: (res) => {
this.settlingDealId = null;
this.toast( this.toast(
res.rollbackTransaction res.rollbackTransaction
? `Settlement of deal ${h.dealNo} sent for approval (Checker Inbox).` ? `Settlement of deal ${h.dealNo} sent for approval (Checker Inbox).`
@@ -1035,7 +1040,10 @@ export class BondInvestmentComponent implements OnInit {
); );
this.loadPurchases(); this.loadPurchases();
}, },
error: (err) => this.toast(apiErrorMessage(err)) error: (err) => {
this.settlingDealId = null;
this.toast(apiErrorMessage(err));
}
}); });
} }
@@ -1345,29 +1353,44 @@ export class BondInvestmentComponent implements OnInit {
}); });
} }
/** The pending sale being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
busySaleId: number | null = null;
settleSale(s: InvestmentSaleDto): void { settleSale(s: InvestmentSaleDto): void {
if (!s.id) { if (!s.id || this.busySaleId !== null) {
return; return;
} }
this.busySaleId = s.id;
this.investmentService.settleSale(s.id).subscribe({ this.investmentService.settleSale(s.id).subscribe({
next: (res) => { next: (res) => {
this.busySaleId = null;
this.sales = this.sales.filter((x) => x.id !== s.id);
this.toast(`Sale of deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`); this.toast(`Sale of deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`);
this.loadPurchases(); this.loadPurchases();
}, },
error: (err) => this.toast(apiErrorMessage(err)) error: (err) => {
this.busySaleId = null;
this.toast(apiErrorMessage(err));
}
}); });
} }
dropSale(s: InvestmentSaleDto): void { dropSale(s: InvestmentSaleDto): void {
if (!s.id) { if (!s.id || this.busySaleId !== null) {
return; return;
} }
this.busySaleId = s.id;
this.investmentService.deleteSale(s.id).subscribe({ this.investmentService.deleteSale(s.id).subscribe({
next: () => { next: () => {
this.busySaleId = null;
this.sales = this.sales.filter((x) => x.id !== s.id);
this.toast(`Pending sale of deal ${s.dealNo} dropped.`); this.toast(`Pending sale of deal ${s.dealNo} dropped.`);
this.loadInterestLog(); this.loadInterestLog();
}, },
error: (err) => this.toast(apiErrorMessage(err)) error: (err) => {
this.busySaleId = null;
this.toast(apiErrorMessage(err));
}
}); });
} }
@@ -1723,7 +1746,7 @@ export class BondInvestmentComponent implements OnInit {
for (let k = 1; parseDDMMYYYY(addPeriod(h.lastIntDate, 0, 6 * k)) <= parseDDMMYYYY(asOn); k++) { for (let k = 1; parseDDMMYYYY(addPeriod(h.lastIntDate, 0, 6 * k)) <= parseDDMMYYYY(asOn); k++) {
lastCoupon = addPeriod(h.lastIntDate, 0, 6 * k); lastCoupon = addPeriod(h.lastIntDate, 0, 6 * k);
} }
return Math.round((h.faceValue * (h.interestRate / 100) * days360(lastCoupon, asOn)) / 360); return Math.round((h.faceValue * h.interestRate * days360(lastCoupon, asOn)) / 360) / 100;
} }
maturity(h: InvestmentHolding): number { maturity(h: InvestmentHolding): number {
@@ -1749,6 +1772,7 @@ export class BondInvestmentComponent implements OnInit {
title: `${this.instrumentLabel(this.bondType)} Holdings Register`, title: `${this.instrumentLabel(this.bondType)} Holdings Register`,
subtitle: `As on ${this.asOnDate}`, subtitle: `As on ${this.asOnDate}`,
headers: [ headers: [
'Deal No',
'Cert No', 'Cert No',
'Script', 'Script',
'Party', 'Party',
@@ -1762,7 +1786,8 @@ export class BondInvestmentComponent implements OnInit {
'Status' 'Status'
], ],
widths: [ widths: [
14, 16,
10,
26, 26,
22, 22,
12, 12,
@@ -1775,6 +1800,7 @@ export class BondInvestmentComponent implements OnInit {
12 12
], ],
rows: this.holdingsForType.map((h) => [ rows: this.holdingsForType.map((h) => [
h.dealNo ?? '',
h.certNo, h.certNo,
h.scriptName, h.scriptName,
h.party, h.party,
@@ -106,6 +106,9 @@
@if (f.closureTds) { @if (f.closureTds) {
· TDS ₹{{ f.closureTds | number: '1.2-2' }} · TDS ₹{{ f.closureTds | number: '1.2-2' }}
} }
@if (f.closurePenal) {
· penal ₹{{ f.closurePenal | number: '1.2-2' }}
}
</small> </small>
} }
@if (f.renewedFromDealNo) { @if (f.renewedFromDealNo) {
@@ -240,7 +243,14 @@
/></label> /></label>
<label class="inv-form-field" <label class="inv-form-field"
><span>Maturity Amount</span ><span>Maturity Amount</span
><input type="text" [value]="'₹' + (q.expectedMaturityAmount | number: '1.2-2')" readonly ><input
type="text"
[value]="
q.expectedMaturityAmount === null || q.expectedMaturityAmount === undefined
? ''
: '₹' + (q.expectedMaturityAmount | number: '1.2-2')
"
readonly
/></label> /></label>
@if (q.payout !== 'MATURITY') { @if (q.payout !== 'MATURITY') {
<label class="inv-form-field" <label class="inv-form-field"
@@ -416,12 +426,18 @@
<div class="inv-form-grid"> <div class="inv-form-grid">
<label class="inv-form-field" <label class="inv-form-field"
><span>Amount received from Bank (₹)</span ><span>Amount received from Bank (₹)</span
><input type="number" min="0" [(ngModel)]="payoutReceived" [placeholder]="r.grossInterest" ><input
/></label> type="number"
<label class="inv-form-field" min="0"
><span>TDS deducted (₹)</span [(ngModel)]="payoutReceived"
><input type="number" min="0" [(ngModel)]="payoutTds" placeholder="0" [placeholder]="r.grossInterest - (r.tds || 0)"
/></label> /></label>
@if (s.fd.tdsRate) {
<label class="inv-form-field"
><span>TDS deducted (₹) · auto at {{ s.fd.tdsRate }}%</span
><input type="number" min="0" [(ngModel)]="payoutTds" [placeholder]="r.tds || 0"
/></label>
}
<label class="inv-form-field"> <label class="inv-form-field">
<span>Received into</span> <span>Received into</span>
<select [(ngModel)]="payoutBankGlAccountId"> <select [(ngModel)]="payoutBankGlAccountId">
@@ -432,8 +448,13 @@
</label> </label>
</div> </div>
<small <small
>Leave the amount blank to take the system's interest. Voucher: Dr bank + Dr 100602 TDS / Cr >Leave the amount blank to take the system's interest.
100603 (provisioned) + Cr interest income (the rest).</small @if (s.fd.tdsRate) {
Leave TDS blank to work it out at {{ s.fd.tdsRate }}%; enter the bank's figure if it differs.
} @else {
{{ s.fd.partyName }} does not deduct TDS (Bank / Party Master).
}
Voucher: Dr bank + Dr 100602 TDS / Cr 100603 (provisioned) + Cr interest income (the rest).</small
> >
</td> </td>
</tr> </tr>
@@ -506,10 +527,17 @@
[placeholder]="closure.type === 'PREMATURE' ? '' : (closureQuote?.receivedAmount ?? '')" [placeholder]="closure.type === 'PREMATURE' ? '' : (closureQuote?.receivedAmount ?? '')"
(change)="refreshClosureQuote()" (change)="refreshClosureQuote()"
/></label> /></label>
<label class="inv-form-field" @if (closureFd?.tdsRate) {
><span>TDS deducted by Bank (₹)</span <label class="inv-form-field"
><input type="number" min="0" [(ngModel)]="closure.tds" placeholder="0" (change)="refreshClosureQuote()" ><span>TDS deducted by Bank (₹) · auto at {{ closureFd?.tdsRate }}%</span
/></label> ><input
type="number"
min="0"
[(ngModel)]="closure.tds"
[placeholder]="closureQuote?.tds ?? ''"
(change)="refreshClosureQuote()"
/></label>
}
@if (closure.type === 'PREMATURE') { @if (closure.type === 'PREMATURE') {
<label class="inv-form-field" <label class="inv-form-field"
><span>Penal Interest (₹)</span ><span>Penal Interest (₹)</span
@@ -519,7 +547,7 @@
@if (closure.type !== 'RENEWAL') { @if (closure.type !== 'RENEWAL') {
<label class="inv-form-field"> <label class="inv-form-field">
<span>Principal received into</span> <span>Principal received into</span>
<select [(ngModel)]="closure.principalBankGlAccountId"> <select [(ngModel)]="closure.principalBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
@for (g of bankGlOptions; track g.id) { @for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option> <option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
} }
@@ -527,7 +555,7 @@
</label> </label>
<label class="inv-form-field"> <label class="inv-form-field">
<span>Interest received into</span> <span>Interest received into</span>
<select [(ngModel)]="closure.interestBankGlAccountId"> <select [(ngModel)]="closure.interestBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
@for (g of bankGlOptions; track g.id) { @for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option> <option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
} }
@@ -601,7 +629,7 @@
}} }}
— {{ closureTopUp > 0 ? 'paid from' : 'credited to' }}</span — {{ closureTopUp > 0 ? 'paid from' : 'credited to' }}</span
> >
<select [(ngModel)]="closure.diffBankGlAccountId"> <select [(ngModel)]="closure.diffBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
<option [ngValue]="null">-- Select --</option> <option [ngValue]="null">-- Select --</option>
@for (g of bankGlOptions; track g.id) { @for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option> <option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
@@ -659,6 +687,12 @@
} }
</tbody> </tbody>
</table> </table>
@if (closure.type === 'RENEWAL' && closureTopUp !== 0 && !closure.diffBankGlAccountId) {
<p class="pg-sub" style="padding: 0 16px; color: #b42318">
Choose the bank for the {{ closureTopUp > 0 ? 'extra amount' : 'amount taken out' }} to complete the
voucher.
</p>
}
<p class="pg-sub" style="padding: 0 16px"> <p class="pg-sub" style="padding: 0 16px">
Voucher dated {{ q.closeDate }}. The lines follow the amounts entered above. Voucher dated {{ q.closeDate }}. The lines follow the amounts entered above.
</p> </p>
@@ -139,7 +139,7 @@ export class FdInvestmentComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.investmentService.getParties().subscribe({ this.investmentService.getParties().subscribe({
next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active)), next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active && !!p.interestGlAccountId)),
error: (err) => this.toast(`Could not load FD banks: ${apiErrorMessage(err)}`) error: (err) => this.toast(`Could not load FD banks: ${apiErrorMessage(err)}`)
}); });
this.investmentService.getAssetGlAccounts().subscribe({ this.investmentService.getAssetGlAccounts().subscribe({
@@ -162,9 +162,19 @@ export class FdInvestmentComponent implements OnInit {
this.activeTab = tab; this.activeTab = tab;
if (tab === 'purchase') { if (tab === 'purchase') {
this.refreshQuote(); this.refreshQuote();
} else if (tab === 'closure' && this.closure.fdId) {
// Interest may have been posted on the other tab meanwhile: work the closure out again.
this.refreshClosureQuote();
} else if (tab === 'interest' && this.interestFdId) {
this.loadSchedule();
} }
} }
/** A complete DD-MM-YYYY date: half-typed dates are not sent to the server. */
private static fullDate(d: string | null | undefined): boolean {
return !!d && /^\d{2}-\d{2}-\d{4}$/.test(d);
}
private loadFds(): void { private loadFds(): void {
this.investmentService.getFds().subscribe({ this.investmentService.getFds().subscribe({
next: (list) => (this.fds = list), next: (list) => (this.fds = list),
@@ -196,7 +206,7 @@ export class FdInvestmentComponent implements OnInit {
/** Period (years / months / days) sets the maturity date; the maturity date can also be typed directly. */ /** Period (years / months / days) sets the maturity date; the maturity date can also be typed directly. */
onPeriodChange(): void { onPeriodChange(): void {
const f = this.form; const f = this.form;
if (f.dealDate && (f.periodYears || f.periodMonths || f.periodDays)) { if (FdInvestmentComponent.fullDate(f.dealDate) && (f.periodYears || f.periodMonths || f.periodDays)) {
f.maturityDate = addPeriod(f.dealDate, f.periodYears ?? 0, f.periodMonths ?? 0, f.periodDays ?? 0); f.maturityDate = addPeriod(f.dealDate, f.periodYears ?? 0, f.periodMonths ?? 0, f.periodDays ?? 0);
} }
this.refreshQuote(); this.refreshQuote();
@@ -206,6 +216,9 @@ export class FdInvestmentComponent implements OnInit {
refreshQuote(): void { refreshQuote(): void {
const f = this.form; const f = this.form;
this.quoteError = ''; this.quoteError = '';
if (f.dealDate && !FdInvestmentComponent.fullDate(f.dealDate)) {
return;
}
const params: Record<string, string | number> = { const params: Record<string, string | number> = {
dealDate: f.dealDate || todayDDMMYYYY(), dealDate: f.dealDate || todayDDMMYYYY(),
payout: f.payout, payout: f.payout,
@@ -220,7 +233,7 @@ export class FdInvestmentComponent implements OnInit {
if (f.intRate) { if (f.intRate) {
params['intRate'] = f.intRate; params['intRate'] = f.intRate;
} }
if (f.maturityDate) { if (FdInvestmentComponent.fullDate(f.maturityDate)) {
params['maturityDate'] = f.maturityDate; params['maturityDate'] = f.maturityDate;
} }
this.investmentService.quoteFd(params).subscribe({ this.investmentService.quoteFd(params).subscribe({
@@ -315,6 +328,7 @@ export class FdInvestmentComponent implements OnInit {
} }
resetPurchase(): void { resetPurchase(): void {
this.quoteError = '';
this.form = this.blankForm(); this.form = this.blankForm();
this.refreshQuote(); this.refreshQuote();
} }
@@ -362,7 +376,8 @@ export class FdInvestmentComponent implements OnInit {
if (this.payoutReceived) { if (this.payoutReceived) {
body['receivedAmount'] = this.payoutReceived; body['receivedAmount'] = this.payoutReceived;
} }
if (this.payoutTds) { // Blank TDS: the system works it out at the bank's rate; an entered figure (even 0) is used as it is.
if (entered(this.payoutTds)) {
body['tds'] = this.payoutTds; body['tds'] = this.payoutTds;
} }
if (this.payoutBankGlAccountId) { if (this.payoutBankGlAccountId) {
@@ -434,14 +449,29 @@ export class FdInvestmentComponent implements OnInit {
this.closureQuote = null; this.closureQuote = null;
return; return;
} }
if (c.type === 'PREMATURE' && c.closeDate && !FdInvestmentComponent.fullDate(c.closeDate)) {
return;
}
const params: Record<string, string | number> = { type: c.type }; const params: Record<string, string | number> = { type: c.type };
if (c.type === 'PREMATURE' && c.closeDate) { if (c.type === 'PREMATURE' && c.closeDate) {
params['closeDate'] = c.closeDate; params['closeDate'] = c.closeDate;
} }
if (c.type === 'RENEWAL') {
if (c.diffBankGlAccountId) {
params['diffBankGlAccountId'] = c.diffBankGlAccountId;
}
} else {
if (c.principalBankGlAccountId) {
params['principalBankGlAccountId'] = c.principalBankGlAccountId;
}
if (c.interestBankGlAccountId) {
params['interestBankGlAccountId'] = c.interestBankGlAccountId;
}
}
if (c.receivedAmount) { if (c.receivedAmount) {
params['receivedAmount'] = c.receivedAmount; params['receivedAmount'] = c.receivedAmount;
} }
if (c.tds) { if (entered(c.tds)) {
params['tds'] = c.tds; params['tds'] = c.tds;
} }
if (c.type === 'RENEWAL' && c.renewalAmount) { if (c.type === 'RENEWAL' && c.renewalAmount) {
@@ -464,7 +494,7 @@ export class FdInvestmentComponent implements OnInit {
return; return;
} }
const base = { locale: 'en', dateFormat: 'dd-MM-yyyy', ...(c.remarks.trim() ? { remarks: c.remarks.trim() } : {}) }; const base = { locale: 'en', dateFormat: 'dd-MM-yyyy', ...(c.remarks.trim() ? { remarks: c.remarks.trim() } : {}) };
const tds = c.tds ? { tds: c.tds } : {}; const tds = entered(c.tds) ? { tds: c.tds } : {};
let body: object; let body: object;
if (c.type === 'RENEWAL') { if (c.type === 'RENEWAL') {
if (!c.newCertNo.trim() || !c.newIntRate || !c.newMaturityDate) { if (!c.newCertNo.trim() || !c.newIntRate || !c.newMaturityDate) {
@@ -587,3 +617,8 @@ export class FdInvestmentComponent implements OnInit {
this.toastTimer = setTimeout(() => (this.toastVisible = false), 4000); this.toastTimer = setTimeout(() => (this.toastVisible = false), 4000);
} }
} }
/** A number box the user filled in (0 counts; blank does not). */
function entered(v: number | null | undefined | string): boolean {
return v !== null && v !== undefined && v !== '';
}
+3
View File
@@ -1910,6 +1910,9 @@ export interface PartyMaster {
interestGlAccountId?: number; interestGlAccountId?: number;
interestGlCode?: string; interestGlCode?: string;
interestGlName?: string; interestGlName?: string;
/** Whether this bank deducts TDS from FD interest (some do, some do not), and at what rate (%). */
tdsApplicable?: boolean;
tdsRate?: number | null;
rbiCode?: string; rbiCode?: string;
/** Exposure limit for this party (₹). */ /** Exposure limit for this party (₹). */
limit?: number | null; limit?: number | null;
@@ -157,7 +157,7 @@
<span>* GL A/c No.</span> <span>* GL A/c No.</span>
<select [(ngModel)]="partyForm.glAccountId"> <select [(ngModel)]="partyForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option> <option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) { @for (g of partyGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option> <option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
} }
</select> </select>
@@ -172,6 +172,19 @@
} }
</select> </select>
</label> </label>
<label class="inv-form-field">
<span>Bank deducts TDS on FD interest?</span>
<select [ngModel]="partyForm.tdsApplicable ?? false" (ngModelChange)="partyForm.tdsApplicable = $event">
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
@if (partyForm.tdsApplicable) {
<label class="inv-form-field"
><span>* TDS rate (%)</span
><input type="number" min="0" max="99" step="0.01" [(ngModel)]="partyForm.tdsRate" placeholder="e.g. 10"
/></label>
}
} }
<label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label> <label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
<label class="inv-form-field" <label class="inv-form-field"
@@ -280,7 +293,7 @@
<span>* GL A/c No.</span> <span>* GL A/c No.</span>
<select [(ngModel)]="scriptForm.glAccountId"> <select [(ngModel)]="scriptForm.glAccountId">
<option [ngValue]="undefined">-- Select --</option> <option [ngValue]="undefined">-- Select --</option>
@for (g of investmentGlOptions; track g.id) { @for (g of scriptGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option> <option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
} }
</select> </select>
@@ -175,6 +175,22 @@ export class InvestmentMastersComponent implements OnInit {
); );
} }
/** FD GLs are the bank-wise children of 100704 (codes 1007040x). */
private static isFdGl(glCode: string): boolean {
return glCode.startsWith('1007040');
}
/** An FD bank is mapped to its own FD GL; any other party to a non-FD investment GL. */
get partyGlOptions(): InvestmentGlAccount[] {
const fd = this.partyForm.bondType === 'FD';
return this.investmentGlOptions.filter((g) => InvestmentMastersComponent.isFdGl(g.glCode) === fd);
}
/** Scripts are securities, never an FD with a bank. */
get scriptGlOptions(): InvestmentGlAccount[] {
return this.investmentGlOptions.filter((g) => !InvestmentMastersComponent.isFdGl(g.glCode));
}
// -------------------- Bank / Party Master -------------------- // -------------------- Bank / Party Master --------------------
/** Preview of the code the backend will assign; the saved code always comes from the backend. */ /** Preview of the code the backend will assign; the saved code always comes from the backend. */
get nextPartyCode(): string { get nextPartyCode(): string {
@@ -192,6 +208,11 @@ export class InvestmentMastersComponent implements OnInit {
this.toast('An FD bank needs its Interest GL as well.'); this.toast('An FD bank needs its Interest GL as well.');
return; return;
} }
const tds = f.bondType === 'FD' && !!f.tdsApplicable;
if (tds && !(Number(f.tdsRate) > 0 && Number(f.tdsRate) < 100)) {
this.toast('Enter the TDS rate (%) this bank deducts on FD interest.');
return;
}
const body = { const body = {
bondType: f.bondType, bondType: f.bondType,
partyName: f.partyName, partyName: f.partyName,
@@ -205,6 +226,8 @@ export class InvestmentMastersComponent implements OnInit {
bankType: f.bankType ?? '', bankType: f.bankType ?? '',
glAccountId: Number(f.glAccountId), glAccountId: Number(f.glAccountId),
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null, interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
tdsApplicable: tds,
tdsRate: tds ? Number(f.tdsRate) : null,
rbiCode: f.rbiCode ?? '', rbiCode: f.rbiCode ?? '',
limit: f.limit ?? null, limit: f.limit ?? null,
leiNo: f.leiNo ?? '', leiNo: f.leiNo ?? '',
+6
View File
@@ -42,6 +42,8 @@ export interface InvestmentPartyDto {
interestGlAccountId?: number; interestGlAccountId?: number;
interestGlCode?: string; interestGlCode?: string;
interestGlName?: string; interestGlName?: string;
tdsApplicable?: boolean;
tdsRate?: number;
rbiCode?: string; rbiCode?: string;
limit?: number; limit?: number;
leiNo?: string; leiNo?: string;
@@ -76,6 +78,8 @@ export type FdCalcType = 'SIMPLE' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'Y
export interface InvestmentFdDto { export interface InvestmentFdDto {
id?: number; id?: number;
dealNo?: string; dealNo?: string;
/** TDS rate the bank deducts on interest (from its FD party); absent when it does not deduct TDS. */
tdsRate?: number | null;
partyId?: number; partyId?: number;
partyCode?: string; partyCode?: string;
partyName?: string; partyName?: string;
@@ -542,6 +546,8 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
interestGlAccountId: p.interestGlAccountId, interestGlAccountId: p.interestGlAccountId,
interestGlCode: p.interestGlCode, interestGlCode: p.interestGlCode,
interestGlName: p.interestGlName, interestGlName: p.interestGlName,
tdsApplicable: p.tdsApplicable ?? false,
tdsRate: p.tdsRate ?? null,
rbiCode: p.rbiCode, rbiCode: p.rbiCode,
limit: p.limit ?? null, limit: p.limit ?? null,
leiNo: p.leiNo, leiNo: p.leiNo,