feat(investment): interest in transaction log, accrued interest since last coupon, investment-only GL list
This commit is contained in:
@@ -853,7 +853,7 @@
|
|||||||
<tr [style.opacity]="r.due ? 1 : 0.6">
|
<tr [style.opacity]="r.due ? 1 : 0.6">
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}</span>
|
<span class="pg-chip">{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }}</span>
|
||||||
@if (r.due) {
|
@if (r.due && !r.posted) {
|
||||||
<span class="pg-chip">Due</span>
|
<span class="pg-chip">Due</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { exportWorkbook } from '../investment-export';
|
|||||||
import {
|
import {
|
||||||
InvestmentDailyRateDto,
|
InvestmentDailyRateDto,
|
||||||
InvestmentInterestDueDto,
|
InvestmentInterestDueDto,
|
||||||
|
InvestmentInterestPostingDto,
|
||||||
InvestmentInterestRowDto,
|
InvestmentInterestRowDto,
|
||||||
InvestmentInterestScheduleDto,
|
InvestmentInterestScheduleDto,
|
||||||
InvestmentPurchaseDto,
|
InvestmentPurchaseDto,
|
||||||
@@ -243,11 +244,42 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
this.holdings = purchases.map(toHolding);
|
this.holdings = purchases.map(toHolding);
|
||||||
this.txns = purchases.map((d) => this.purchaseTxn(d));
|
this.txns = purchases.map((d) => this.purchaseTxn(d));
|
||||||
this.purchaseForm = this.blankPurchaseForm();
|
this.purchaseForm = this.blankPurchaseForm();
|
||||||
|
this.loadInterestLog();
|
||||||
},
|
},
|
||||||
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
|
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. */
|
/** A purchase reaches the GL only when it is settled, dated the settlement date. */
|
||||||
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
|
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
|
||||||
const posted = !!d.glTransactionId;
|
const posted = !!d.glTransactionId;
|
||||||
@@ -1128,6 +1160,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
this.retrieveInterestSchedule();
|
this.retrieveInterestSchedule();
|
||||||
this.loadDueInterest();
|
this.loadDueInterest();
|
||||||
|
this.loadInterestLog();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.intPosting = false;
|
this.intPosting = false;
|
||||||
@@ -1531,8 +1564,20 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Helpers --------------------
|
// -------------------- 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 {
|
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 {
|
maturity(h: InvestmentHolding): number {
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ export interface InvestmentHolding {
|
|||||||
settlementAmount?: number;
|
settlementAmount?: number;
|
||||||
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
|
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
|
||||||
settlementDate?: string;
|
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. */
|
/** Journal entry transaction posted when the deal was settled. */
|
||||||
glTransactionId?: string;
|
glTransactionId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ type MasterTab =
|
|||||||
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
|
* 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.
|
* 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({
|
@Component({
|
||||||
selector: 'mifosx-investment-masters',
|
selector: 'mifosx-investment-masters',
|
||||||
templateUrl: './investment-masters.component.html',
|
templateUrl: './investment-masters.component.html',
|
||||||
@@ -120,7 +124,7 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
saving = false;
|
saving = false;
|
||||||
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
||||||
readonly slrTypeOptions = SLR_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[] = [];
|
investmentGlOptions: InvestmentGlAccount[] = [];
|
||||||
newSubType: Partial<SubTypeMaster> = {};
|
newSubType: Partial<SubTypeMaster> = {};
|
||||||
newBroker: Partial<BrokerMaster> = {};
|
newBroker: Partial<BrokerMaster> = {};
|
||||||
@@ -133,7 +137,10 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadDailyRates();
|
this.loadDailyRates();
|
||||||
this.investmentService.getAssetGlAccounts().subscribe({
|
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)}`)
|
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
||||||
});
|
});
|
||||||
this.reloadMasters();
|
this.reloadMasters();
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ export class InvestmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */
|
/** 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<InvestmentInterestPostingDto[]> {
|
||||||
|
return this.http.get<InvestmentInterestPostingDto[]>('/investment/interest-postings');
|
||||||
|
}
|
||||||
|
|
||||||
/** Interest lines of all holdings that are due now or within the next few days. */
|
/** Interest lines of all holdings that are due now or within the next few days. */
|
||||||
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
|
getDueInterest(): Observable<InvestmentInterestDueDto[]> {
|
||||||
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
|
return this.http.get<InvestmentInterestDueDto[]>('/investment/interest-postings/due');
|
||||||
@@ -342,6 +347,7 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
|
|||||||
brokerCode: '',
|
brokerCode: '',
|
||||||
status: p.status as HoldingStatus,
|
status: p.status as HoldingStatus,
|
||||||
settlementDate: p.settlementDate,
|
settlementDate: p.settlementDate,
|
||||||
|
lastIntDate: p.lastIntDate,
|
||||||
glTransactionId: p.glTransactionId,
|
glTransactionId: p.glTransactionId,
|
||||||
category: p.category,
|
category: p.category,
|
||||||
bookValue: p.bookValue,
|
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. */
|
/** 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 {
|
export interface InvestmentInterestDueDto {
|
||||||
purchaseId: number;
|
purchaseId: number;
|
||||||
dealNo: string;
|
dealNo: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user