diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html
index 8e1c1c440..37c544297 100644
--- a/src/app/investment/bond-investment/bond-investment.component.html
+++ b/src/app/investment/bond-investment/bond-investment.component.html
@@ -853,7 +853,7 @@
|
{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}
- @if (r.due) {
+ @if (r.due && !r.posted) {
Due
}
|
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts
index 72bc94367..157970240 100644
--- a/src/app/investment/bond-investment/bond-investment.component.ts
+++ b/src/app/investment/bond-investment/bond-investment.component.ts
@@ -58,6 +58,7 @@ import { exportWorkbook } from '../investment-export';
import {
InvestmentDailyRateDto,
InvestmentInterestDueDto,
+ InvestmentInterestPostingDto,
InvestmentInterestRowDto,
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
@@ -243,11 +244,42 @@ export class BondInvestmentComponent implements OnInit {
this.holdings = purchases.map(toHolding);
this.txns = purchases.map((d) => this.purchaseTxn(d));
this.purchaseForm = this.blankPurchaseForm();
+ this.loadInterestLog();
},
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
});
}
+ /** Posted coupons / provisions join the purchases in the transaction log, latest date first. */
+ private loadInterestLog(): void {
+ this.investmentService.getInterestPostings().subscribe({
+ next: (postings) => {
+ const others = this.txns.filter((t) => !(t.txnType === 'INTEREST' && t.voucherNo.startsWith('JV/')));
+ this.txns = [
+ ...others,
+ ...postings.map((x) => this.interestTxn(x))
+ ].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
+ },
+ error: () => undefined
+ });
+ }
+
+ /** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
+ private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
+ const coupon = x.lineType === 'COUPON';
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: 'INTEREST',
+ voucherNo: x.voucherNo,
+ txnDate: x.intDate,
+ amount: coupon ? x.grossInterest : x.amount,
+ particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`,
+ glCode: x.drGlCode,
+ glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})`
+ };
+ }
+
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
const posted = !!d.glTransactionId;
@@ -1128,6 +1160,7 @@ export class BondInvestmentComponent implements OnInit {
);
this.retrieveInterestSchedule();
this.loadDueInterest();
+ this.loadInterestLog();
},
error: (err) => {
this.intPosting = false;
@@ -1531,8 +1564,20 @@ export class BondInvestmentComponent implements OnInit {
}
// -------------------- Helpers --------------------
+ /**
+ * Interest earned since the last coupon up to the as-on date (30/360), as the backend schedule counts it.
+ * Coupons fall every six months from the purchase's last interest date; a coupon received resets it to zero.
+ */
accrued(h: InvestmentHolding): number {
- return accruedInterest(h, this.asOnDate);
+ if (!h.lastIntDate || h.status !== 'ACTIVE') {
+ return h.lastIntDate ? 0 : accruedInterest(h, this.asOnDate);
+ }
+ const asOn = parseDDMMYYYY(this.asOnDate) > parseDDMMYYYY(h.maturityDate) ? h.maturityDate : this.asOnDate;
+ let lastCoupon = h.lastIntDate;
+ 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);
}
maturity(h: InvestmentHolding): number {
diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts
index cf69b9abe..34e830788 100644
--- a/src/app/investment/investment-data.ts
+++ b/src/app/investment/investment-data.ts
@@ -135,6 +135,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;
}
diff --git a/src/app/investment/investment-masters/investment-masters.component.ts b/src/app/investment/investment-masters/investment-masters.component.ts
index 19bf54027..b0b0fa5e8 100644
--- a/src/app/investment/investment-masters/investment-masters.component.ts
+++ b/src/app/investment/investment-masters/investment-masters.component.ts
@@ -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 = {};
newBroker: Partial = {};
@@ -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();
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
index 076b8f8c6..2ff933c63 100644
--- a/src/app/investment/investment.service.ts
+++ b/src/app/investment/investment.service.ts
@@ -175,6 +175,11 @@ export class InvestmentService {
}
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
+ /** Interest lines already posted (coupons and provisions), newest first. */
+ getInterestPostings(): Observable {
+ return this.http.get('/investment/interest-postings');
+ }
+
/** 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');
@@ -342,6 +347,7 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
brokerCode: '',
status: p.status as HoldingStatus,
settlementDate: p.settlementDate,
+ lastIntDate: p.lastIntDate,
glTransactionId: p.glTransactionId,
category: p.category,
bookValue: p.bookValue,
@@ -372,6 +378,30 @@ export interface InvestmentInterestRowDto {
}
/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */
+/** 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;