Interest Entry — coupon and 31 Mar / 30 Sep provision schedule
+ @if (intDueTasks.length) {
+
🔔 Interest entries due ({{ intDueTasks.length }})
+
+
+
+ | Deal No |
+ Script |
+ Type |
+ Int Date |
+ Amount |
+ Status |
+ |
+
+
+
+ @for (t of intDueTasks; track t.purchaseId) {
+
+ |
+ {{ t.dealNo }}
+ |
+ {{ t.scriptCode }} — {{ t.scriptDesc }} |
+ {{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} |
+ {{ t.intDate }} |
+ ₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }} |
+ {{ t.overdue ? 'Overdue — post now' : t.postable ? 'Due today' : 'Due on ' + t.intDate }} |
+
+
+ |
+
+ }
+
+
+ }
- Interest Schedule ({{ sc.rows.length }} lines) — due up to {{ sc.asOnDate }}: ₹{{
- intDueTotal | number: '1.2-2'
- }}
+ Interest Schedule ({{ intVisibleRows.length }} of {{ sc.rows.length }} lines) — due up to {{ sc.asOnDate }}:
+ ₹{{ intDueTotal | number: '1.2-2' }}
Days are 30/360. A provision books the interest earned since the last coupon; the coupon then clears what is
- already booked. Display only — nothing is posted to GL yet.
+ already booked. Post each line with Generate Voucher on or after its date, in date order — the JV voucher is
+ passed as it is raised.
+
+
+
+
@@ -796,10 +845,11 @@
| Already Booked |
Amount |
Accounting |
+ Voucher |
- @for (r of sc.rows; track r.intDate) {
+ @for (r of intVisibleRows; track r.type + r.intDate) {
|
{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}
@@ -829,10 +879,26 @@
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}
}
|
+
+ @if (r.posted) {
+ Posted · {{ r.voucherNo }}
+ } @else if (r.postable) {
+
+ } @else if (r.dueSoon) {
+ 🔔 Due {{ r.intDate }}
+ }
+ |
} @empty {
- |
+ |
No interest lines — check the rate, last interest date and maturity date.
|
@@ -840,8 +906,8 @@
- Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }}. Posting to GL will be enabled after
- approval.
+ Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }} · Interest receivable GL:
+ {{ interestReceivableGl.glCode }}.
} @else {
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts
index dbdee702a..72bc94367 100644
--- a/src/app/investment/bond-investment/bond-investment.component.ts
+++ b/src/app/investment/bond-investment/bond-investment.component.ts
@@ -9,6 +9,7 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
SCRIPT_MASTER,
@@ -56,6 +57,8 @@ import {
import { exportWorkbook } from '../investment-export';
import {
InvestmentDailyRateDto,
+ InvestmentInterestDueDto,
+ InvestmentInterestRowDto,
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
InvestmentService,
@@ -205,9 +208,14 @@ interface BondSellForm {
})
export class BondInvestmentComponent implements OnInit {
private readonly investmentService = inject(InvestmentService);
+ private readonly route = inject(ActivatedRoute);
/** Script and party dropdowns offer the masters saved in the backend. */
ngOnInit(): void {
+ // The home-screen interest alert links here with ?tab=interest.
+ if (this.route.snapshot.queryParamMap.get('tab') === 'interest') {
+ this.activeTab = 'interest';
+ }
this.investmentService.syncSharedMasters().subscribe({
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
});
@@ -222,6 +230,7 @@ export class BondInvestmentComponent implements OnInit {
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
this.loadPurchases();
+ this.loadDueInterest();
}
/** GL id for a GL code — the purchase API takes ids, the form works with codes. */
@@ -307,6 +316,13 @@ export class BondInvestmentComponent implements OnInit {
intAsOnDate = this.asOnDate;
intSchedule: InvestmentInterestScheduleDto | null = null;
intLoading = false;
+ /** Interest lines of all holdings due now or within two days — what Jitendra Sir asked to be reminded of. */
+ intDueTasks: InvestmentInterestDueDto[] = [];
+ /** By default the schedule shows only lines up to the end of next year (and anything already posted). */
+ intShowFullSchedule = false;
+ /** Bank a coupon is received into; defaults to the purchase's paying bank. */
+ intBankGlCode = '';
+ intPosting = false;
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
// Write-off
@@ -1052,6 +1068,7 @@ export class BondInvestmentComponent implements OnInit {
this.investmentService.getInterestSchedule(this.intPurchaseId, this.intAsOnDate || undefined).subscribe({
next: (schedule) => {
this.intSchedule = schedule;
+ this.intBankGlCode = schedule.bankGlCode;
this.intLoading = false;
},
error: (err) => {
@@ -1062,6 +1079,63 @@ export class BondInvestmentComponent implements OnInit {
});
}
+ loadDueInterest(): void {
+ this.investmentService.getDueInterest().subscribe({
+ next: (tasks) => (this.intDueTasks = tasks),
+ error: () => (this.intDueTasks = [])
+ });
+ }
+
+ /** Opens a due task's holding in the schedule below. */
+ openDueTask(t: InvestmentInterestDueDto): void {
+ this.intType = '';
+ this.intScript = '';
+ this.intPurchaseId = t.purchaseId;
+ this.retrieveInterestSchedule();
+ }
+
+ /** Lines shown: up to 31 December of next year, plus anything already posted — unless the full schedule is asked for. */
+ get intVisibleRows(): InvestmentInterestRowDto[] {
+ const rows = this.intSchedule?.rows ?? [];
+ if (this.intShowFullSchedule) {
+ return rows;
+ }
+ const upTo = new Date(new Date().getFullYear() + 1, 11, 31);
+ return rows.filter((r) => r.posted || parseDDMMYYYY(r.intDate) <= upTo);
+ }
+
+ /** Posts one interest line with its JV voucher (passed as it is raised), then refreshes the schedule and tasks. */
+ generateVoucher(r: InvestmentInterestRowDto): void {
+ const sc = this.intSchedule;
+ if (!sc || this.intPosting) {
+ return;
+ }
+ const bankGlAccountId =
+ r.type === 'COUPON' ? (this.glIdByCode.get(this.intBankGlCode) ?? sc.bankGlAccountId) : undefined;
+ const body = {
+ lineType: r.type,
+ intDate: r.intDate,
+ ...(bankGlAccountId ? { bankGlAccountId } : {}),
+ dateFormat: 'dd-MM-yyyy',
+ locale: 'en'
+ };
+ this.intPosting = true;
+ this.investmentService.postInterest(sc.purchaseId, body).subscribe({
+ next: (res) => {
+ this.intPosting = false;
+ this.toast(
+ `${r.type === 'COUPON' ? 'Coupon' : 'Provision'} of ${r.intDate} posted — voucher ${res.resourceIdentifier ?? ''}.`
+ );
+ this.retrieveInterestSchedule();
+ this.loadDueInterest();
+ },
+ error: (err) => {
+ this.intPosting = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
/** Total of the lines falling due up to the as-on date. */
get intDueTotal(): number {
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
index b9723b0e4..076b8f8c6 100644
--- a/src/app/investment/investment.service.ts
+++ b/src/app/investment/investment.service.ts
@@ -175,6 +175,19 @@ export class InvestmentService {
}
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
+ /** Interest lines of all holdings that are due now or within the next few days. */
+ getDueInterest(): Observable {
+ return this.http.get('/investment/interest-postings/due');
+ }
+
+ /** Posts one interest line (coupon / provision) with its JV voucher; the voucher number comes back as resourceIdentifier. */
+ postInterest(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string; transactionId?: string }> {
+ return this.http.post<{ resourceIdentifier?: string; transactionId?: string }>(
+ `/investment/purchases/${purchaseId}/interest-postings`,
+ body
+ );
+ }
+
getInterestSchedule(purchaseId: number, asOnDate?: string): Observable {
const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : '';
return this.http.get(
@@ -347,6 +360,30 @@ export interface InvestmentInterestRowDto {
alreadyBooked: number;
amount: number;
due: boolean;
+ /** Posted to GL: its JV voucher and GL transaction. */
+ posted: boolean;
+ voucherNo?: string;
+ glTransactionId?: string;
+ postedOnDate?: string;
+ /** The next line to post and its date has come. */
+ postable: boolean;
+ /** Unposted and due within the reminder window (or past). */
+ dueSoon: boolean;
+}
+
+/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
+export interface InvestmentInterestDueDto {
+ purchaseId: number;
+ dealNo: string;
+ certNo: string;
+ scriptCode: string;
+ scriptDesc: string;
+ lineType: 'COUPON' | 'PROVISION';
+ intDate: string;
+ grossInterest: number;
+ amount: number;
+ postable: boolean;
+ overdue: boolean;
}
export interface InvestmentInterestScheduleDto {
@@ -369,6 +406,10 @@ export interface InvestmentInterestScheduleDto {
asOnDate: string;
interestGlCode: string;
interestGlName: string;
+ bankGlAccountId: number;
+ bankGlCode: string;
+ bankGlName: string;
+ status: string;
rows: InvestmentInterestRowDto[];
}