feat(investment): deal time, interest cycle and charges on Purchase Entry

As per Jitendra Sir's review:
- Deal Time is now a mandatory field next to Deal Date.
- New Interest Cycle section: the two coupon days, last and next coupon,
  next coupon amount and days to it.
- Stamp duty, brokerage and other charges sit on the main form. They are
  expenses on their own GLs (500601/500602/500603), so they are added to
  the settlement amount but no longer to the book value, and each shows
  as its own debit line in the accounting preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 11:54:33 +05:30
co-authored by Claude Opus 5.5
parent 88c4242692
commit 88f34f58e3
4 changed files with 140 additions and 17 deletions
@@ -29,11 +29,16 @@ import {
INVESTMENT_GL_BY_TYPE,
INVESTMENT_GL_LOOKUP,
INTEREST_RECEIVABLE_GL,
STAMP_DUTY_GL,
BROKERAGE_GL,
OTHER_CHARGES_GL,
SETTLEMENT_BANK_GL_OPTIONS,
periodBetween,
parseDDMMYYYY,
days360,
lastHalfYearlyCouponDate,
nextHalfYearlyCouponDate,
couponDatesLabel,
InvestmentHolding,
InvestmentTxn,
InstrumentType,
@@ -135,6 +140,7 @@ interface BondPurchaseForm {
holdingDays: number | null;
stampDuty: number | null;
stampDutyAc: string;
otherCharges: number | null;
}
interface AccountingPreviewLine {
@@ -448,7 +454,8 @@ export class BondInvestmentComponent implements OnInit {
nav: null,
holdingDays: null,
stampDuty: null,
stampDutyAc: ''
stampDutyAc: '',
otherCharges: null
};
}
@@ -518,14 +525,41 @@ export class BondInvestmentComponent implements OnInit {
return this.isMutualFund ? this.purchaseForm.nav || 0 : 100;
}
/** Book Value — what the investment is carried at, i.e. amount actually paid. */
/** Book Value — the price paid for the security. Charges are expenses and stay out of it (Jitendra Sir). */
get purchaseBookValue(): number {
const f = this.purchaseForm;
if (this.isMutualFund) {
return this.purchaseBaseAmount + (f.stampDuty || 0) + (f.brokerage || 0);
return this.purchaseBaseAmount;
}
const rate = f.purchaseRate || 100;
return Math.round((this.purchaseBaseAmount * rate) / 100) + (f.brokerage || 0);
return Math.round((this.purchaseBaseAmount * rate) / 100);
}
/** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */
get purchaseTotalCharges(): number {
const f = this.purchaseForm;
return (f.stampDuty || 0) + (f.brokerage || 0) + (f.otherCharges || 0);
}
// Interest cycle of the security being bought: its two coupon days and the next coupon after the deal.
get purchaseCouponDates(): string {
return this.purchaseMaturityDate ? couponDatesLabel(this.purchaseMaturityDate) : '';
}
get purchaseNextCouponDate(): string {
const f = this.purchaseForm;
return this.purchaseMaturityDate && f.dealDate
? nextHalfYearlyCouponDate(this.purchaseMaturityDate, f.dealDate)
: '';
}
get purchaseNextCouponAmount(): number {
const f = this.purchaseForm;
return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 200) : 0;
}
get purchaseDaysToNextCoupon(): number {
return this.purchaseNextCouponDate ? days360(this.purchaseForm.dealDate, this.purchaseNextCouponDate) : 0;
}
get purchaseYtm(): number {
@@ -574,9 +608,9 @@ export class BondInvestmentComponent implements OnInit {
}
}
/** Settlement Amt of Purchase — book value plus accrued interest payable to the seller. */
/** Settlement Amt of Purchase — book value, accrued interest payable to the seller and the charges. */
get purchaseSettlementAmount(): number {
return this.purchaseBookValue + this.purchaseAccruedInterest;
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
}
get purchaseMaturityAmount(): number {
@@ -683,6 +717,28 @@ export class BondInvestmentComponent implements OnInit {
amount: accrued
});
}
const f = this.purchaseForm;
for (const [
gl,
amount
] of [
[
STAMP_DUTY_GL,
f.stampDuty
],
[
BROKERAGE_GL,
f.brokerage
],
[
OTHER_CHARGES_GL,
f.otherCharges
]
] as const) {
if (amount && amount > 0) {
lines.push({ side: 'Dr', glCode: gl.glCode, glName: gl.glName, amount });
}
}
lines.push({ side: 'Cr', glCode: crGl, glName: this.glNameOf(crGl), amount: total });
return lines;
}
@@ -693,6 +749,7 @@ export class BondInvestmentComponent implements OnInit {
if (!f.scriptCode) missing.push('Script Code');
if (!f.partyCode) missing.push('Party Code');
if (!f.dealDate) missing.push('Deal Date');
if (!f.dealTime) missing.push('Deal Time');
if (this.isMutualFund) {
if (!f.considerAmt) missing.push('Consider Amt');
if (!f.nav) missing.push('NAV');
@@ -726,9 +783,11 @@ export class BondInvestmentComponent implements OnInit {
dealTime: f.dealTime || '',
settlementType: f.settlementType,
...(this.isMutualFund
? { considerAmt: f.considerAmt, nav: f.nav, stampDuty: f.stampDuty ?? null }
? { considerAmt: f.considerAmt, nav: f.nav }
: { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
stampDuty: f.stampDuty ?? null,
brokerage: f.brokerage ?? null,
otherCharges: f.otherCharges ?? null,
category: f.category,
crGlAccountId,
subType: f.subType || '',