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>
<!-- Pending investment interest vouchers (coupon / provision due now or within two days) -->
@if (pendingInvestmentInterest.length) {
<a class="investment-interest-alert" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }">
<!-- Pending investment entries (GOI / bond interest, FDs with other banks): one line each, details on demand -->
@if (pendingInvestmentInterest.length || pendingFd.length) {
<div class="investment-interest-alert">
<span class="iia-icon">🔔</span>
<span class="iia-text">
<strong>{{ pendingInvestmentInterest.length }} investment interest voucher(s) pending</strong>
@for (t of pendingInvestmentInterest; track t.purchaseId) {
<span class="iia-item">
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</span>
<div class="iia-text">
<strong>Investment entries pending</strong>
@if (pendingInvestmentInterest.length) {
<div class="iia-row">
<span class="iia-summary"
>GOI / Bonds: {{ pendingInvestmentInterest.length }} interest voucher(s){{
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>
<span class="iia-action">Open Interest Entry →</span>
</a>
}
<!-- FDs placed with other banks: interest vouchers and maturities due -->
@if (pendingFd.length) {
<a
class="investment-interest-alert"
routerLink="/investment/fd"
[queryParams]="{ tab: 'interest', fd: pendingFd[0].fdId }"
>
<span class="iia-icon">🔔</span>
<span class="iia-text">
<strong>{{ pendingFd.length }} FD investment entry(ies) pending</strong>
@for (t of pendingFd; track t.fdId) {
<span class="iia-item">
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }} ·
{{ t.partyName }} · FD {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</span>
@if (pendingFd.length) {
<div class="iia-row">
<span class="iia-summary"
>FD with banks: {{ pendingFd.length }} entry(ies){{ overdueNote(pendingFd) }}</span
>
<button type="button" class="iia-toggle" (click)="showFdDetails = !showFdDetails">
{{ showFdDetails ? 'Hide' : 'Details' }}
</button>
<a class="iia-action" routerLink="/investment/fd">Open FD Investment →</a>
</div>
@if (showFdDetails) {
@for (t of pendingFd.slice(0, detailLimit); track t.fdId + t.lineType + t.intDate) {
<a class="iia-item" routerLink="/investment/fd" [queryParams]="{ tab: 'interest', fd: t.fdId }">
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }}
· {{ t.partyName }} · FD {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</a>
}
@if (pendingFd.length > detailLimit) {
<span class="iia-more">+{{ pendingFd.length - detailLimit }} more in FD Investment</span>
}
}
}
</span>
<span class="iia-action">Open FD Investment →</span>
</a>
</div>
</div>
}
<!-- Quick Action Cards -->
+40 -1
View File
@@ -447,16 +447,55 @@ $accent: #0ea5e9;
display: flex;
flex: 1;
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 {
padding-left: 12px;
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 {
align-self: center;
font-weight: 600;
white-space: nowrap;
color: inherit;
text-decoration: none;
}
}
+15
View File
@@ -124,8 +124,23 @@ export class HomeComponent implements OnInit, AfterViewInit {
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
/** FDs placed with other banks: interest vouchers and maturities due now or within two days. */
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);
/** " · 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. */
private loadPendingInvestmentInterest(): void {
this.investmentService.getDueInterest().subscribe({
@@ -131,7 +131,7 @@
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Deal No / Cert No</th>
<th>Script</th>
<th>Party</th>
<th class="num">Units</th>
@@ -150,7 +150,8 @@
@for (h of holdingsForType; track h.certNo) {
<tr>
<td>
<span class="pg-chip">{{ h.certNo }}</span>
<span class="pg-chip">{{ h.dealNo }}</span>
<br /><small>Cert {{ h.certNo }}</small>
</td>
<td>{{ h.scriptName }}</td>
<td>{{ h.party }}</td>
@@ -169,8 +170,13 @@
</td>
<td>
@if (h.status === 'PENDING_SETTLEMENT') {
<button type="button" class="pg-btn pg-btn--primary" (click)="settleDeal(h)">
Settle ({{ h.settlementDate }})
<button
type="button"
class="pg-btn pg-btn--primary"
[disabled]="settlingDealId !== null"
(click)="settleDeal(h)"
>
{{ settlingDealId === h.id ? 'Settling…' : 'Settle (' + h.settlementDate + ')' }}
</button>
} @else if (h.glTransactionId) {
<span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span>
@@ -357,7 +363,7 @@
@if (!isMutualFund) {
<label class="inv-form-field"
><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 class="inv-form-field"
><span>Accrued Interest ({{ purchaseAccruedDays }} days)</span
@@ -908,16 +914,16 @@
<td class="num col-in">₹{{ r.amount | number: '1.2-2' }}</td>
<td style="white-space: nowrap; font-size: 12px">
@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) {
<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>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 {
<div>Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.0-0' }}</div>
<div>Cr {{ sc.interestGlCode }} Int. Income ₹{{ 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.2-2' }}</div>
}
</td>
<td style="white-space: nowrap">
@@ -1129,8 +1135,22 @@
<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>
<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>
</tr>
} @empty {
@@ -604,7 +604,7 @@ export class BondInvestmentComponent implements OnInit {
return this.purchaseBaseAmount;
}
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. */
@@ -627,7 +627,7 @@ export class BondInvestmentComponent implements OnInit {
get purchaseNextCouponAmount(): number {
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 {
@@ -648,7 +648,7 @@ export class BondInvestmentComponent implements OnInit {
if (!f.intRate || !f.lastIntDt || !f.dealDate) {
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 {
@@ -663,7 +663,7 @@ export class BondInvestmentComponent implements OnInit {
if (this.isMutualFund || !f.faceValue || !f.purchaseRate) {
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. */
@@ -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. */
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
settlingDealId: number | null = null;
settleDeal(h: InvestmentHolding): void {
if (!h.id) {
if (!h.id || this.settlingDealId !== null) {
return;
}
this.settlingDealId = h.id;
this.investmentService.settlePurchase(h.id).subscribe({
next: (res) => {
this.settlingDealId = null;
this.toast(
res.rollbackTransaction
? `Settlement of deal ${h.dealNo} sent for approval (Checker Inbox).`
@@ -1035,7 +1040,10 @@ export class BondInvestmentComponent implements OnInit {
);
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 {
if (!s.id) {
if (!s.id || this.busySaleId !== null) {
return;
}
this.busySaleId = s.id;
this.investmentService.settleSale(s.id).subscribe({
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.loadPurchases();
},
error: (err) => this.toast(apiErrorMessage(err))
error: (err) => {
this.busySaleId = null;
this.toast(apiErrorMessage(err));
}
});
}
dropSale(s: InvestmentSaleDto): void {
if (!s.id) {
if (!s.id || this.busySaleId !== null) {
return;
}
this.busySaleId = s.id;
this.investmentService.deleteSale(s.id).subscribe({
next: () => {
this.busySaleId = null;
this.sales = this.sales.filter((x) => x.id !== s.id);
this.toast(`Pending sale of deal ${s.dealNo} dropped.`);
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++) {
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 {
@@ -1749,6 +1772,7 @@ export class BondInvestmentComponent implements OnInit {
title: `${this.instrumentLabel(this.bondType)} Holdings Register`,
subtitle: `As on ${this.asOnDate}`,
headers: [
'Deal No',
'Cert No',
'Script',
'Party',
@@ -1762,7 +1786,8 @@ export class BondInvestmentComponent implements OnInit {
'Status'
],
widths: [
14,
16,
10,
26,
22,
12,
@@ -1775,6 +1800,7 @@ export class BondInvestmentComponent implements OnInit {
12
],
rows: this.holdingsForType.map((h) => [
h.dealNo ?? '',
h.certNo,
h.scriptName,
h.party,
@@ -106,6 +106,9 @@
@if (f.closureTds) {
· TDS ₹{{ f.closureTds | number: '1.2-2' }}
}
@if (f.closurePenal) {
· penal ₹{{ f.closurePenal | number: '1.2-2' }}
}
</small>
}
@if (f.renewedFromDealNo) {
@@ -240,7 +243,14 @@
/></label>
<label class="inv-form-field"
><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>
@if (q.payout !== 'MATURITY') {
<label class="inv-form-field"
@@ -416,12 +426,18 @@
<div class="inv-form-grid">
<label class="inv-form-field"
><span>Amount received from Bank (₹)</span
><input type="number" min="0" [(ngModel)]="payoutReceived" [placeholder]="r.grossInterest"
/></label>
<label class="inv-form-field"
><span>TDS deducted (₹)</span
><input type="number" min="0" [(ngModel)]="payoutTds" placeholder="0"
><input
type="number"
min="0"
[(ngModel)]="payoutReceived"
[placeholder]="r.grossInterest - (r.tds || 0)"
/></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">
<span>Received into</span>
<select [(ngModel)]="payoutBankGlAccountId">
@@ -432,8 +448,13 @@
</label>
</div>
<small
>Leave the amount blank to take the system's interest. Voucher: Dr bank + Dr 100602 TDS / Cr
100603 (provisioned) + Cr interest income (the rest).</small
>Leave the amount blank to take the system's interest.
@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>
</tr>
@@ -506,10 +527,17 @@
[placeholder]="closure.type === 'PREMATURE' ? '' : (closureQuote?.receivedAmount ?? '')"
(change)="refreshClosureQuote()"
/></label>
<label class="inv-form-field"
><span>TDS deducted by Bank (₹)</span
><input type="number" min="0" [(ngModel)]="closure.tds" placeholder="0" (change)="refreshClosureQuote()"
/></label>
@if (closureFd?.tdsRate) {
<label class="inv-form-field"
><span>TDS deducted by Bank (₹) · auto at {{ closureFd?.tdsRate }}%</span
><input
type="number"
min="0"
[(ngModel)]="closure.tds"
[placeholder]="closureQuote?.tds ?? ''"
(change)="refreshClosureQuote()"
/></label>
}
@if (closure.type === 'PREMATURE') {
<label class="inv-form-field"
><span>Penal Interest (₹)</span
@@ -519,7 +547,7 @@
@if (closure.type !== 'RENEWAL') {
<label class="inv-form-field">
<span>Principal received into</span>
<select [(ngModel)]="closure.principalBankGlAccountId">
<select [(ngModel)]="closure.principalBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
@for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
@@ -527,7 +555,7 @@
</label>
<label class="inv-form-field">
<span>Interest received into</span>
<select [(ngModel)]="closure.interestBankGlAccountId">
<select [(ngModel)]="closure.interestBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
@for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
@@ -601,7 +629,7 @@
}}
— {{ closureTopUp > 0 ? 'paid from' : 'credited to' }}</span
>
<select [(ngModel)]="closure.diffBankGlAccountId">
<select [(ngModel)]="closure.diffBankGlAccountId" (ngModelChange)="refreshClosureQuote()">
<option [ngValue]="null">-- Select --</option>
@for (g of bankGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
@@ -659,6 +687,12 @@
}
</tbody>
</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">
Voucher dated {{ q.closeDate }}. The lines follow the amounts entered above.
</p>
@@ -139,7 +139,7 @@ export class FdInvestmentComponent implements OnInit {
ngOnInit(): void {
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)}`)
});
this.investmentService.getAssetGlAccounts().subscribe({
@@ -162,9 +162,19 @@ export class FdInvestmentComponent implements OnInit {
this.activeTab = tab;
if (tab === 'purchase') {
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 {
this.investmentService.getFds().subscribe({
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. */
onPeriodChange(): void {
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);
}
this.refreshQuote();
@@ -206,6 +216,9 @@ export class FdInvestmentComponent implements OnInit {
refreshQuote(): void {
const f = this.form;
this.quoteError = '';
if (f.dealDate && !FdInvestmentComponent.fullDate(f.dealDate)) {
return;
}
const params: Record<string, string | number> = {
dealDate: f.dealDate || todayDDMMYYYY(),
payout: f.payout,
@@ -220,7 +233,7 @@ export class FdInvestmentComponent implements OnInit {
if (f.intRate) {
params['intRate'] = f.intRate;
}
if (f.maturityDate) {
if (FdInvestmentComponent.fullDate(f.maturityDate)) {
params['maturityDate'] = f.maturityDate;
}
this.investmentService.quoteFd(params).subscribe({
@@ -315,6 +328,7 @@ export class FdInvestmentComponent implements OnInit {
}
resetPurchase(): void {
this.quoteError = '';
this.form = this.blankForm();
this.refreshQuote();
}
@@ -362,7 +376,8 @@ export class FdInvestmentComponent implements OnInit {
if (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;
}
if (this.payoutBankGlAccountId) {
@@ -434,14 +449,29 @@ export class FdInvestmentComponent implements OnInit {
this.closureQuote = null;
return;
}
if (c.type === 'PREMATURE' && c.closeDate && !FdInvestmentComponent.fullDate(c.closeDate)) {
return;
}
const params: Record<string, string | number> = { type: c.type };
if (c.type === 'PREMATURE' && 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) {
params['receivedAmount'] = c.receivedAmount;
}
if (c.tds) {
if (entered(c.tds)) {
params['tds'] = c.tds;
}
if (c.type === 'RENEWAL' && c.renewalAmount) {
@@ -464,7 +494,7 @@ export class FdInvestmentComponent implements OnInit {
return;
}
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;
if (c.type === 'RENEWAL') {
if (!c.newCertNo.trim() || !c.newIntRate || !c.newMaturityDate) {
@@ -587,3 +617,8 @@ export class FdInvestmentComponent implements OnInit {
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;
interestGlCode?: 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;
/** Exposure limit for this party (₹). */
limit?: number | null;
@@ -157,7 +157,7 @@
<span>* GL A/c No.</span>
<select [(ngModel)]="partyForm.glAccountId">
<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>
}
</select>
@@ -172,6 +172,19 @@
}
</select>
</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"
@@ -280,7 +293,7 @@
<span>* GL A/c No.</span>
<select [(ngModel)]="scriptForm.glAccountId">
<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>
}
</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 --------------------
/** Preview of the code the backend will assign; the saved code always comes from the backend. */
get nextPartyCode(): string {
@@ -192,6 +208,11 @@ export class InvestmentMastersComponent implements OnInit {
this.toast('An FD bank needs its Interest GL as well.');
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 = {
bondType: f.bondType,
partyName: f.partyName,
@@ -205,6 +226,8 @@ export class InvestmentMastersComponent implements OnInit {
bankType: f.bankType ?? '',
glAccountId: Number(f.glAccountId),
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
tdsApplicable: tds,
tdsRate: tds ? Number(f.tdsRate) : null,
rbiCode: f.rbiCode ?? '',
limit: f.limit ?? null,
leiNo: f.leiNo ?? '',
+6
View File
@@ -42,6 +42,8 @@ export interface InvestmentPartyDto {
interestGlAccountId?: number;
interestGlCode?: string;
interestGlName?: string;
tdsApplicable?: boolean;
tdsRate?: number;
rbiCode?: string;
limit?: number;
leiNo?: string;
@@ -76,6 +78,8 @@ export type FdCalcType = 'SIMPLE' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'Y
export interface InvestmentFdDto {
id?: number;
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;
partyCode?: string;
partyName?: string;
@@ -542,6 +546,8 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
interestGlAccountId: p.interestGlAccountId,
interestGlCode: p.interestGlCode,
interestGlName: p.interestGlName,
tdsApplicable: p.tdsApplicable ?? false,
tdsRate: p.tdsRate ?? null,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,