Compare commits
2
Commits
88c4242692
...
6a04a92113
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a04a92113 | ||
|
|
88f34f58e3 |
@@ -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,7 +265,38 @@
|
|||||||
}
|
}
|
||||||
</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>
|
||||||
|
@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 -->
|
<!-- 2. Calculated — filled by the system, read-only -->
|
||||||
<div class="pos-section-label">Calculated by System</div>
|
<div class="pos-section-label">Calculated by System</div>
|
||||||
@@ -316,12 +351,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 +419,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 +431,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 +467,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">
|
||||||
@@ -703,7 +754,7 @@
|
|||||||
><span>Maturity Date</span><input type="text" [value]="sc.maturityDate" readonly
|
><span>Maturity Date</span><input type="text" [value]="sc.maturityDate" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<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
|
><input type="text" [value]="'₹' + (sc.purchasedAccruedInterest | number: '1.2-2')" readonly
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
|
|||||||
@@ -29,11 +29,17 @@ import {
|
|||||||
INVESTMENT_GL_BY_TYPE,
|
INVESTMENT_GL_BY_TYPE,
|
||||||
INVESTMENT_GL_LOOKUP,
|
INVESTMENT_GL_LOOKUP,
|
||||||
INTEREST_RECEIVABLE_GL,
|
INTEREST_RECEIVABLE_GL,
|
||||||
|
BROKEN_PERIOD_INTEREST_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,
|
||||||
@@ -49,6 +55,7 @@ import {
|
|||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
import {
|
import {
|
||||||
|
InvestmentDailyRateDto,
|
||||||
InvestmentInterestScheduleDto,
|
InvestmentInterestScheduleDto,
|
||||||
InvestmentPurchaseDto,
|
InvestmentPurchaseDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
@@ -135,6 +142,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 {
|
||||||
@@ -385,6 +393,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
const type = this.bondType ?? 'GOVT_SECURITY';
|
const type = this.bondType ?? 'GOVT_SECURITY';
|
||||||
this.showMoreDetails = false;
|
this.showMoreDetails = false;
|
||||||
this.retrieveCertNo = '';
|
this.retrieveCertNo = '';
|
||||||
|
this.purchaseMarketRate = undefined;
|
||||||
return {
|
return {
|
||||||
bondType: type,
|
bondType: type,
|
||||||
dealNo: this.nextDealNo(),
|
dealNo: this.nextDealNo(),
|
||||||
@@ -448,7 +457,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
nav: null,
|
nav: null,
|
||||||
holdingDays: null,
|
holdingDays: null,
|
||||||
stampDuty: null,
|
stampDuty: null,
|
||||||
stampDutyAc: ''
|
stampDutyAc: '',
|
||||||
|
otherCharges: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,14 +528,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 {
|
||||||
@@ -564,6 +601,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
/** Re-suggests the last coupon date when the deal date changes. */
|
/** Re-suggests the last coupon date when the deal date changes. */
|
||||||
onDealDateChange(): void {
|
onDealDateChange(): void {
|
||||||
this.suggestLastIntDate();
|
this.suggestLastIntDate();
|
||||||
|
this.loadMarketRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private suggestLastIntDate(): void {
|
private suggestLastIntDate(): void {
|
||||||
@@ -574,9 +612,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 {
|
||||||
@@ -638,6 +676,46 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
f.productName = s.scriptName;
|
f.productName = s.scriptName;
|
||||||
}
|
}
|
||||||
this.suggestLastIntDate();
|
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). */
|
/** Settlement Date = Deal Date + T+n days (greyed / auto in the existing CBS). */
|
||||||
@@ -675,14 +753,37 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
const invGl = this.purchaseInvestmentGlCode;
|
const invGl = this.purchaseInvestmentGlCode;
|
||||||
const lines: AccountingPreviewLine[] = [
|
const lines: AccountingPreviewLine[] = [
|
||||||
{ side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }];
|
{ 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) {
|
if (accrued > 0) {
|
||||||
lines.push({
|
lines.push({
|
||||||
side: 'Dr',
|
side: 'Dr',
|
||||||
glCode: INTEREST_RECEIVABLE_GL.glCode,
|
glCode: BROKEN_PERIOD_INTEREST_GL.glCode,
|
||||||
glName: INTEREST_RECEIVABLE_GL.glName,
|
glName: BROKEN_PERIOD_INTEREST_GL.glName,
|
||||||
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 +794,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 +828,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,18 @@ export const INVESTMENT_GL_BY_TYPE: Record<InstrumentType, string> = {
|
|||||||
SHARE: '100706'
|
SHARE: '100706'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Broken-period interest paid to the seller at purchase — an expense, not part of cost (RBI UCB Investment
|
||||||
|
* Directions para 152). */
|
||||||
|
export const BROKEN_PERIOD_INTEREST_GL: InvestmentGlOption = {
|
||||||
|
glCode: '500606',
|
||||||
|
glName: 'BROKEN PERIOD INTEREST PAID ON INVESTMENTS'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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',
|
||||||
@@ -2127,13 +2168,15 @@ export const LIEN_REASON_MASTER: LienReason[] = [
|
|||||||
/** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported
|
/** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported
|
||||||
* every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */
|
* every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */
|
||||||
export interface DailyScriptRate {
|
export interface DailyScriptRate {
|
||||||
|
/** Backend id once saved. */
|
||||||
|
id?: number;
|
||||||
rateDate: string;
|
rateDate: string;
|
||||||
scriptCode: string;
|
scriptCode: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
openRate: number;
|
openRate: number | null;
|
||||||
closeRate: number;
|
closeRate: number;
|
||||||
highRate: number;
|
highRate: number | null;
|
||||||
lowRate: number;
|
lowRate: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [
|
export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [
|
||||||
|
|||||||
@@ -225,7 +225,7 @@
|
|||||||
<td>{{ p.city || '—' }}</td>
|
<td>{{ p.city || '—' }}</td>
|
||||||
<td>{{ p.bankType || '—' }}</td>
|
<td>{{ p.bankType || '—' }}</td>
|
||||||
<td>{{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }}</td>
|
<td>{{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }}</td>
|
||||||
<td class="num">{{ p.limit != null ? '₹' + (p.limit | number: '1.0-0') : '—' }}</td>
|
<td class="num">{{ (p.limit ?? null) !== null ? '₹' + (p.limit | number: '1.0-0') : '—' }}</td>
|
||||||
<td>{{ p.cpBankName ? p.cpBankName + (p.cpIfsc ? ' / ' + p.cpIfsc : '') : '—' }}</td>
|
<td>{{ p.cpBankName ? p.cpBankName + (p.cpIfsc ? ' / ' + p.cpIfsc : '') : '—' }}</td>
|
||||||
<td>{{ p.status ?? 'ACTIVE' }}</td>
|
<td>{{ p.status ?? 'ACTIVE' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -545,10 +545,11 @@
|
|||||||
<th class="num">High</th>
|
<th class="num">High</th>
|
||||||
<th class="num">Low</th>
|
<th class="num">Low</th>
|
||||||
<th class="num">Close</th>
|
<th class="num">Close</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (r of dailyScriptRates; track r.rateDate + r.scriptCode) {
|
@for (r of dailyScriptRates; track r.id ?? r.rateDate + r.scriptCode) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ r.rateDate }}</td>
|
<td>{{ r.rateDate }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -559,6 +560,13 @@
|
|||||||
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
|
||||||
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
|
||||||
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropDailyRate(r)">Drop</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="pg-empty">No rates entered yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
MUTUAL_FUND_PRODUCTS,
|
MUTUAL_FUND_PRODUCTS,
|
||||||
SGL_BANK_MASTER,
|
SGL_BANK_MASTER,
|
||||||
SCRIPT_MARKET_DETAIL,
|
SCRIPT_MARKET_DETAIL,
|
||||||
DAILY_SCRIPT_RATES,
|
|
||||||
LIEN_REASON_MASTER,
|
LIEN_REASON_MASTER,
|
||||||
ScriptMaster,
|
ScriptMaster,
|
||||||
BrokerMaster,
|
BrokerMaster,
|
||||||
@@ -36,7 +35,7 @@ import {
|
|||||||
periodBetween
|
periodBetween
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||||
import { InvestmentGlAccount, InvestmentService, apiErrorMessage } from '../investment.service';
|
import { InvestmentGlAccount, InvestmentService, apiErrorMessage, toDailyScriptRate } from '../investment.service';
|
||||||
|
|
||||||
type MasterTab =
|
type MasterTab =
|
||||||
| 'party'
|
| 'party'
|
||||||
@@ -79,7 +78,8 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
|
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
|
||||||
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
|
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
|
||||||
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
|
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
|
||||||
dailyScriptRates: DailyScriptRate[] = [...DAILY_SCRIPT_RATES];
|
/** Loaded from the backend (Daily Script Master) in ngOnInit. */
|
||||||
|
dailyScriptRates: DailyScriptRate[] = [];
|
||||||
lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
|
lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
|
||||||
|
|
||||||
readonly instrumentTypes: InstrumentType[] = [
|
readonly instrumentTypes: InstrumentType[] = [
|
||||||
@@ -131,6 +131,7 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
newDailyRate: Partial<DailyScriptRate> = {};
|
newDailyRate: Partial<DailyScriptRate> = {};
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.loadDailyRates();
|
||||||
this.investmentService.getAssetGlAccounts().subscribe({
|
this.investmentService.getAssetGlAccounts().subscribe({
|
||||||
next: (gls) => (this.investmentGlOptions = gls),
|
next: (gls) => (this.investmentGlOptions = gls),
|
||||||
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
||||||
@@ -435,24 +436,56 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
this.toast('SGL Bank master added.');
|
this.toast('SGL Bank master added.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadDailyRates(): void {
|
||||||
|
this.investmentService.getDailyRates().subscribe({
|
||||||
|
next: (rows) => (this.dailyScriptRates = rows.map(toDailyScriptRate)),
|
||||||
|
error: (err) => this.toast(`Could not load daily script rates: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addDailyRate(): void {
|
addDailyRate(): void {
|
||||||
if (!this.newDailyRate.scriptCode || !this.newDailyRate.rateDate || this.newDailyRate.closeRate == null) {
|
const r = this.newDailyRate;
|
||||||
|
if (!r.scriptCode || !r.rateDate || r.closeRate == null) {
|
||||||
this.toast('Enter Rate Date, Script Code and Close Rate.');
|
this.toast('Enter Rate Date, Script Code and Close Rate.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const script = this.scripts.find((s) => s.scriptCode === this.newDailyRate.scriptCode);
|
const script = this.scripts.find((s) => s.scriptCode === r.scriptCode);
|
||||||
this.dailyScriptRates = [
|
if (!script?.id) {
|
||||||
{
|
this.toast(`Script ${r.scriptCode} is not saved in Script Master.`);
|
||||||
scriptName: script?.scriptName || '',
|
return;
|
||||||
openRate: this.newDailyRate.closeRate,
|
}
|
||||||
highRate: this.newDailyRate.closeRate,
|
const body = {
|
||||||
lowRate: this.newDailyRate.closeRate,
|
scriptId: script.id,
|
||||||
...this.newDailyRate
|
rateDate: r.rateDate,
|
||||||
} as DailyScriptRate,
|
closeRate: r.closeRate,
|
||||||
...this.dailyScriptRates
|
...(r.openRate != null ? { openRate: r.openRate } : {}),
|
||||||
];
|
...(r.highRate != null ? { highRate: r.highRate } : {}),
|
||||||
this.newDailyRate = {};
|
...(r.lowRate != null ? { lowRate: r.lowRate } : {}),
|
||||||
this.toast('Daily script rate added.');
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
this.investmentService.createDailyRate(body).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} saved.`);
|
||||||
|
this.newDailyRate = {};
|
||||||
|
this.loadDailyRates();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A wrong rate is corrected by dropping it and entering it again. */
|
||||||
|
dropDailyRate(r: DailyScriptRate): void {
|
||||||
|
if (!r.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.deleteDailyRate(r.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} dropped.`);
|
||||||
|
this.loadDailyRates();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Export to Excel --------------------
|
// -------------------- Export to Excel --------------------
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|||||||
import { Observable, forkJoin } from 'rxjs';
|
import { Observable, forkJoin } from 'rxjs';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { map, tap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
DailyScriptRate,
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
PARTY_MASTER,
|
PARTY_MASTER,
|
||||||
@@ -97,6 +98,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;
|
||||||
@@ -177,6 +179,27 @@ export class InvestmentService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Daily Script Master --------------------
|
||||||
|
getDailyRates(scriptId?: number): Observable<InvestmentDailyRateDto[]> {
|
||||||
|
const params = scriptId ? `?scriptId=${scriptId}` : '';
|
||||||
|
return this.http.get<InvestmentDailyRateDto[]>(`/investment/daily-rates${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Newest Daily Script Master rate of a script on or before a date; null when none has been entered. */
|
||||||
|
getLatestDailyRate(scriptId: number, onDate: string): Observable<InvestmentDailyRateDto | null> {
|
||||||
|
return this.http.get<InvestmentDailyRateDto | null>(
|
||||||
|
`/investment/daily-rates/latest?scriptId=${scriptId}&onDate=${encodeURIComponent(onDate)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDailyRate(body: object): Observable<{ resourceId: number }> {
|
||||||
|
return this.http.post<{ resourceId: number }>('/investment/daily-rates', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDailyRate(rateId: number): Observable<unknown> {
|
||||||
|
return this.http.delete(`/investment/daily-rates/${rateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
||||||
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
|
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
|
||||||
}
|
}
|
||||||
@@ -336,6 +359,33 @@ export interface InvestmentInterestScheduleDto {
|
|||||||
rows: InvestmentInterestRowDto[];
|
rows: InvestmentInterestRowDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A Daily Script Master row (see InvestmentDailyRateData in the backend). */
|
||||||
|
export interface InvestmentDailyRateDto {
|
||||||
|
id: number;
|
||||||
|
scriptId: number;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
bondType: string;
|
||||||
|
rateDate: string;
|
||||||
|
openRate: number | null;
|
||||||
|
highRate: number | null;
|
||||||
|
lowRate: number | null;
|
||||||
|
closeRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDailyScriptRate(d: InvestmentDailyRateDto): DailyScriptRate {
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
rateDate: d.rateDate,
|
||||||
|
scriptCode: d.scriptCode,
|
||||||
|
scriptName: d.scriptDesc,
|
||||||
|
openRate: d.openRate,
|
||||||
|
closeRate: d.closeRate,
|
||||||
|
highRate: d.highRate,
|
||||||
|
lowRate: d.lowRate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
|
/** The first validation / domain-rule message from a Fineract error response, for a toast. */
|
||||||
export function apiErrorMessage(err: unknown): string {
|
export function apiErrorMessage(err: unknown): string {
|
||||||
const e = err as HttpErrorResponse;
|
const e = err as HttpErrorResponse;
|
||||||
|
|||||||
Reference in New Issue
Block a user