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()"
|
(ngModelChange)="onDealDateChange()"
|
||||||
></mifosx-date-ddmmyyyy
|
></mifosx-date-ddmmyyyy
|
||||||
></label>
|
></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">
|
<label class="inv-form-field">
|
||||||
<span>* Settlement Type</span>
|
<span>* Settlement Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.settlementType">
|
<select [(ngModel)]="purchaseForm.settlementType">
|
||||||
@@ -261,6 +265,15 @@
|
|||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 2. Calculated — filled by the system, read-only -->
|
<!-- 2. Calculated — filled by the system, read-only -->
|
||||||
@@ -316,12 +329,38 @@
|
|||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"><span>YTM %</span><input type="text" [value]="purchaseYtm" readonly /></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"
|
<label class="inv-form-field"
|
||||||
><span>Total Settlement Amount</span
|
><span>Total Settlement Amount</span
|
||||||
><input type="text" [value]="'₹' + (purchaseSettlementAmount | number: '1.2-2')" readonly
|
><input type="text" [value]="'₹' + (purchaseSettlementAmount | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</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 -->
|
<!-- 3. Accounting preview -->
|
||||||
<div class="pos-section-label">Accounting Preview (posted after checker approval)</div>
|
<div class="pos-section-label">Accounting Preview (posted after checker approval)</div>
|
||||||
@if (purchaseAccountingPreview.length) {
|
@if (purchaseAccountingPreview.length) {
|
||||||
@@ -358,10 +397,6 @@
|
|||||||
@if (showMoreDetails) {
|
@if (showMoreDetails) {
|
||||||
<div class="pos-section-label">More Details (optional)</div>
|
<div class="pos-section-label">More Details (optional)</div>
|
||||||
<div class="inv-form-grid">
|
<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">
|
<label class="inv-form-field">
|
||||||
<span>Sub Type</span>
|
<span>Sub Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.subType">
|
<select [(ngModel)]="purchaseForm.subType">
|
||||||
@@ -374,9 +409,6 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Trade Amt</span><input type="number" [(ngModel)]="purchaseForm.tradeAmt" min="0"
|
><span>Trade Amt</span><input type="number" [(ngModel)]="purchaseForm.tradeAmt" min="0"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Stamp Duty</span><input type="number" [(ngModel)]="purchaseForm.stampDuty" min="0"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Stamp Duty A/c</span><input type="text" [(ngModel)]="purchaseForm.stampDutyAc"
|
><span>Stamp Duty A/c</span><input type="text" [(ngModel)]="purchaseForm.stampDutyAc"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -413,9 +445,6 @@
|
|||||||
><span>TBill GL A/c</span><input type="text" [(ngModel)]="purchaseForm.tbillGlAc"
|
><span>TBill GL A/c</span><input type="text" [(ngModel)]="purchaseForm.tbillGlAc"
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Brokerage (₹)</span><input type="number" [(ngModel)]="purchaseForm.brokerage" min="0"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Broker</span>
|
<span>Broker</span>
|
||||||
<select [(ngModel)]="purchaseForm.brokerName">
|
<select [(ngModel)]="purchaseForm.brokerName">
|
||||||
|
|||||||
@@ -29,11 +29,16 @@ import {
|
|||||||
INVESTMENT_GL_BY_TYPE,
|
INVESTMENT_GL_BY_TYPE,
|
||||||
INVESTMENT_GL_LOOKUP,
|
INVESTMENT_GL_LOOKUP,
|
||||||
INTEREST_RECEIVABLE_GL,
|
INTEREST_RECEIVABLE_GL,
|
||||||
|
STAMP_DUTY_GL,
|
||||||
|
BROKERAGE_GL,
|
||||||
|
OTHER_CHARGES_GL,
|
||||||
SETTLEMENT_BANK_GL_OPTIONS,
|
SETTLEMENT_BANK_GL_OPTIONS,
|
||||||
periodBetween,
|
periodBetween,
|
||||||
parseDDMMYYYY,
|
parseDDMMYYYY,
|
||||||
days360,
|
days360,
|
||||||
lastHalfYearlyCouponDate,
|
lastHalfYearlyCouponDate,
|
||||||
|
nextHalfYearlyCouponDate,
|
||||||
|
couponDatesLabel,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
@@ -135,6 +140,7 @@ interface BondPurchaseForm {
|
|||||||
holdingDays: number | null;
|
holdingDays: number | null;
|
||||||
stampDuty: number | null;
|
stampDuty: number | null;
|
||||||
stampDutyAc: string;
|
stampDutyAc: string;
|
||||||
|
otherCharges: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountingPreviewLine {
|
interface AccountingPreviewLine {
|
||||||
@@ -448,7 +454,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
nav: null,
|
nav: null,
|
||||||
holdingDays: null,
|
holdingDays: null,
|
||||||
stampDuty: null,
|
stampDuty: null,
|
||||||
stampDutyAc: ''
|
stampDutyAc: '',
|
||||||
|
otherCharges: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,14 +525,41 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return this.isMutualFund ? this.purchaseForm.nav || 0 : 100;
|
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 {
|
get purchaseBookValue(): number {
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
if (this.isMutualFund) {
|
if (this.isMutualFund) {
|
||||||
return this.purchaseBaseAmount + (f.stampDuty || 0) + (f.brokerage || 0);
|
return this.purchaseBaseAmount;
|
||||||
}
|
}
|
||||||
const rate = f.purchaseRate || 100;
|
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 {
|
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 {
|
get purchaseSettlementAmount(): number {
|
||||||
return this.purchaseBookValue + this.purchaseAccruedInterest;
|
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
|
||||||
}
|
}
|
||||||
|
|
||||||
get purchaseMaturityAmount(): number {
|
get purchaseMaturityAmount(): number {
|
||||||
@@ -683,6 +717,28 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
amount: accrued
|
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 });
|
lines.push({ side: 'Cr', glCode: crGl, glName: this.glNameOf(crGl), amount: total });
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -693,6 +749,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
if (!f.scriptCode) missing.push('Script Code');
|
if (!f.scriptCode) missing.push('Script Code');
|
||||||
if (!f.partyCode) missing.push('Party Code');
|
if (!f.partyCode) missing.push('Party Code');
|
||||||
if (!f.dealDate) missing.push('Deal Date');
|
if (!f.dealDate) missing.push('Deal Date');
|
||||||
|
if (!f.dealTime) missing.push('Deal Time');
|
||||||
if (this.isMutualFund) {
|
if (this.isMutualFund) {
|
||||||
if (!f.considerAmt) missing.push('Consider Amt');
|
if (!f.considerAmt) missing.push('Consider Amt');
|
||||||
if (!f.nav) missing.push('NAV');
|
if (!f.nav) missing.push('NAV');
|
||||||
@@ -726,9 +783,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
dealTime: f.dealTime || '',
|
dealTime: f.dealTime || '',
|
||||||
settlementType: f.settlementType,
|
settlementType: f.settlementType,
|
||||||
...(this.isMutualFund
|
...(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 }),
|
: { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
|
||||||
|
stampDuty: f.stampDuty ?? null,
|
||||||
brokerage: f.brokerage ?? null,
|
brokerage: f.brokerage ?? null,
|
||||||
|
otherCharges: f.otherCharges ?? null,
|
||||||
category: f.category,
|
category: f.category,
|
||||||
crGlAccountId,
|
crGlAccountId,
|
||||||
subType: f.subType || '',
|
subType: f.subType || '',
|
||||||
|
|||||||
@@ -303,6 +303,35 @@ export function lastHalfYearlyCouponDate(maturityStr: string, onStr: string): st
|
|||||||
return best ? formatDDMMYYYY(best) : '';
|
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 {
|
export function formatDDMMYYYY(d: Date): string {
|
||||||
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
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'
|
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. */
|
/** Interest receivable (the senior's "Provision A/c") — debited for interest bought from the seller. */
|
||||||
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
|
export const INTEREST_RECEIVABLE_GL: InvestmentGlOption = {
|
||||||
glCode: '100603',
|
glCode: '100603',
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface InvestmentPurchaseDto {
|
|||||||
balanceUnits: number;
|
balanceUnits: number;
|
||||||
brokerage?: number;
|
brokerage?: number;
|
||||||
stampDuty?: number;
|
stampDuty?: number;
|
||||||
|
otherCharges?: number;
|
||||||
bookValue: number;
|
bookValue: number;
|
||||||
premiumDiscount?: number;
|
premiumDiscount?: number;
|
||||||
intRate?: number;
|
intRate?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user