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:
@@ -214,6 +214,10 @@
|
||||
(ngModelChange)="onDealDateChange()"
|
||||
></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
<label class="inv-form-field"
|
||||
><span>* Deal Time (HH:mm:ss)</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>* Settlement Type</span>
|
||||
<select [(ngModel)]="purchaseForm.settlementType">
|
||||
@@ -261,6 +265,15 @@
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Stamp Duty (₹)</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Brokerage (₹)</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Other Charges (₹)</span><input type="number" [(ngModel)]="purchaseForm.otherCharges" min="0"
|
||||
/></label>
|
||||
</div>
|
||||
|
||||
<!-- 2. Calculated — filled by the system, read-only -->
|
||||
@@ -316,12 +329,38 @@
|
||||
/></label>
|
||||
<label class="inv-form-field"><span>YTM %</span><input type="text" [value]="purchaseYtm" readonly /></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Charges (expense)</span
|
||||
><input type="text" [value]="'₹' + (purchaseTotalCharges | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Settlement Amount</span
|
||||
><input type="text" [value]="'₹' + (purchaseSettlementAmount | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
|
||||
@if (!isMutualFund && purchaseCouponDates) {
|
||||
<div class="pos-section-label">Interest Cycle</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Interest Dates (half-yearly)</span><input type="text" [value]="purchaseCouponDates" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Last Coupon</span><input type="text" [value]="purchaseForm.lastIntDt || '—'" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Next Coupon</span><input type="text" [value]="purchaseNextCouponDate || '—'" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Next Coupon Amount</span
|
||||
><input type="text" [value]="'₹' + (purchaseNextCouponAmount | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Days to Next Coupon</span><input type="text" [value]="purchaseDaysToNextCoupon" readonly
|
||||
/></label>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 3. Accounting preview -->
|
||||
<div class="pos-section-label">Accounting Preview (posted after checker approval)</div>
|
||||
@if (purchaseAccountingPreview.length) {
|
||||
@@ -358,10 +397,6 @@
|
||||
@if (showMoreDetails) {
|
||||
<div class="pos-section-label">More Details (optional)</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Deal Time (HH:mm:ss)</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Sub Type</span>
|
||||
<select [(ngModel)]="purchaseForm.subType">
|
||||
@@ -374,9 +409,6 @@
|
||||
<label class="inv-form-field"
|
||||
><span>Trade Amt</span><input type="number" [(ngModel)]="purchaseForm.tradeAmt" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Stamp Duty</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Stamp Duty A/c</span><input type="text" [(ngModel)]="purchaseForm.stampDutyAc"
|
||||
/></label>
|
||||
@@ -413,9 +445,6 @@
|
||||
><span>TBill GL A/c</span><input type="text" [(ngModel)]="purchaseForm.tbillGlAc"
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Brokerage (₹)</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Broker</span>
|
||||
<select [(ngModel)]="purchaseForm.brokerName">
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -303,6 +303,35 @@ export function lastHalfYearlyCouponDate(maturityStr: string, onStr: string): st
|
||||
return best ? formatDDMMYYYY(best) : '';
|
||||
}
|
||||
|
||||
/** First half-yearly coupon date after `onStr` (coupons fall on the maturity day/month and six months
|
||||
* either side), never later than maturity. */
|
||||
export function nextHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
|
||||
const last = lastHalfYearlyCouponDate(maturityStr, onStr);
|
||||
const mat = parseDDMMYYYY(maturityStr);
|
||||
if (!last || isNaN(mat.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const l = parseDDMMYYYY(last);
|
||||
const year = l.getFullYear() + (l.getMonth() >= 6 ? 1 : 0);
|
||||
const month = (l.getMonth() + 6) % 12;
|
||||
const next = new Date(year, month, Math.min(mat.getDate(), new Date(year, month + 1, 0).getDate()));
|
||||
return formatDDMMYYYY(next > mat ? mat : next);
|
||||
}
|
||||
|
||||
/** The two coupon days of a half-yearly security, e.g. "14 Feb / 14 Aug". */
|
||||
export function couponDatesLabel(maturityStr: string): string {
|
||||
const mat = parseDDMMYYYY(maturityStr);
|
||||
if (isNaN(mat.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const label = (month: number): string => {
|
||||
const day = Math.min(mat.getDate(), new Date(2001, month + 1, 0).getDate());
|
||||
return `${day} ${new Date(2001, month, 1).toLocaleString('en-US', { month: 'short' })}`;
|
||||
};
|
||||
const first = mat.getMonth() % 6;
|
||||
return `${label(first)} / ${label(first + 6)}`;
|
||||
}
|
||||
|
||||
export function formatDDMMYYYY(d: Date): string {
|
||||
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||
}
|
||||
@@ -1480,6 +1509,11 @@ export const INVESTMENT_GL_BY_TYPE: Record<InstrumentType, string> = {
|
||||
SHARE: '100706'
|
||||
};
|
||||
|
||||
/** Charges on an investment deal — expenses on their own GLs, not part of the book value (Jitendra Sir). */
|
||||
export const STAMP_DUTY_GL: InvestmentGlOption = { glCode: '500601', glName: 'STAMP DUTY ON INVESTMENTS' };
|
||||
export const BROKERAGE_GL: InvestmentGlOption = { glCode: '500602', glName: 'BROKERAGE ON INVESTMENTS' };
|
||||
export const OTHER_CHARGES_GL: InvestmentGlOption = { glCode: '500603', glName: 'OTHER CHARGES ON INVESTMENTS' };
|
||||
|
||||
/** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */
|
||||
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
|
||||
glCode: '100603',
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface InvestmentPurchaseDto {
|
||||
balanceUnits: number;
|
||||
brokerage?: number;
|
||||
stampDuty?: number;
|
||||
otherCharges?: number;
|
||||
bookValue: number;
|
||||
premiumDiscount?: number;
|
||||
intRate?: number;
|
||||
|
||||
Reference in New Issue
Block a user