feat(investment): Daily Script Master, market rate on purchase, broken-period interest as expense

- 09 Daily Script Master now saves to and loads from the backend (sample
  rows removed), with Drop to correct a wrong rate.
- Purchase Entry takes the rate (NAV for a mutual fund) from the newest
  Daily Script Master rate on or before the deal date, shows which rate
  was used, warns when the rate entered is outside the day's low-high,
  and clears the rate when the script has none.
- Broken-period interest paid to the seller is debited to expense GL
  500606 instead of Interest Receivable (RBI UCB Investment Directions,
  para 152, confirmed by Jitendra Sir); Interest Entry labels it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 15:27:15 +05:30
co-authored by Claude Opus 5.5
parent 88f34f58e3
commit 6a04a92113
6 changed files with 191 additions and 25 deletions
@@ -275,6 +275,28 @@
><span>Other Charges (₹)</span><input type="number" [(ngModel)]="purchaseForm.otherCharges" min="0"
/></label>
</div>
@if (purchaseMarketRate) {
<p class="pg-sub" style="padding: 0 16px 8px">
Market rate (Daily Script Master) as on {{ purchaseMarketRate.rateDate }}: Close
{{ purchaseMarketRate.closeRate | number: '1.2-4' }}
@if (purchaseMarketRate.lowRate && purchaseMarketRate.highRate) {
(Low {{ purchaseMarketRate.lowRate | number: '1.2-4' }} – High
{{ purchaseMarketRate.highRate | number: '1.2-4' }})
}
@if (purchaseMarketRate.rateDate !== purchaseForm.dealDate) {
— no rate entered for the deal date, the latest earlier rate is used.
}
</p>
@if (purchaseRateOutOfRange) {
<p class="pg-sub" style="padding: 0 16px 8px; color: #b42318; font-weight: 600">
Warning: the rate entered is outside the day's market range.
</p>
}
} @else if (purchaseMarketRate === null) {
<p class="pg-sub" style="padding: 0 16px 8px; color: #b42318">
No rate in the Daily Script Master for this script — enter the day's rate there first.
</p>
}
<!-- 2. Calculated — filled by the system, read-only -->
<div class="pos-section-label">Calculated by System</div>
@@ -732,7 +754,7 @@
><span>Maturity Date</span><input type="text" [value]="sc.maturityDate" readonly
/></label>
<label class="inv-form-field"
><span>Accrued Interest Paid at Purchase</span
><span>Broken Period Interest Paid (expense 500606)</span
><input type="text" [value]="'₹' + (sc.purchasedAccruedInterest | number: '1.2-2')" readonly
/></label>
<label class="inv-form-field"
@@ -29,6 +29,7 @@ import {
INVESTMENT_GL_BY_TYPE,
INVESTMENT_GL_LOOKUP,
INTEREST_RECEIVABLE_GL,
BROKEN_PERIOD_INTEREST_GL,
STAMP_DUTY_GL,
BROKERAGE_GL,
OTHER_CHARGES_GL,
@@ -54,6 +55,7 @@ import {
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
import {
InvestmentDailyRateDto,
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
InvestmentService,
@@ -391,6 +393,7 @@ export class BondInvestmentComponent implements OnInit {
const type = this.bondType ?? 'GOVT_SECURITY';
this.showMoreDetails = false;
this.retrieveCertNo = '';
this.purchaseMarketRate = undefined;
return {
bondType: type,
dealNo: this.nextDealNo(),
@@ -598,6 +601,7 @@ export class BondInvestmentComponent implements OnInit {
/** Re-suggests the last coupon date when the deal date changes. */
onDealDateChange(): void {
this.suggestLastIntDate();
this.loadMarketRate();
}
private suggestLastIntDate(): void {
@@ -672,6 +676,46 @@ export class BondInvestmentComponent implements OnInit {
f.productName = s.scriptName;
}
this.suggestLastIntDate();
this.loadMarketRate();
}
/**
* Market rate of the script for the deal date from the Daily Script Master (newest rate on or before it). It fills
* the purchase rate (NAV for a mutual fund); the dealer may still change it. undefined = not looked up yet,
* null = no rate entered for the script.
*/
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
private loadMarketRate(): void {
const s = this.purchaseScript;
const f = this.purchaseForm;
if (!s?.id || !f.dealDate) {
this.purchaseMarketRate = undefined;
return;
}
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
next: (rate) => {
this.purchaseMarketRate = rate;
// No market rate: clear the field rather than leave another script's rate in it.
if (this.isMutualFund) {
f.nav = rate ? rate.closeRate : null;
} else {
f.purchaseRate = rate ? rate.closeRate : null;
}
},
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
});
}
/** True when the rate entered is outside the day's low–high in the Daily Script Master. */
get purchaseRateOutOfRange(): boolean {
const m = this.purchaseMarketRate;
const f = this.purchaseForm;
const rate = this.isMutualFund ? f.nav : f.purchaseRate;
if (!m || rate == null) {
return false;
}
return (m.lowRate != null && rate < m.lowRate) || (m.highRate != null && rate > m.highRate);
}
/** Settlement Date = Deal Date + T+n days (greyed / auto in the existing CBS). */
@@ -709,11 +753,12 @@ export class BondInvestmentComponent implements OnInit {
const invGl = this.purchaseInvestmentGlCode;
const lines: AccountingPreviewLine[] = [
{ side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }];
// Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir).
if (accrued > 0) {
lines.push({
side: 'Dr',
glCode: INTEREST_RECEIVABLE_GL.glCode,
glName: INTEREST_RECEIVABLE_GL.glName,
glCode: BROKEN_PERIOD_INTEREST_GL.glCode,
glName: BROKEN_PERIOD_INTEREST_GL.glName,
amount: accrued
});
}