Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c09fb5dba | ||
|
|
b3f2addb5c | ||
|
|
ad0a1a0b1d | ||
|
|
d26cb89c5a | ||
|
|
13d04353e1 | ||
|
|
8e98077158 | ||
|
|
731ab5a0e0 | ||
|
|
7bd880387e | ||
|
|
86e3a5da27 | ||
|
|
d89f13471c | ||
|
|
6890584b16 | ||
|
|
d584af8e54 | ||
|
|
e1b33827ea | ||
|
|
8ed8f92b00 | ||
|
|
c9feeae3cb | ||
|
|
0404197c1b | ||
|
|
8a6becff86 | ||
|
|
c803e20628 | ||
|
|
6a04a92113 | ||
|
|
88f34f58e3 | ||
|
|
88c4242692 | ||
|
|
ca01e9b65b | ||
|
|
a4e0a22205 |
@@ -386,6 +386,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Investment interest vouchers that are due but not posted. A reminder only: unlike the interest
|
||||||
|
above it never stops the close (Jitendra Sir); the voucher can still be posted afterwards. -->
|
||||||
|
@if (preview?.investmentInterestPending?.length) {
|
||||||
|
<div class="bd-warn">
|
||||||
|
<mat-icon>notifications_active</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Reminder: investment interest vouchers are pending.</strong>
|
||||||
|
@for (t of preview.investmentInterestPending; track t.purchaseId) {
|
||||||
|
<div>
|
||||||
|
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
|
||||||
|
{{ t.intDate }} · ₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
Post them under Investment → Govt Security / Bond → 03 Interest Entry. You can still close the day.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (preview?.investmentFdPending?.length) {
|
||||||
|
<div class="bd-warn">
|
||||||
|
<mat-icon>notifications_active</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Reminder: FD investment entries are pending.</strong>
|
||||||
|
@for (t of preview.investmentFdPending; track t.fdId) {
|
||||||
|
<div>
|
||||||
|
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }}
|
||||||
|
· {{ t.partyName }} · FD {{ t.dealNo }} · {{ t.intDate }} · ₹{{ t.amount | number: '1.2-2' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
Post them under Investment → FD Investment. You can still close the day.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if (preview?.savingsInterestPending?.length) {
|
@if (preview?.savingsInterestPending?.length) {
|
||||||
<div class="bd-warn">
|
<div class="bd-warn">
|
||||||
<mat-icon>warning</mat-icon>
|
<mat-icon>warning</mat-icon>
|
||||||
|
|||||||
@@ -109,7 +109,15 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
} else if (status === 400) {
|
} else if (status === 400) {
|
||||||
alert = { type: 'Bad Request', message: errorMessage || 'Invalid parameters were passed in the request!' };
|
alert = { type: 'Bad Request', message: errorMessage || 'Invalid parameters were passed in the request!' };
|
||||||
} else if (status === 403) {
|
} else if (status === 403) {
|
||||||
alert = { type: 'Unauthorized Request', message: errorMessage || 'You are not authorized for this request!' };
|
// A 403 is either a missing permission or a business rule the record does not meet yet ("post the coupon
|
||||||
|
// first", "matures on …"). Only the first is an authorisation problem; the rule is told by its code.
|
||||||
|
const errorCode: string =
|
||||||
|
errorBody?.errors?.[0]?.userMessageGlobalisationCode || errorBody?.userMessageGlobalisationCode || '';
|
||||||
|
const isPermission = !errorCode || /not\.authori[sz]ed|unauthori[sz]ed|access\.denied|forbidden/i.test(errorCode);
|
||||||
|
alert = {
|
||||||
|
type: isPermission ? 'Unauthorized Request' : 'Action Not Allowed',
|
||||||
|
message: errorMessage || 'You are not authorized for this request!'
|
||||||
|
};
|
||||||
} else if (status === 404) {
|
} else if (status === 404) {
|
||||||
alert = {
|
alert = {
|
||||||
type: this.translate.instant('error.resource.not.found'),
|
type: this.translate.instant('error.resource.not.found'),
|
||||||
|
|||||||
@@ -1522,13 +1522,13 @@
|
|||||||
<!-- Investment -->
|
<!-- Investment -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Investment</div>
|
<div class="deposit-group-title">Investment</div>
|
||||||
<button mat-menu-item routerLink="/investment/dashboard" tabindex="0">
|
<button mat-menu-item routerLink="/investment/diary" tabindex="0">
|
||||||
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
<button mat-menu-item routerLink="/investment/interest-report" tabindex="0">
|
||||||
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
<button mat-menu-item routerLink="/investment/bucket-report" tabindex="0">
|
||||||
<fa-icon icon="piggy-bank" class="menu-item-icon"></fa-icon> Bucket Wise Investment
|
<fa-icon icon="piggy-bank" class="menu-item-icon"></fa-icon> Bucket Wise Investment
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/deaf" tabindex="0">
|
<button mat-menu-item routerLink="/deaf" tabindex="0">
|
||||||
@@ -2392,7 +2392,7 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
|
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
|
||||||
routerLink="/investment/dashboard"
|
routerLink="/investment/diary"
|
||||||
(click)="investmentMenuOpen = false"
|
(click)="investmentMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
|
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
|
||||||
@@ -2400,7 +2400,7 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesInvestmentSearch('Bucket Wise Investment')"
|
[class.hidden]="!matchesInvestmentSearch('Bucket Wise Investment')"
|
||||||
routerLink="/investment/reports"
|
routerLink="/investment/bucket-report"
|
||||||
(click)="investmentMenuOpen = false"
|
(click)="investmentMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="layer-group" class="gl-icon"></fa-icon> Bucket Wise Investment
|
<fa-icon icon="layer-group" class="gl-icon"></fa-icon> Bucket Wise Investment
|
||||||
@@ -2500,7 +2500,7 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
||||||
routerLink="/investment/reports"
|
routerLink="/investment/interest-report"
|
||||||
(click)="investmentMenuOpen = false"
|
(click)="investmentMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
||||||
|
|||||||
@@ -47,6 +47,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending investment entries (GOI / bond interest, FDs with other banks): one line each, details on demand -->
|
||||||
|
@if (pendingInvestmentInterest.length || pendingFd.length) {
|
||||||
|
<div class="investment-interest-alert">
|
||||||
|
<span class="iia-icon">🔔</span>
|
||||||
|
<div class="iia-text">
|
||||||
|
<strong>Investment entries pending</strong>
|
||||||
|
@if (pendingInvestmentInterest.length) {
|
||||||
|
<div class="iia-row">
|
||||||
|
<span class="iia-summary"
|
||||||
|
>GOI / Bonds: {{ pendingInvestmentInterest.length }} holding(s) with interest pending{{
|
||||||
|
overdueNote(pendingInvestmentInterest)
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<button type="button" class="iia-toggle" (click)="showInterestDetails = !showInterestDetails">
|
||||||
|
{{ showInterestDetails ? 'Hide' : 'Details' }}
|
||||||
|
</button>
|
||||||
|
<a class="iia-action" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }"
|
||||||
|
>Open Interest Entry →</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
@if (showInterestDetails) {
|
||||||
|
@for (t of pendingInvestmentInterest.slice(0, detailLimit); track t.purchaseId) {
|
||||||
|
<a class="iia-item" routerLink="/investment/bonds" [queryParams]="{ tab: 'interest' }">
|
||||||
|
{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} ·
|
||||||
|
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (pendingInvestmentInterest.length > detailLimit) {
|
||||||
|
<span class="iia-more">+{{ pendingInvestmentInterest.length - detailLimit }} more in Interest Entry</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (pendingFd.length) {
|
||||||
|
<div class="iia-row">
|
||||||
|
<span class="iia-summary"
|
||||||
|
>FD with banks: {{ pendingFd.length }} FD(s) with entries pending{{ overdueNote(pendingFd) }}</span
|
||||||
|
>
|
||||||
|
<button type="button" class="iia-toggle" (click)="showFdDetails = !showFdDetails">
|
||||||
|
{{ showFdDetails ? 'Hide' : 'Details' }}
|
||||||
|
</button>
|
||||||
|
<a class="iia-action" routerLink="/investment/fd">Open FD Investment →</a>
|
||||||
|
</div>
|
||||||
|
@if (showFdDetails) {
|
||||||
|
@for (t of pendingFd.slice(0, detailLimit); track t.fdId + t.lineType + t.intDate) {
|
||||||
|
<a class="iia-item" routerLink="/investment/fd" [queryParams]="{ tab: 'interest', fd: t.fdId }">
|
||||||
|
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }}
|
||||||
|
· {{ t.partyName }} · FD {{ t.dealNo }} ·
|
||||||
|
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (pendingFd.length > detailLimit) {
|
||||||
|
<span class="iia-more">+{{ pendingFd.length - detailLimit }} more in FD Investment</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Quick Action Cards -->
|
<!-- Quick Action Cards -->
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
|
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
|
||||||
|
|||||||
@@ -424,3 +424,78 @@ $accent: #0ea5e9;
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.investment-interest-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #f5c26b;
|
||||||
|
border-left: 4px solid #e08e0b;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #5c3d00;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.iia-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-text {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-summary {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-toggle {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border: 1px solid #e0a43a;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-item {
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-more {
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.iia-item:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iia-action {
|
||||||
|
align-self: center;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service'
|
|||||||
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
|
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
import { DscService } from 'app/users/dsc/dsc.service';
|
import { DscService } from 'app/users/dsc/dsc.service';
|
||||||
|
import { InvestmentFdDueDto, InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service';
|
||||||
|
|
||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
|
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
|
||||||
@@ -109,6 +110,7 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
|||||||
this.tenant = this.tenantIdentifier();
|
this.tenant = this.tenantIdentifier();
|
||||||
this.setFilteredActivities();
|
this.setFilteredActivities();
|
||||||
this.loadDscStatus();
|
this.loadDscStatus();
|
||||||
|
this.loadPendingInvestmentInterest();
|
||||||
if (!this.authenticationService.hasDialogBeenShown()) {
|
if (!this.authenticationService.hasDialogBeenShown()) {
|
||||||
this.dialog.open(WarningDialogComponent, {
|
this.dialog.open(WarningDialogComponent, {
|
||||||
disableClose: false,
|
disableClose: false,
|
||||||
@@ -118,6 +120,39 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */
|
||||||
|
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
|
||||||
|
/** FDs placed with other banks: interest vouchers and maturities due now or within two days. */
|
||||||
|
pendingFd: InvestmentFdDueDto[] = [];
|
||||||
|
/** The lists stay folded to one line each; the user opens them when needed. */
|
||||||
|
showInterestDetails = false;
|
||||||
|
showFdDetails = false;
|
||||||
|
readonly detailLimit = 5;
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
/** " · 3 overdue, oldest 14-08-2025" for a list of due entries (dates DD-MM-YYYY); empty when none is overdue. */
|
||||||
|
overdueNote(items: Array<{ overdue?: boolean; intDate: string }>): string {
|
||||||
|
const late = items.filter((t) => t.overdue);
|
||||||
|
if (!late.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const key = (d: string) => d.split('-').reverse().join('');
|
||||||
|
const oldest = late.map((t) => t.intDate).sort((a, b) => key(a).localeCompare(key(b)))[0];
|
||||||
|
return ` · ${late.length} overdue, oldest ${oldest}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
|
||||||
|
private loadPendingInvestmentInterest(): void {
|
||||||
|
this.investmentService.getDueInterest().subscribe({
|
||||||
|
next: (due) => (this.pendingInvestmentInterest = due ?? []),
|
||||||
|
error: () => (this.pendingInvestmentInterest = [])
|
||||||
|
});
|
||||||
|
this.investmentService.getFdDue().subscribe({
|
||||||
|
next: (due) => (this.pendingFd = due ?? []),
|
||||||
|
error: () => (this.pendingFd = [])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Surface a small "DSC verified" badge in the welcome banner when the
|
* Surface a small "DSC verified" badge in the welcome banner when the
|
||||||
* signed-in user has at least one ACTIVE DSC certificate. Silent on
|
* signed-in user has at least one ACTIVE DSC certificate. Silent on
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-page Investment Deal Slip: the serially numbered record of a single deal (purchase, sale, share refund,
|
||||||
|
* redemption or switch) with the counterparty, security, amount, price, deal date and time, settlement date, how the
|
||||||
|
* deal was confirmed, direct or through a broker, the counterparty's confirmation, and who entered, approved and
|
||||||
|
* settled it. A cancelled (dropped) deal prints with a CANCELLED stamp so its number stays accounted for.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GState, jsPDF } from 'jspdf';
|
||||||
|
import { BROKER_MASTER } from './investment-data';
|
||||||
|
import { confirmationModeLabel, InvestmentDealSlipDto } from './investment.service';
|
||||||
|
|
||||||
|
const BANK_NAME = 'Nexora Co-operative Bank Ltd.';
|
||||||
|
const C_TEXT: [number, number, number] = [
|
||||||
|
15,
|
||||||
|
23,
|
||||||
|
42
|
||||||
|
];
|
||||||
|
const C_MUTED: [number, number, number] = [
|
||||||
|
100,
|
||||||
|
116,
|
||||||
|
139
|
||||||
|
];
|
||||||
|
const C_BORDER: [number, number, number] = [
|
||||||
|
226,
|
||||||
|
232,
|
||||||
|
240
|
||||||
|
];
|
||||||
|
const ACCENT: [number, number, number] = [
|
||||||
|
30,
|
||||||
|
64,
|
||||||
|
175
|
||||||
|
];
|
||||||
|
const C_CANCELLED: [number, number, number] = [
|
||||||
|
220,
|
||||||
|
38,
|
||||||
|
38
|
||||||
|
];
|
||||||
|
|
||||||
|
const SIDE_LABEL: Record<string, string> = {
|
||||||
|
BUY: 'PURCHASE',
|
||||||
|
SELL: 'SALE',
|
||||||
|
REFUND: 'SHARE REFUND',
|
||||||
|
REDEEM: 'REDEMPTION',
|
||||||
|
SWITCH: 'SWITCH OUT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<string, string> = {
|
||||||
|
PENDING_SETTLEMENT: 'Pending settlement',
|
||||||
|
SETTLED: 'Settled',
|
||||||
|
CANCELLED: 'Cancelled (dropped)'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Amount in rupees; the PDF's standard font has no rupee sign, so "Rs". */
|
||||||
|
function rs(n: number | null | undefined): string {
|
||||||
|
if (n === null || n === undefined) return '—';
|
||||||
|
return 'Rs ' + n.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function num(n: number | null | undefined, maxDecimals = 6): string {
|
||||||
|
if (n === null || n === undefined) return '—';
|
||||||
|
return n.toLocaleString('en-IN', { maximumFractionDigits: maxDecimals });
|
||||||
|
}
|
||||||
|
|
||||||
|
function who(by?: string, on?: string): string {
|
||||||
|
if (!by) return '—';
|
||||||
|
return on ? `${by} on ${on}` : by;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Price label of the security: per Rs 100 of face value, NAV per unit, or price per share. */
|
||||||
|
function priceLabel(bondType: string): string {
|
||||||
|
if (bondType === 'MUTUAL_FUND') return 'NAV per unit';
|
||||||
|
if (bondType === 'SHARE') return 'Price per share';
|
||||||
|
return 'Price (per Rs 100 face value)';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDealSlipPdf(slip: InvestmentDealSlipDto, brokerName?: string): jsPDF {
|
||||||
|
const doc = new jsPDF({ unit: 'mm', format: 'a4' });
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
|
const margin = 14;
|
||||||
|
const colWidth = (pageWidth - margin * 2 - 6) / 2;
|
||||||
|
const labelWidth = 38;
|
||||||
|
let y = 16;
|
||||||
|
|
||||||
|
doc.setFillColor(...ACCENT);
|
||||||
|
doc.rect(0, 0, pageWidth, 4, 'F');
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(15);
|
||||||
|
doc.setTextColor(...C_TEXT);
|
||||||
|
doc.text(BANK_NAME, margin, y);
|
||||||
|
doc.setFontSize(13);
|
||||||
|
doc.text(`Slip No. ${slip.slipNo}`, pageWidth - margin, y, { align: 'right' });
|
||||||
|
y += 6;
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setTextColor(...C_MUTED);
|
||||||
|
doc.text('Treasury - Investment Deal Slip', margin, y);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setTextColor(...(slip.status === 'CANCELLED' ? C_CANCELLED : ACCENT));
|
||||||
|
doc.text(
|
||||||
|
`${SIDE_LABEL[slip.side] ?? slip.side} · ${STATUS_LABEL[slip.status] ?? slip.status}`,
|
||||||
|
pageWidth - margin,
|
||||||
|
y,
|
||||||
|
{ align: 'right' }
|
||||||
|
);
|
||||||
|
y += 5;
|
||||||
|
doc.setDrawColor(...C_BORDER);
|
||||||
|
doc.line(margin, y, pageWidth - margin, y);
|
||||||
|
y += 7;
|
||||||
|
|
||||||
|
/** A titled block of label / value pairs in two columns; long values wrap. */
|
||||||
|
const section = (title: string, rows: [string, string][]) => {
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(9.5);
|
||||||
|
doc.setTextColor(...ACCENT);
|
||||||
|
doc.text(title.toUpperCase(), margin, y);
|
||||||
|
y += 5;
|
||||||
|
for (let i = 0; i < rows.length; i += 2) {
|
||||||
|
let lines = 1;
|
||||||
|
[
|
||||||
|
rows[i],
|
||||||
|
rows[i + 1]
|
||||||
|
].forEach((r, col) => {
|
||||||
|
if (!r) return;
|
||||||
|
const x = margin + col * (colWidth + 6);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(8.5);
|
||||||
|
doc.setTextColor(...C_MUTED);
|
||||||
|
const label = doc.splitTextToSize(r[0], labelWidth - 3) as string[];
|
||||||
|
doc.text(label, x, y);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9.5);
|
||||||
|
doc.setTextColor(...C_TEXT);
|
||||||
|
const value = doc.splitTextToSize(r[1] || '—', colWidth - labelWidth) as string[];
|
||||||
|
doc.text(value, x + labelWidth, y);
|
||||||
|
lines = Math.max(lines, label.length, value.length);
|
||||||
|
});
|
||||||
|
y += 4.6 * lines + 1.6;
|
||||||
|
}
|
||||||
|
y += 2;
|
||||||
|
doc.setDrawColor(...C_BORDER);
|
||||||
|
doc.line(margin, y, pageWidth - margin, y);
|
||||||
|
y += 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buy = slip.side === 'BUY';
|
||||||
|
const settlement = slip.settlementType ? `${slip.settlementType}, ${slip.settlementDate}` : slip.settlementDate;
|
||||||
|
section('Deal', [
|
||||||
|
[
|
||||||
|
'Slip No.',
|
||||||
|
slip.slipNo
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Deal type',
|
||||||
|
SIDE_LABEL[slip.side] ?? slip.side
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Holding deal no.',
|
||||||
|
slip.certNo ? `${slip.dealNo} (Cert ${slip.certNo})` : slip.dealNo
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Category',
|
||||||
|
slip.category ?? '—'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Deal date & time',
|
||||||
|
slip.dealTime ? `${slip.dealDate} ${slip.dealTime}` : slip.dealDate
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Settlement',
|
||||||
|
settlement
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Status',
|
||||||
|
STATUS_LABEL[slip.status] ?? slip.status
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Voucher / GL txn',
|
||||||
|
slip.voucherNo ?? slip.glTransactionId ?? '—'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
section('Counterparty and security', [
|
||||||
|
[
|
||||||
|
buy ? 'Seller' : 'Buyer / payer',
|
||||||
|
slip.partyCode ? `${slip.partyCode} - ${slip.counterparty ?? ''}` : (slip.counterparty ?? '—')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Instrument',
|
||||||
|
slip.bondType.replace(/_/g, ' ')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Security',
|
||||||
|
`${slip.scriptCode} - ${slip.scriptDesc}`
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ISIN',
|
||||||
|
slip.isin ?? '—'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
section('Amount', [
|
||||||
|
[
|
||||||
|
'Units / quantity',
|
||||||
|
num(slip.units)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Face value',
|
||||||
|
rs(slip.faceValue)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
priceLabel(slip.bondType),
|
||||||
|
num(slip.price, 4)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Consideration',
|
||||||
|
rs(slip.consideration)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buy ? 'Interest paid to seller' : 'Interest from buyer',
|
||||||
|
rs(slip.accruedInterest)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Brokerage',
|
||||||
|
rs(slip.brokerage)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Stamp duty / charges',
|
||||||
|
rs(slip.otherCharges)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buy ? 'Net amount paid' : 'Net amount received',
|
||||||
|
rs(slip.netAmount)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buy ? 'Paid from (bank GL)' : 'Received in (bank GL)',
|
||||||
|
slip.bankGlCode ? `${slip.bankGlCode} - ${slip.bankGlName ?? ''}` : '—'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
section('Deal mode and confirmation', [
|
||||||
|
[
|
||||||
|
'Direct / broker',
|
||||||
|
slip.dealMode === 'BROKER' ? 'Through a broker' : 'Direct deal'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Broker',
|
||||||
|
slip.brokerCode ? `${slip.brokerCode}${brokerName ? ' - ' + brokerName : ''}` : '—'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Trade / order no.',
|
||||||
|
[
|
||||||
|
slip.tradeNo,
|
||||||
|
slip.orderNo
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ') || '—'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Confirmation sent by',
|
||||||
|
confirmationModeLabel(slip.confirmationMode) || '—'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Counterparty confirmation',
|
||||||
|
slip.confirmationReceivedOn
|
||||||
|
? `Received on ${slip.confirmationReceivedOn}${slip.confirmationRef ? ', ref ' + slip.confirmationRef : ''}`
|
||||||
|
: slip.status === 'CANCELLED'
|
||||||
|
? '—'
|
||||||
|
: 'Awaited'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
section('Processing (maker-checker)', [
|
||||||
|
[
|
||||||
|
'Entered by',
|
||||||
|
who(slip.enteredBy, slip.enteredOn)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Approved by',
|
||||||
|
who(slip.approvedBy, slip.approvedOn)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Settled by',
|
||||||
|
who(slip.settledBy, slip.settledOn)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Settlement approved by',
|
||||||
|
who(slip.settleApprovedBy, slip.settleApprovedOn)
|
||||||
|
],
|
||||||
|
...(slip.status === 'CANCELLED' ? ([
|
||||||
|
[
|
||||||
|
'Cancelled by',
|
||||||
|
who(slip.cancelledBy, slip.cancelledOn)
|
||||||
|
]
|
||||||
|
] as [string, string][]) : [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (slip.remarks) {
|
||||||
|
section('Remarks', [[
|
||||||
|
'Remarks',
|
||||||
|
slip.remarks
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signatures: the dealer, the back office that checked the confirmation, and the officer who authorised the deal.
|
||||||
|
const sigY = Math.max(y + 18, pageHeight - 40);
|
||||||
|
const sigWidth = (pageWidth - margin * 2 - 16) / 3;
|
||||||
|
doc.setDrawColor(...C_TEXT);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(...C_MUTED);
|
||||||
|
[
|
||||||
|
'Dealer',
|
||||||
|
'Back Office',
|
||||||
|
'Authorised Officer'
|
||||||
|
].forEach((label, i) => {
|
||||||
|
const x = margin + i * (sigWidth + 8);
|
||||||
|
doc.line(x, sigY, x + sigWidth, sigY);
|
||||||
|
doc.text(label, x, sigY + 5);
|
||||||
|
});
|
||||||
|
doc.setFontSize(7.5);
|
||||||
|
doc.text(`Printed on ${new Date().toLocaleString('en-IN')}`, margin, pageHeight - 10);
|
||||||
|
|
||||||
|
if (slip.status === 'CANCELLED') {
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(64);
|
||||||
|
doc.setTextColor(...C_CANCELLED);
|
||||||
|
doc.setGState(new GState({ opacity: 0.18 }));
|
||||||
|
// Rotated text is placed from its start, so step back half its width along the slope to centre it.
|
||||||
|
const half = doc.getTextWidth('CANCELLED') / 2;
|
||||||
|
const slope = (35 * Math.PI) / 180;
|
||||||
|
doc.text('CANCELLED', pageWidth / 2 - half * Math.cos(slope), pageHeight / 2 + half * Math.sin(slope), {
|
||||||
|
angle: 35
|
||||||
|
});
|
||||||
|
doc.setGState(new GState({ opacity: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker name from the Broker Master, for a broker code on a deal. */
|
||||||
|
export function brokerNameOf(code?: string | null): string | undefined {
|
||||||
|
return code ? BROKER_MASTER.find((b) => b.brokerCode === code)?.brokerName : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the slip and downloads it as deal-slip-<slip no>.pdf. */
|
||||||
|
export function saveDealSlipPdf(slip: InvestmentDealSlipDto): void {
|
||||||
|
buildDealSlipPdf(slip, brokerNameOf(slip.brokerCode)).save(`deal-slip-${slip.slipNo}.pdf`);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,198 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pg">
|
||||||
|
<header class="pg-head">
|
||||||
|
<div>
|
||||||
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||||
|
<h1 class="pg-title">Bucket Wise Investment</h1>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Govt Securities, bonds and FDs held on a date, split by the time left to maturity. Click a bucket to see the
|
||||||
|
holdings in it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="!report || generating" (click)="exportExcel()">
|
||||||
|
{{ generating ? 'Generating…' : 'Export to Excel' }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>As on</span><mifosx-date-ddmmyyyy [(ngModel)]="asOnDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
</div>
|
||||||
|
<div class="diary-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (error) {
|
||||||
|
<p class="diary-error">{{ error }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (report; as r) {
|
||||||
|
<div class="diary-summary">
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Total Investment</span
|
||||||
|
><span class="diary-tile-v">₹{{ r.total.totalAmount | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Govt Securities (book value)</span
|
||||||
|
><span class="diary-tile-v">₹{{ r.total.govtBookValue | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
@if (hasBonds) {
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Bonds / Non-SLR (book value)</span
|
||||||
|
><span class="diary-tile-v">₹{{ r.total.bondBookValue | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">FD with banks</span
|
||||||
|
><span class="diary-tile-v">₹{{ r.total.fdPrincipal | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">As on {{ r.asOnDate }} · by time left to maturity</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bucket</th>
|
||||||
|
<th class="num">Govt Sec</th>
|
||||||
|
<th class="num">Govt Sec Face Value</th>
|
||||||
|
<th class="num">Govt Sec Book Value</th>
|
||||||
|
@if (hasBonds) {
|
||||||
|
<th class="num">Bonds</th>
|
||||||
|
<th class="num">Bonds Book Value</th>
|
||||||
|
}
|
||||||
|
<th class="num">FD</th>
|
||||||
|
<th class="num">FD Principal</th>
|
||||||
|
<th class="num">Total Amount</th>
|
||||||
|
<th class="num">Share</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (b of r.buckets; track b.code) {
|
||||||
|
<tr class="report-pick" [class.report-pick--on]="bucket === b.code" (click)="pickBucket(b)">
|
||||||
|
<td>
|
||||||
|
<strong>{{ b.label }}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ b.govtCount }}</td>
|
||||||
|
<td class="num">₹{{ b.govtFaceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ b.govtBookValue | number: '1.2-2' }}</td>
|
||||||
|
@if (hasBonds) {
|
||||||
|
<td class="num">{{ b.bondCount }}</td>
|
||||||
|
<td class="num">₹{{ b.bondBookValue | number: '1.2-2' }}</td>
|
||||||
|
}
|
||||||
|
<td class="num">{{ b.fdCount }}</td>
|
||||||
|
<td class="num">₹{{ b.fdPrincipal | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ b.totalAmount | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ share(b) | number: '1.2-2' }}%</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="report-total">
|
||||||
|
<td>Total</td>
|
||||||
|
<td class="num">{{ r.total.govtCount }}</td>
|
||||||
|
<td class="num">₹{{ r.total.govtFaceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ r.total.govtBookValue | number: '1.2-2' }}</td>
|
||||||
|
@if (hasBonds) {
|
||||||
|
<td class="num">{{ r.total.bondCount }}</td>
|
||||||
|
<td class="num">₹{{ r.total.bondBookValue | number: '1.2-2' }}</td>
|
||||||
|
}
|
||||||
|
<td class="num">{{ r.total.fdCount }}</td>
|
||||||
|
<td class="num">₹{{ r.total.fdPrincipal | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ r.total.totalAmount | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ r.total.totalAmount > 0 ? '100.00%' : '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Bucket</span>
|
||||||
|
<select [(ngModel)]="bucket">
|
||||||
|
<option value="">All buckets</option>
|
||||||
|
@for (b of r.buckets; track b.code) {
|
||||||
|
<option [value]="b.code">{{ b.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Investment</span>
|
||||||
|
<select [(ngModel)]="instrument">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="GOVT_SECURITY">Govt Security</option>
|
||||||
|
@if (hasBonds) {
|
||||||
|
<option value="BOND">Bond / Non-SLR</option>
|
||||||
|
}
|
||||||
|
<option value="FD">FD with banks</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bucket</th>
|
||||||
|
<th>Investment</th>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
|
<th>Maturity</th>
|
||||||
|
<th>Time Left</th>
|
||||||
|
<th class="num">Face Value / Principal</th>
|
||||||
|
<th class="num">Book Value / Principal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (x of rows; track x.instrument + x.refId) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ bucketLabel(x.bucket) }}</td>
|
||||||
|
<td>{{ instrumentLabel(x.instrument) }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ x.particulars }}</td>
|
||||||
|
<td>{{ x.category || '—' }}</td>
|
||||||
|
<td class="num">{{ x.rate !== null && x.rate !== undefined ? x.rate + '%' : '—' }}</td>
|
||||||
|
<td>{{ x.maturityDate }}</td>
|
||||||
|
<td>
|
||||||
|
@if (x.daysToMaturity < 0) {
|
||||||
|
<span class="inv-badge diary-badge--overdue">{{ timeLeft(x) }}</span>
|
||||||
|
} @else {
|
||||||
|
{{ timeLeft(x) }}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="num">₹{{ x.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.bookValue | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="pg-empty">No investment held in this selection.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
@if (rows.length) {
|
||||||
|
<tfoot>
|
||||||
|
<tr class="report-total">
|
||||||
|
<td colspan="8">Total ({{ rows.length }})</td>
|
||||||
|
<td class="num">₹{{ rowsTotal.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ rowsTotal.bookValue | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import { todayDDMMYYYY } from '../investment-data';
|
||||||
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentBucketDto,
|
||||||
|
InvestmentBucketReportDto,
|
||||||
|
InvestmentBucketRowDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
|
type InstrumentFilter = '' | InvestmentBucketRowDto['instrument'];
|
||||||
|
|
||||||
|
const INSTRUMENT_LABEL: Record<InvestmentBucketRowDto['instrument'], string> = {
|
||||||
|
GOVT_SECURITY: 'Govt Security',
|
||||||
|
BOND: 'Bond / Non-SLR',
|
||||||
|
FD: 'FD'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity — up to
|
||||||
|
* 1 year, over 1 to 3, over 3 to 5 and over 5 years. Clicking a bucket lists the holdings in it.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-investment-bucket-report',
|
||||||
|
templateUrl: './investment-bucket-report.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../investment-shared.scss',
|
||||||
|
'../investment-diary/investment-diary.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InvestmentBucketReportComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
asOnDate = todayDDMMYYYY();
|
||||||
|
bucket = '';
|
||||||
|
instrument: InstrumentFilter = '';
|
||||||
|
report: InvestmentBucketReportDto | null = null;
|
||||||
|
loading = false;
|
||||||
|
generating = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (!this.asOnDate) {
|
||||||
|
this.error = 'Enter the As on date.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.investmentService.getBucketReport(this.asOnDate).subscribe({
|
||||||
|
next: (r: InvestmentBucketReportDto) => {
|
||||||
|
this.report = r;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.report = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bonds / Non-SLR columns only when any are held. */
|
||||||
|
get hasBonds(): boolean {
|
||||||
|
return (this.report?.total.bondCount ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows(): InvestmentBucketRowDto[] {
|
||||||
|
const all: InvestmentBucketRowDto[] = this.report?.rows ?? [];
|
||||||
|
return all.filter(
|
||||||
|
(r: InvestmentBucketRowDto) =>
|
||||||
|
(!this.bucket || r.bucket === this.bucket) && (!this.instrument || r.instrument === this.instrument)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get rowsTotal(): { faceValue: number; bookValue: number } {
|
||||||
|
return this.rows.reduce(
|
||||||
|
(t: { faceValue: number; bookValue: number }, r: InvestmentBucketRowDto) => ({
|
||||||
|
faceValue: t.faceValue + r.faceValue,
|
||||||
|
bookValue: t.bookValue + r.bookValue
|
||||||
|
}),
|
||||||
|
{ faceValue: 0, bookValue: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pickBucket(b: InvestmentBucketDto): void {
|
||||||
|
this.bucket = this.bucket === b.code ? '' : b.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketLabel(code: string): string {
|
||||||
|
return this.report?.buckets.find((b: InvestmentBucketDto) => b.code === code)?.label ?? code;
|
||||||
|
}
|
||||||
|
|
||||||
|
instrumentLabel(i: InvestmentBucketRowDto['instrument']): string {
|
||||||
|
return INSTRUMENT_LABEL[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Share of the bucket in the total amount, in %. */
|
||||||
|
share(b: InvestmentBucketDto): number {
|
||||||
|
const total = this.report?.total.totalAmount ?? 0;
|
||||||
|
return total > 0 ? (b.totalAmount / total) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time left to maturity as years and months, or how long it is overdue. */
|
||||||
|
timeLeft(r: InvestmentBucketRowDto): string {
|
||||||
|
if (r.daysToMaturity < 0) {
|
||||||
|
return `Overdue ${-r.daysToMaturity} days`;
|
||||||
|
}
|
||||||
|
const months = Math.floor(r.daysToMaturity / 30.4375);
|
||||||
|
const years = Math.floor(months / 12);
|
||||||
|
if (years === 0 && months === 0) {
|
||||||
|
return `${r.daysToMaturity} days`;
|
||||||
|
}
|
||||||
|
return years > 0 ? `${years}y ${months % 12}m` : `${months}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportExcel(): Promise<void> {
|
||||||
|
const report = this.report;
|
||||||
|
if (!report) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.generating = true;
|
||||||
|
try {
|
||||||
|
const bucketRow = (b: InvestmentBucketDto): (string | number | null)[] => [
|
||||||
|
b.label,
|
||||||
|
b.govtCount,
|
||||||
|
b.govtFaceValue,
|
||||||
|
b.govtBookValue,
|
||||||
|
...(this.hasBonds ? [
|
||||||
|
b.bondCount,
|
||||||
|
b.bondFaceValue,
|
||||||
|
b.bondBookValue
|
||||||
|
] : []),
|
||||||
|
b.fdCount,
|
||||||
|
b.fdPrincipal,
|
||||||
|
b.totalAmount,
|
||||||
|
Number(this.share(b).toFixed(2))
|
||||||
|
];
|
||||||
|
await exportWorkbook(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
sheetName: 'Bucket Summary',
|
||||||
|
title: 'Bucket Wise Investment',
|
||||||
|
subtitle: `As on ${report.asOnDate} · by time left to maturity`,
|
||||||
|
headers: [
|
||||||
|
'Bucket',
|
||||||
|
'Govt Sec (No.)',
|
||||||
|
'Govt Sec Face Value',
|
||||||
|
'Govt Sec Book Value',
|
||||||
|
...(this.hasBonds ? [
|
||||||
|
'Bonds (No.)',
|
||||||
|
'Bonds Face Value',
|
||||||
|
'Bonds Book Value'
|
||||||
|
] : []),
|
||||||
|
'FD (No.)',
|
||||||
|
'FD Principal',
|
||||||
|
'Total Amount',
|
||||||
|
'Share %'
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...report.buckets.map(bucketRow),
|
||||||
|
bucketRow(report.total)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sheetName: 'Holdings',
|
||||||
|
title: 'Bucket Wise Investment — Holdings',
|
||||||
|
subtitle: `As on ${report.asOnDate}`,
|
||||||
|
headers: [
|
||||||
|
'Bucket',
|
||||||
|
'Investment',
|
||||||
|
'Deal No',
|
||||||
|
'Particulars',
|
||||||
|
'Category',
|
||||||
|
'Rate %',
|
||||||
|
'Maturity',
|
||||||
|
'Time Left',
|
||||||
|
'Face Value / Principal',
|
||||||
|
'Book Value / Principal'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
22,
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
40,
|
||||||
|
10,
|
||||||
|
8,
|
||||||
|
12,
|
||||||
|
16,
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
rows: report.rows.map((r: InvestmentBucketRowDto) => [
|
||||||
|
this.bucketLabel(r.bucket),
|
||||||
|
this.instrumentLabel(r.instrument),
|
||||||
|
r.dealNo,
|
||||||
|
r.particulars,
|
||||||
|
r.category ?? '',
|
||||||
|
r.rate ?? null,
|
||||||
|
r.maturityDate,
|
||||||
|
this.timeLeft(r),
|
||||||
|
r.faceValue,
|
||||||
|
r.bookValue
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
`Bucket-Wise-Investment-${report.asOnDate}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.generating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,34 +12,50 @@
|
|||||||
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
|
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
|
||||||
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
|
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
|
||||||
<p class="pg-sub">
|
<p class="pg-sub">
|
||||||
Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on
|
What the bank holds today across Govt Securities, FDs with banks, Non-SLR, Bonds, Mutual Funds and Shares, as on
|
||||||
{{ asOnDate }}.
|
{{ asOnDate }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-toolbar">
|
<div class="pg-toolbar">
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating || loading" (click)="exportExcel()">
|
||||||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@if (error) {
|
||||||
|
<div class="pg-card">
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="inv-kpis">
|
<div class="inv-kpis">
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Total Holdings</span>
|
<span class="inv-kpi__label">Holdings</span>
|
||||||
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
||||||
<span class="inv-kpi__sub">{{ activeCount }} active · {{ maturedCount }} matured</span>
|
<span class="inv-kpi__sub"
|
||||||
|
>{{ maturingSoon.length }} due within 90 days{{ overdueCount ? ' (' + overdueCount + ' overdue)' : '' }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Total Invested (Face Value)</span>
|
<span class="inv-kpi__label">Total Invested (Book Value)</span>
|
||||||
<span class="inv-kpi__value">₹{{ totalFaceValue | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value">₹{{ totalBookValue | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub">Face value ₹{{ totalFaceValue | number: '1.2-2' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Accrued Interest / Income</span>
|
<span class="inv-kpi__label">Interest Receivable</span>
|
||||||
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub"
|
||||||
|
>Earned till today, not received · ₹{{ totalProvisioned | number: '1.2-2' }} provisioned</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-kpi">
|
<div class="inv-kpi">
|
||||||
<span class="inv-kpi__label">Projected Value at Maturity</span>
|
<span class="inv-kpi__label">Value at Maturity</span>
|
||||||
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
||||||
|
<span class="inv-kpi__sub">Securities at face value, FDs with interest</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,7 +67,9 @@
|
|||||||
<th>Instrument Type</th>
|
<th>Instrument Type</th>
|
||||||
<th class="num">Holdings</th>
|
<th class="num">Holdings</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th class="num col-in">Accrued Interest</th>
|
<th class="num">Book Value</th>
|
||||||
|
<th class="num">Share</th>
|
||||||
|
<th class="num col-in">Interest Receivable</th>
|
||||||
<th class="num col-net">Maturity Value</th>
|
<th class="num col-net">Maturity Value</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -62,6 +80,8 @@
|
|||||||
<td>{{ s.label }}</td>
|
<td>{{ s.label }}</td>
|
||||||
<td class="num">{{ s.count }}</td>
|
<td class="num">{{ s.count }}</td>
|
||||||
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ s.bookValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ s.sharePct | number: '1.1-1' }}%</td>
|
||||||
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
|
||||||
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
|
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -75,6 +95,8 @@
|
|||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td class="num">{{ totalHoldings }}</td>
|
<td class="num">{{ totalHoldings }}</td>
|
||||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totalBookValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ totalBookValue > 0 ? '100%' : '—' }}</td>
|
||||||
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -84,93 +106,70 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
|
<div class="pos-section-label">RBI Limits</div>
|
||||||
<p class="pg-sub">
|
@if (limitNote) {
|
||||||
Govt Security, Non-SLR, Mutual Fund and Share targets trace to RBI's SLR floor (18% of NDTL) and Non-SLR ceiling
|
<p class="inv-hint inv-hint--bad">{{ limitNote }}</p>
|
||||||
(≤10% of deposits) for Urban Co-operative Banks; Fixed Deposit and Bond are board-approved policy choices within
|
}
|
||||||
the room that framework leaves.
|
@if (limits.length) {
|
||||||
</p>
|
<table class="pg-table">
|
||||||
<table class="pg-table">
|
<thead>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Instrument Type</th>
|
|
||||||
<th>Target Basis</th>
|
|
||||||
<th class="num">Target %</th>
|
|
||||||
<th class="num">Actual %</th>
|
|
||||||
<th class="num">Actual Value</th>
|
|
||||||
<th class="num">Variance</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (a of allocationVsTarget; track a.type) {
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ a.label }}</td>
|
<th>Limit</th>
|
||||||
<td>
|
<th>Basis</th>
|
||||||
<span class="inv-badge" [class.inv-badge--matured]="a.isRbiLinked">
|
<th class="num">Allowed</th>
|
||||||
{{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }}
|
<th class="num">Used</th>
|
||||||
</span>
|
<th class="num">Used %</th>
|
||||||
</td>
|
<th>Status</th>
|
||||||
<td class="num">{{ a.targetPct }}%</td>
|
|
||||||
<td class="num">{{ a.actualPct }}%</td>
|
|
||||||
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
|
|
||||||
<td
|
|
||||||
class="num"
|
|
||||||
[class.col-out]="a.statusLabel !== 'Within Target'"
|
|
||||||
[class.col-in]="a.statusLabel === 'Within Target'"
|
|
||||||
>
|
|
||||||
{{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}%
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
class="inv-badge"
|
|
||||||
[class.inv-badge--active]="a.statusLabel === 'Within Target'"
|
|
||||||
[class.inv-badge--sold]="a.statusLabel === 'Over-Allocated'"
|
|
||||||
[class.inv-badge--written_off]="a.statusLabel === 'Under-Allocated'"
|
|
||||||
>
|
|
||||||
{{ a.statusLabel }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
<tfoot>
|
@for (l of limits; track l.label) {
|
||||||
<tr class="cctr-total-row">
|
<tr>
|
||||||
<td>Total</td>
|
<td>{{ l.label }}</td>
|
||||||
<td class="num">100%</td>
|
<td>{{ l.basis }}</td>
|
||||||
<td class="num">100%</td>
|
<td class="num">₹{{ l.limit | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ l.used | number: '1.2-2' }}</td>
|
||||||
<td colspan="2"></td>
|
<td class="num">{{ l.usedPct === null ? '—' : (l.usedPct | number: '1.1-1') + '%' }}</td>
|
||||||
</tr>
|
<td>
|
||||||
</tfoot>
|
<span class="inv-badge" [class.inv-badge--active]="l.within" [class.inv-badge--written_off]="!l.within">
|
||||||
</table>
|
{{ l.within ? 'Within' : 'Above limit' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Due-Date Diary — Maturing Within 90 Days</div>
|
<div class="pos-section-label">Maturing Within 90 Days</div>
|
||||||
@if (maturingSoon.length === 0) {
|
@if (maturingSoon.length === 0) {
|
||||||
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div>
|
<div>
|
||||||
@for (h of maturingSoon; track h.certNo) {
|
@for (m of maturingSoon; track m.dealNo) {
|
||||||
<div class="inv-maturity-row">
|
<div class="inv-maturity-row">
|
||||||
<span>
|
<span>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ m.dealNo }}</span>
|
||||||
{{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
|
{{ m.particulars }} ({{ m.instrument }})
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Matures {{ h.maturityDate }} ·
|
Matures {{ m.maturityDate }} · ₹{{ m.amount | number: '1.2-2' }} ·
|
||||||
<span
|
<span
|
||||||
class="inv-maturity-days"
|
class="inv-maturity-days"
|
||||||
[class.inv-maturity-days--soon]="daysLeft(h) <= 30"
|
[class.inv-maturity-days--soon]="m.days <= 30"
|
||||||
[class.inv-maturity-days--ok]="daysLeft(h) > 30"
|
[class.inv-maturity-days--ok]="m.days > 30"
|
||||||
>
|
>
|
||||||
{{ daysLeft(h) }} days
|
{{ m.days < 0 ? 'Overdue ' + -m.days + ' days' : m.days + ' days' }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inv-form-actions" style="padding: 8px 16px">
|
||||||
|
<a class="pg-btn pg-btn--ghost" routerLink="/investment/diary">Open Due Date Diary</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,19 +178,17 @@
|
|||||||
<div class="inv-nav-grid" style="padding: 16px">
|
<div class="inv-nav-grid" style="padding: 16px">
|
||||||
<a class="inv-nav-card" routerLink="/investment/fd">
|
<a class="inv-nav-card" routerLink="/investment/fd">
|
||||||
<span class="inv-nav-card__label">Fixed Deposit</span>
|
<span class="inv-nav-card__label">Fixed Deposit</span>
|
||||||
<span class="inv-nav-card__count">{{ summaries[0].count }}</span>
|
<span class="inv-nav-card__count">{{ countOf('FD') }}</span>
|
||||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Write-off</span>
|
<span class="inv-nav-card__value">Purchase / Interest / Maturity / Renewal</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/bonds">
|
<a class="inv-nav-card" routerLink="/investment/bonds">
|
||||||
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
|
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
|
||||||
<span class="inv-nav-card__count">{{
|
<span class="inv-nav-card__count">{{ countOf('GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND') }}</span>
|
||||||
summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count
|
|
||||||
}}</span>
|
|
||||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
|
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/share">
|
<a class="inv-nav-card" routerLink="/investment/share">
|
||||||
<span class="inv-nav-card__label">Share Entry</span>
|
<span class="inv-nav-card__label">Share Entry</span>
|
||||||
<span class="inv-nav-card__count">{{ summaries[5].count }}</span>
|
<span class="inv-nav-card__count">{{ countOf('SHARE') }}</span>
|
||||||
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
|
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="inv-nav-card" routerLink="/investment/interest">
|
<a class="inv-nav-card" routerLink="/investment/interest">
|
||||||
|
|||||||
@@ -6,21 +6,25 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import {
|
import { forkJoin } from 'rxjs';
|
||||||
ALL_HOLDINGS,
|
import { INSTRUMENT_LABELS, InstrumentType, parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
|
||||||
ALLOCATION_TARGET_IS_RBI_LINKED,
|
|
||||||
INSTRUMENT_LABELS,
|
|
||||||
InstrumentType,
|
|
||||||
InvestmentHolding,
|
|
||||||
TARGET_ALLOCATION_PCT,
|
|
||||||
accruedInterest,
|
|
||||||
daysToMaturity,
|
|
||||||
maturityAmount
|
|
||||||
} from '../investment-data';
|
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentBucketReportDto,
|
||||||
|
InvestmentBucketRowDto,
|
||||||
|
InvestmentFdDto,
|
||||||
|
InvestmentFdLimitDto,
|
||||||
|
InvestmentInterestReportDto,
|
||||||
|
InvestmentNonSlrLimitDto,
|
||||||
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentSaleDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage,
|
||||||
|
bookValueSoldByPurchase
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
interface InstrumentSummary {
|
interface InstrumentSummary {
|
||||||
type: InstrumentType;
|
type: InstrumentType;
|
||||||
@@ -28,25 +32,45 @@ interface InstrumentSummary {
|
|||||||
route: string;
|
route: string;
|
||||||
count: number;
|
count: number;
|
||||||
faceValue: number;
|
faceValue: number;
|
||||||
|
bookValue: number;
|
||||||
accruedInterest: number;
|
accruedInterest: number;
|
||||||
maturityValue: number;
|
maturityValue: number;
|
||||||
|
sharePct: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AllocationRow {
|
interface LimitRow {
|
||||||
type: InstrumentType;
|
|
||||||
label: string;
|
label: string;
|
||||||
targetPct: number;
|
basis: string;
|
||||||
actualPct: number;
|
limit: number | null;
|
||||||
actualValue: number;
|
used: number;
|
||||||
variancePct: number;
|
usedPct: number | null;
|
||||||
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
|
within: boolean;
|
||||||
/** True when the target itself traces to an RBI SLR/Non-SLR rule rather than being a
|
|
||||||
* board-only policy choice — see TARGET_ALLOCATION_PCT's doc comment in investment-data.ts. */
|
|
||||||
isRbiLinked: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
|
/** A holding maturing soon, or past maturity and not yet redeemed / closed. */
|
||||||
* hand-duplicated, so it can never drift from the per-instrument screens or Reports. */
|
interface MaturingRow {
|
||||||
|
dealNo: string;
|
||||||
|
particulars: string;
|
||||||
|
instrument: string;
|
||||||
|
maturityDate: string;
|
||||||
|
days: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROUTE: Record<InstrumentType, string> = {
|
||||||
|
FD: '/investment/fd',
|
||||||
|
GOVT_SECURITY: '/investment/bonds',
|
||||||
|
NON_SLR: '/investment/bonds',
|
||||||
|
BOND: '/investment/bonds',
|
||||||
|
MUTUAL_FUND: '/investment/bonds',
|
||||||
|
SHARE: '/investment/share'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-instrument portfolio dashboard on live data: what is held today by instrument (face value, book value,
|
||||||
|
* interest earned and not yet received, value at maturity), where the portfolio stands against the RBI limits, and
|
||||||
|
* what matures in the next 90 days.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-investment-dashboard',
|
selector: 'mifosx-investment-dashboard',
|
||||||
templateUrl: './investment-dashboard.component.html',
|
templateUrl: './investment-dashboard.component.html',
|
||||||
@@ -60,107 +84,220 @@ interface AllocationRow {
|
|||||||
RouterLink
|
RouterLink
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentDashboardComponent {
|
export class InvestmentDashboardComponent implements OnInit {
|
||||||
asOnDate = '';
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
asOnDate = todayDDMMYYYY();
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
generating = false;
|
generating = false;
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
private readonly types: Array<{ type: InstrumentType; route: string }> = [
|
|
||||||
{ type: 'FD', route: '/investment/fd' },
|
|
||||||
{ type: 'GOVT_SECURITY', route: '/investment/bonds' },
|
|
||||||
{ type: 'NON_SLR', route: '/investment/bonds' },
|
|
||||||
{ type: 'BOND', route: '/investment/bonds' },
|
|
||||||
{ type: 'MUTUAL_FUND', route: '/investment/bonds' },
|
|
||||||
{ type: 'SHARE', route: '/investment/share' }
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
summaries: InstrumentSummary[] = [];
|
||||||
const d = new Date();
|
limits: LimitRow[] = [];
|
||||||
this.asOnDate = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
limitNote = '';
|
||||||
|
maturingSoon: MaturingRow[] = [];
|
||||||
|
totalReceivable = 0;
|
||||||
|
totalProvisioned = 0;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
private holdingsOf(type: InstrumentType): InvestmentHolding[] {
|
load(): void {
|
||||||
return ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
const today = this.asOnDate;
|
||||||
}
|
const d = parseDDMMYYYY(today);
|
||||||
|
const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||||
get summaries(): InstrumentSummary[] {
|
this.loading = true;
|
||||||
return this.types.map(({ type, route }) => {
|
this.error = '';
|
||||||
const holdings = this.holdingsOf(type);
|
forkJoin({
|
||||||
return {
|
bucket: this.investmentService.getBucketReport(today),
|
||||||
type,
|
interest: this.investmentService.getInterestReport(fyStart, today),
|
||||||
label: INSTRUMENT_LABELS[type],
|
purchases: this.investmentService.getPurchases(),
|
||||||
route,
|
sales: this.investmentService.getSales(),
|
||||||
count: holdings.length,
|
fds: this.investmentService.getFds(),
|
||||||
faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
|
fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }),
|
||||||
accruedInterest: holdings.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0),
|
nonSlr: this.investmentService.getNonSlrLimit(null, today, 0)
|
||||||
maturityValue: holdings.reduce((s, h) => s + maturityAmount(h), 0)
|
}).subscribe({
|
||||||
};
|
next: (r) => {
|
||||||
|
this.build(r.bucket, r.interest, r.purchases, r.sales, r.fds, r.fdQuote.limit ?? null, r.nonSlr);
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Board-approved policy target % per instrument type vs the portfolio's actual allocation
|
private build(
|
||||||
* (by invested/face value) — a ±2 percentage-point band is treated as "Within Target" since
|
bucket: InvestmentBucketReportDto,
|
||||||
* a treasury desk can't rebalance to the exact decimal every day. */
|
interest: InvestmentInterestReportDto,
|
||||||
get allocationVsTarget(): AllocationRow[] {
|
purchases: InvestmentPurchaseDto[],
|
||||||
const total = this.totalFaceValue;
|
sales: InvestmentSaleDto[],
|
||||||
return this.types.map(({ type }) => {
|
fds: InvestmentFdDto[],
|
||||||
const value = this.holdingsOf(type).reduce((s, h) => s + h.faceValue, 0);
|
fdLimit: InvestmentFdLimitDto | null,
|
||||||
const actualPct = total ? (value / total) * 100 : 0;
|
nonSlr: InvestmentNonSlrLimitDto
|
||||||
const targetPct = TARGET_ALLOCATION_PCT[type];
|
): void {
|
||||||
const variancePct = +(actualPct - targetPct).toFixed(1);
|
const purchaseById = new Map<number, InvestmentPurchaseDto>(
|
||||||
const statusLabel: AllocationRow['statusLabel'] =
|
purchases.map((p) => [
|
||||||
Math.abs(variancePct) <= 2 ? 'Within Target' : variancePct > 0 ? 'Over-Allocated' : 'Under-Allocated';
|
p.id,
|
||||||
return {
|
p
|
||||||
type,
|
])
|
||||||
label: INSTRUMENT_LABELS[type],
|
);
|
||||||
targetPct,
|
const fdById = new Map<number, InvestmentFdDto>(
|
||||||
actualPct: +actualPct.toFixed(1),
|
fds
|
||||||
actualValue: value,
|
.filter((f) => f.id != null)
|
||||||
variancePct,
|
.map((f) => [
|
||||||
statusLabel,
|
f.id as number,
|
||||||
isRbiLinked: ALLOCATION_TARGET_IS_RBI_LINKED[type]
|
f
|
||||||
};
|
])
|
||||||
});
|
);
|
||||||
|
const receivable = new Map<string, number>(
|
||||||
|
interest.rows.map((x) => [
|
||||||
|
x.instrument + ':' + x.refId,
|
||||||
|
x.receivable
|
||||||
|
])
|
||||||
|
);
|
||||||
|
this.totalReceivable = interest.totalReceivable;
|
||||||
|
this.totalProvisioned = interest.totalProvisioned;
|
||||||
|
|
||||||
|
// Bucket rows are the Govt Securities, bonds and FDs held today; the purchase tells a Non-SLR from a Bond.
|
||||||
|
const typeOf = (r: InvestmentBucketRowDto): InstrumentType =>
|
||||||
|
r.instrument === 'FD' ? 'FD' : (purchaseById.get(r.refId)?.bondType ?? 'BOND');
|
||||||
|
const acc = new Map<InstrumentType, InstrumentSummary>();
|
||||||
|
const sum = (type: InstrumentType): InstrumentSummary => {
|
||||||
|
let s = acc.get(type);
|
||||||
|
if (!s) {
|
||||||
|
s = {
|
||||||
|
type,
|
||||||
|
label: INSTRUMENT_LABELS[type],
|
||||||
|
route: ROUTE[type],
|
||||||
|
count: 0,
|
||||||
|
faceValue: 0,
|
||||||
|
bookValue: 0,
|
||||||
|
accruedInterest: 0,
|
||||||
|
maturityValue: 0,
|
||||||
|
sharePct: 0
|
||||||
|
};
|
||||||
|
acc.set(type, s);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
for (const r of bucket.rows) {
|
||||||
|
const s = sum(typeOf(r));
|
||||||
|
s.count += 1;
|
||||||
|
s.faceValue += r.faceValue;
|
||||||
|
s.bookValue += r.bookValue;
|
||||||
|
s.accruedInterest += receivable.get((r.instrument === 'FD' ? 'FD' : 'SECURITY') + ':' + r.refId) ?? 0;
|
||||||
|
// A security is redeemed at face value; an FD pays its maturity amount (just the principal on a payout FD).
|
||||||
|
s.maturityValue +=
|
||||||
|
r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue;
|
||||||
|
}
|
||||||
|
// Mutual funds and shares have no maturity, so they are not in the buckets; the units still held are at cost.
|
||||||
|
const sold = bookValueSoldByPurchase(sales);
|
||||||
|
for (const p of purchases) {
|
||||||
|
if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') {
|
||||||
|
const s = sum(p.bondType);
|
||||||
|
const book = p.bookValue - (sold.get(p.id) ?? 0);
|
||||||
|
s.count += 1;
|
||||||
|
s.faceValue += p.units ? (p.faceValue * p.balanceUnits) / p.units : p.faceValue;
|
||||||
|
s.bookValue += book;
|
||||||
|
s.maturityValue += book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const order: InstrumentType[] = [
|
||||||
|
'GOVT_SECURITY',
|
||||||
|
'FD',
|
||||||
|
'NON_SLR',
|
||||||
|
'BOND',
|
||||||
|
'MUTUAL_FUND',
|
||||||
|
'SHARE'
|
||||||
|
];
|
||||||
|
const total = [...acc.values()].reduce((t, s) => t + s.bookValue, 0);
|
||||||
|
this.summaries = order
|
||||||
|
.map((t) => sum(t))
|
||||||
|
.map((s) => ({ ...s, sharePct: total > 0 ? (s.bookValue / total) * 100 : 0 }));
|
||||||
|
|
||||||
|
this.limits = [];
|
||||||
|
this.limitNote = '';
|
||||||
|
if (fdLimit?.grossLimit != null) {
|
||||||
|
const used = fdLimit.fdOutstanding + fdLimit.bankBalances;
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Deposits with other banks',
|
||||||
|
'FDs + current accounts, ≤ 20% of deposits',
|
||||||
|
fdLimit.grossLimit,
|
||||||
|
used
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nonSlr.nonSlrLimit != null) {
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Non-SLR investments',
|
||||||
|
'Bonds, CP / CD, MF, ≤ 10% of deposits',
|
||||||
|
nonSlr.nonSlrLimit,
|
||||||
|
nonSlr.nonSlrOutstanding
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.limits.push(
|
||||||
|
this.limitRow(
|
||||||
|
'Unlisted non-SLR',
|
||||||
|
`≤ 10% of non-SLR held on ${nonSlr.asOnDate}`,
|
||||||
|
nonSlr.unlistedLimit,
|
||||||
|
nonSlr.unlistedOutstanding
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.limitNote = `Total deposits as on ${nonSlr.asOnDate} are not entered yet; enter them on the FD screen to see the RBI limits.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maturingSoon = bucket.rows
|
||||||
|
.filter((r) => r.daysToMaturity <= 90)
|
||||||
|
.map((r) => ({
|
||||||
|
dealNo: r.dealNo,
|
||||||
|
particulars: r.particulars,
|
||||||
|
instrument: INSTRUMENT_LABELS[typeOf(r)],
|
||||||
|
maturityDate: r.maturityDate,
|
||||||
|
days: r.daysToMaturity,
|
||||||
|
amount: r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private limitRow(label: string, basis: string, limit: number, used: number): LimitRow {
|
||||||
|
return { label, basis, limit, used, usedPct: limit > 0 ? (used / limit) * 100 : null, within: used <= limit };
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalHoldings(): number {
|
get totalHoldings(): number {
|
||||||
return ALL_HOLDINGS.length;
|
return this.summaries.reduce((t, s) => t + s.count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalFaceValue(): number {
|
get totalFaceValue(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0);
|
return this.summaries.reduce((t, s) => t + s.faceValue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalBookValue(): number {
|
||||||
|
return this.summaries.reduce((t, s) => t + s.bookValue, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalAccruedInterest(): number {
|
get totalAccruedInterest(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0);
|
return this.summaries.reduce((t, s) => t + s.accruedInterest, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalMaturityValue(): number {
|
get totalMaturityValue(): number {
|
||||||
return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0);
|
return this.summaries.reduce((t, s) => t + s.maturityValue, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeCount(): number {
|
get overdueCount(): number {
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length;
|
return this.maturingSoon.filter((m) => m.days < 0).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maturedCount(): number {
|
/** Holdings of one or more instrument types, for the Quick Entry cards. */
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'MATURED').length;
|
countOf(...types: InstrumentType[]): number {
|
||||||
}
|
return this.summaries.filter((s) => types.includes(s.type)).reduce((t, s) => t + s.count, 0);
|
||||||
|
|
||||||
/** Holdings maturing within the next 90 days — a "due-date diary" style widget. */
|
|
||||||
get maturingSoon(): InvestmentHolding[] {
|
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && daysToMaturity(h, this.asOnDate) <= 90).sort(
|
|
||||||
(a, b) => daysToMaturity(a, this.asOnDate) - daysToMaturity(b, this.asOnDate)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
daysLeft(h: InvestmentHolding): number {
|
|
||||||
return daysToMaturity(h, this.asOnDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
instrumentLabel(type: InstrumentType): string {
|
|
||||||
return INSTRUMENT_LABELS[type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportExcel(): Promise<void> {
|
async exportExcel(): Promise<void> {
|
||||||
@@ -176,54 +313,97 @@ export class InvestmentDashboardComponent {
|
|||||||
'Instrument Type',
|
'Instrument Type',
|
||||||
'Holdings',
|
'Holdings',
|
||||||
'Face Value',
|
'Face Value',
|
||||||
'Accrued Interest',
|
'Book Value',
|
||||||
|
'Share %',
|
||||||
|
'Interest Receivable',
|
||||||
'Maturity Value'
|
'Maturity Value'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
22,
|
||||||
12,
|
10,
|
||||||
18,
|
18,
|
||||||
18,
|
18,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
18
|
18
|
||||||
],
|
],
|
||||||
rows: this.summaries.map((s) => [
|
rows: [
|
||||||
s.label,
|
...this.summaries.map((s) => [
|
||||||
s.count,
|
s.label,
|
||||||
s.faceValue,
|
s.count,
|
||||||
s.accruedInterest,
|
s.faceValue,
|
||||||
s.maturityValue
|
s.bookValue,
|
||||||
])
|
Number(s.sharePct.toFixed(2)),
|
||||||
|
s.accruedInterest,
|
||||||
|
s.maturityValue
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
this.totalHoldings,
|
||||||
|
this.totalFaceValue,
|
||||||
|
this.totalBookValue,
|
||||||
|
100,
|
||||||
|
this.totalAccruedInterest,
|
||||||
|
this.totalMaturityValue
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sheetName: 'Portfolio Allocation',
|
sheetName: 'RBI Limits',
|
||||||
title: 'Portfolio Allocation — Target vs Actual',
|
title: 'RBI Limits',
|
||||||
subtitle: `As on ${this.asOnDate}`,
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
headers: [
|
headers: [
|
||||||
'Instrument Type',
|
'Limit',
|
||||||
'Target Basis',
|
'Basis',
|
||||||
'Target %',
|
'Allowed',
|
||||||
'Actual %',
|
'Used',
|
||||||
'Actual Value',
|
'Used %',
|
||||||
'Variance %',
|
|
||||||
'Status'
|
'Status'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
26,
|
26,
|
||||||
20,
|
40,
|
||||||
12,
|
|
||||||
12,
|
|
||||||
18,
|
18,
|
||||||
|
18,
|
||||||
|
10,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
rows: this.limits.map((l) => [
|
||||||
|
l.label,
|
||||||
|
l.basis,
|
||||||
|
l.limit,
|
||||||
|
l.used,
|
||||||
|
l.usedPct === null ? null : Number(l.usedPct.toFixed(2)),
|
||||||
|
l.within ? 'Within' : 'Above limit'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sheetName: 'Maturing in 90 days',
|
||||||
|
title: 'Maturing Within 90 Days (and overdue)',
|
||||||
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
|
headers: [
|
||||||
|
'Deal No',
|
||||||
|
'Investment',
|
||||||
|
'Particulars',
|
||||||
|
'Maturity',
|
||||||
|
'Days',
|
||||||
|
'Amount'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
40,
|
||||||
12,
|
12,
|
||||||
|
10,
|
||||||
18
|
18
|
||||||
],
|
],
|
||||||
rows: this.allocationVsTarget.map((a) => [
|
rows: this.maturingSoon.map((m) => [
|
||||||
a.label,
|
m.dealNo,
|
||||||
a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy',
|
m.instrument,
|
||||||
a.targetPct,
|
m.particulars,
|
||||||
a.actualPct,
|
m.maturityDate,
|
||||||
a.actualValue,
|
m.days,
|
||||||
a.variancePct,
|
m.amount
|
||||||
a.statusLabel
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE';
|
export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE';
|
||||||
export type HoldingStatus = 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF';
|
export type HoldingStatus = 'PENDING_SETTLEMENT' | 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF';
|
||||||
export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE';
|
export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE';
|
||||||
export type TxnType =
|
export type TxnType =
|
||||||
| 'PURCHASE'
|
| 'PURCHASE'
|
||||||
@@ -28,7 +28,11 @@ export type TxnType =
|
|||||||
| 'REDEEM'
|
| 'REDEEM'
|
||||||
| 'WRITE_OFF'
|
| 'WRITE_OFF'
|
||||||
| 'AMORTIZE'
|
| 'AMORTIZE'
|
||||||
| 'PART_PAYMENT';
|
| 'TRANSFER'
|
||||||
|
| 'SWITCH'
|
||||||
|
| 'PART_PAYMENT'
|
||||||
|
| 'PREMATURE'
|
||||||
|
| 'RENEWAL';
|
||||||
|
|
||||||
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
||||||
FD: 'Fixed Deposit',
|
FD: 'Fixed Deposit',
|
||||||
@@ -123,6 +127,25 @@ export interface InvestmentHolding {
|
|||||||
* never re-counts days up to this date, so re-running a Calculation & Post for an overlapping
|
* never re-counts days up to this date, so re-running a Calculation & Post for an overlapping
|
||||||
* or repeated period always yields 0 instead of double-posting the same interest. */
|
* or repeated period always yields 0 instead of double-posting the same interest. */
|
||||||
interestPostedTillDate?: string;
|
interestPostedTillDate?: string;
|
||||||
|
/** RBI investment category chosen at purchase — HTM / AFS / HFT. */
|
||||||
|
category?: string;
|
||||||
|
/** SGL / CSGL account the security is held in (custody, not an accounting GL). */
|
||||||
|
sglAccountNo?: string;
|
||||||
|
/** Backend id / deal number / party code once the purchase is saved (absent on sample rows). */
|
||||||
|
id?: number;
|
||||||
|
dealNo?: string;
|
||||||
|
partyCode?: string;
|
||||||
|
bookValue?: number;
|
||||||
|
settlementAmount?: number;
|
||||||
|
/** How and when a closed FD ended (MATURITY / PREMATURE / RENEWAL). */
|
||||||
|
closureType?: string;
|
||||||
|
closedOnDate?: string;
|
||||||
|
/** Settlement date of the deal (deal date + T+n) — it can be settled on or after this day. */
|
||||||
|
settlementDate?: string;
|
||||||
|
/** Last coupon date before the purchase; coupons fall every six months from it. */
|
||||||
|
lastIntDate?: string;
|
||||||
|
/** Journal entry transaction posted when the deal was settled. */
|
||||||
|
glTransactionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvestmentTxn {
|
export interface InvestmentTxn {
|
||||||
@@ -248,6 +271,80 @@ export function accruedInterestBetween(h: InvestmentHolding, fromStr: string, to
|
|||||||
return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25));
|
return Math.round(h.faceValue * (h.interestRate / 100) * (days / 365.25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Days between two DD-MM-YYYY dates on a 30/360 basis (every month 30 days), as the bank's existing
|
||||||
|
* CBS counts them — e.g. 10-08 → 30-09 = 50 days, 10-08 → 07-10 = 57 days. */
|
||||||
|
export function days360(fromStr: string, toStr: string): number {
|
||||||
|
const a = parseDDMMYYYY(fromStr);
|
||||||
|
const b = parseDDMMYYYY(toStr);
|
||||||
|
if (isNaN(a.getTime()) || isNaN(b.getTime()) || b <= a) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const d1 = Math.min(a.getDate(), 30);
|
||||||
|
const d2 = Math.min(b.getDate(), 30);
|
||||||
|
return (b.getFullYear() - a.getFullYear()) * 360 + (b.getMonth() - a.getMonth()) * 30 + (d2 - d1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Most recent half-yearly coupon date on or before `onStr`, derived from the maturity date (coupons
|
||||||
|
* fall on the maturity day/month and six months either side of it). */
|
||||||
|
export function lastHalfYearlyCouponDate(maturityStr: string, onStr: string): string {
|
||||||
|
const mat = parseDDMMYYYY(maturityStr);
|
||||||
|
const on = parseDDMMYYYY(onStr);
|
||||||
|
if (isNaN(mat.getTime()) || isNaN(on.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const day = mat.getDate();
|
||||||
|
const couponOn = (year: number, month: number): Date => {
|
||||||
|
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||||
|
return new Date(year, month, Math.min(day, lastDay));
|
||||||
|
};
|
||||||
|
const months = [
|
||||||
|
mat.getMonth() % 6,
|
||||||
|
(mat.getMonth() % 6) + 6
|
||||||
|
];
|
||||||
|
let best: Date | undefined;
|
||||||
|
for (const year of [
|
||||||
|
on.getFullYear() - 1,
|
||||||
|
on.getFullYear()
|
||||||
|
]) {
|
||||||
|
for (const m of months) {
|
||||||
|
const c = couponOn(year, m);
|
||||||
|
if (c <= on && (!best || c > best)) {
|
||||||
|
best = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()}`;
|
||||||
}
|
}
|
||||||
@@ -1404,99 +1501,405 @@ export const CASH_BANK_GL_MASTER: InvestmentGlOption[] = [
|
|||||||
{ glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' }
|
{ glCode: '110030', glName: 'HDFC BANK CURRENT ACCOUNT' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Investment GLs a script / party can be mapped to. PROPOSED codes awaiting approval — the
|
||||||
|
* backend will take these from the chart of accounts, never hard-coded. */
|
||||||
|
export const INVESTMENT_GL_OPTIONS: InvestmentGlOption[] = [
|
||||||
|
{ glCode: '100701', glName: 'INVESTMENT IN CENTRAL GOVT SECURITIES' },
|
||||||
|
{ glCode: '100702', glName: 'INVESTMENT IN STATE GOVT SECURITIES' },
|
||||||
|
{ glCode: '100703', glName: 'INVESTMENT IN BONDS / DEBENTURES' },
|
||||||
|
{ glCode: '100704', glName: 'FIXED DEPOSITS WITH OTHER BANKS' },
|
||||||
|
{ glCode: '100705', glName: 'INVESTMENT IN MUTUAL FUNDS' },
|
||||||
|
{ glCode: '100706', glName: 'INVESTMENT IN SHARES' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Fallback investment GL when a script has none mapped. */
|
||||||
|
export const INVESTMENT_GL_BY_TYPE: Record<InstrumentType, string> = {
|
||||||
|
GOVT_SECURITY: '100701',
|
||||||
|
NON_SLR: '100703',
|
||||||
|
BOND: '100703',
|
||||||
|
FD: '100704',
|
||||||
|
MUTUAL_FUND: '100705',
|
||||||
|
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' };
|
||||||
|
/** Premium paid over face value, held until amortised to maturity (Jitendra Sir). */
|
||||||
|
export const PREMIUM_ON_INVESTMENTS_GL: InvestmentGlOption = { glCode: '100707', glName: 'PREMIUM 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',
|
||||||
|
glName: 'INTEREST RECEIVABLE ON INVESTMENTS (PROVISION)'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Bank accounts a purchase can be paid from — these GLs already exist in the chart of accounts. */
|
||||||
|
export const SETTLEMENT_BANK_GL_OPTIONS: InvestmentGlOption[] = [
|
||||||
|
{ glCode: '100201', glName: 'IDBI BANK' },
|
||||||
|
{ glCode: '100202', glName: 'YES BANK' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Every GL the Investment screens can show by name. */
|
||||||
|
export const INVESTMENT_GL_LOOKUP: InvestmentGlOption[] = [
|
||||||
|
...INVESTMENT_GL_OPTIONS,
|
||||||
|
INTEREST_RECEIVABLE_GL,
|
||||||
|
...SETTLEMENT_BANK_GL_OPTIONS
|
||||||
|
];
|
||||||
|
|
||||||
|
/** SLR classification on the Script Master, as in the bank's existing CBS ("GOI" = Central Govt). */
|
||||||
|
export type SlrType = 'GOI' | 'STATE_GOVT' | 'TREASURY' | 'NON_SLR';
|
||||||
|
|
||||||
|
export const SLR_TYPE_OPTIONS: Array<{ value: SlrType; label: string }> = [
|
||||||
|
{ value: 'GOI', label: 'GOI' },
|
||||||
|
{ value: 'STATE_GOVT', label: 'STATE GOVT' },
|
||||||
|
{ value: 'TREASURY', label: 'TREASURY' },
|
||||||
|
{ value: 'NON_SLR', label: 'NON SLR' }
|
||||||
|
];
|
||||||
|
|
||||||
export interface ScriptMaster {
|
export interface ScriptMaster {
|
||||||
|
/** Backend id once the script is saved (absent on the sample rows). */
|
||||||
|
id?: number;
|
||||||
scriptCode: string;
|
scriptCode: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
instrumentType: InstrumentType;
|
instrumentType: InstrumentType;
|
||||||
isin: string;
|
isin: string;
|
||||||
faceValuePerUnit: number;
|
faceValuePerUnit: number;
|
||||||
|
// Fields below follow the bank's existing CBS Script Master; a purchase picks them up instead
|
||||||
|
// of the dealer re-typing them on every deal.
|
||||||
|
slrType?: SlrType;
|
||||||
|
/** Investment GL this script is booked to. */
|
||||||
|
glAccountId?: number;
|
||||||
|
glCode?: string;
|
||||||
|
glName?: string;
|
||||||
|
active?: boolean;
|
||||||
|
/** Interest (coupon) rate % per annum. */
|
||||||
|
couponRate?: number;
|
||||||
|
issueDate?: string;
|
||||||
|
maturityDate?: string;
|
||||||
|
// Issuer details — used for Bond / Mutual Fund scripts.
|
||||||
|
companyName?: string;
|
||||||
|
address1?: string;
|
||||||
|
address2?: string;
|
||||||
|
address3?: string;
|
||||||
|
city?: string;
|
||||||
|
pincode?: string;
|
||||||
|
phone?: string;
|
||||||
|
fax?: string;
|
||||||
|
corpCode?: string;
|
||||||
|
/** Credit rating — long-term for bonds, short-term for CP / CD (Non-SLR and Bond scripts). */
|
||||||
|
rating?: string;
|
||||||
|
ratingAgency?: string;
|
||||||
|
/** Listed on an exchange or proposed to be listed; unlisted ones have their own RBI limit. */
|
||||||
|
listed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Credit ratings on the Script Master, best first: long-term for bonds, short-term for CP / CD. */
|
||||||
|
export const RATING_OPTIONS: Array<{ group: string; values: string[] }> = [
|
||||||
|
{
|
||||||
|
group: 'Long-term (bonds)',
|
||||||
|
values: [
|
||||||
|
'AAA',
|
||||||
|
'AA+',
|
||||||
|
'AA',
|
||||||
|
'AA-',
|
||||||
|
'A+',
|
||||||
|
'A',
|
||||||
|
'A-',
|
||||||
|
'BBB+',
|
||||||
|
'BBB',
|
||||||
|
'BBB-',
|
||||||
|
'BB+',
|
||||||
|
'BB',
|
||||||
|
'BB-',
|
||||||
|
'B+',
|
||||||
|
'B',
|
||||||
|
'B-',
|
||||||
|
'C',
|
||||||
|
'D'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Short-term (CP / CD)',
|
||||||
|
values: [
|
||||||
|
'A1+',
|
||||||
|
'A1',
|
||||||
|
'A2+',
|
||||||
|
'A2',
|
||||||
|
'A3+',
|
||||||
|
'A3',
|
||||||
|
'A4+',
|
||||||
|
'A4'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ group: 'None', values: ['UNRATED'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** RBI: bonds rated "A" or higher, CP / CD rated "A2" or higher. */
|
||||||
|
export const ELIGIBLE_RATINGS: string[] = [
|
||||||
|
'AAA',
|
||||||
|
'AA+',
|
||||||
|
'AA',
|
||||||
|
'AA-',
|
||||||
|
'A+',
|
||||||
|
'A',
|
||||||
|
'A-',
|
||||||
|
'A1+',
|
||||||
|
'A1',
|
||||||
|
'A2+',
|
||||||
|
'A2'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Credit rating agencies registered with SEBI. */
|
||||||
|
export const RATING_AGENCY_OPTIONS: Array<{ value: string; label: string }> = [
|
||||||
|
{ value: 'CRISIL', label: 'CRISIL' },
|
||||||
|
{ value: 'ICRA', label: 'ICRA' },
|
||||||
|
{ value: 'CARE', label: 'CARE' },
|
||||||
|
{ value: 'INDIA_RATINGS', label: 'India Ratings' },
|
||||||
|
{ value: 'ACUITE', label: 'Acuite' },
|
||||||
|
{ value: 'BRICKWORK', label: 'Brickwork' },
|
||||||
|
{ value: 'INFOMERICS', label: 'Infomerics' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Period between two DD-MM-YYYY dates as years / months / days, as shown on the Script Master. */
|
||||||
|
export function periodBetween(fromStr?: string, toStr?: string): { years: number; months: number; days: number } {
|
||||||
|
const none = { years: 0, months: 0, days: 0 };
|
||||||
|
if (!fromStr || !toStr) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
const from = parseDDMMYYYY(fromStr);
|
||||||
|
const to = parseDDMMYYYY(toStr);
|
||||||
|
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to < from) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
let years = to.getFullYear() - from.getFullYear();
|
||||||
|
let months = to.getMonth() - from.getMonth();
|
||||||
|
let days = to.getDate() - from.getDate();
|
||||||
|
if (days < 0) {
|
||||||
|
months -= 1;
|
||||||
|
days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
|
||||||
|
}
|
||||||
|
if (months < 0) {
|
||||||
|
years -= 1;
|
||||||
|
months += 12;
|
||||||
|
}
|
||||||
|
return { years, months, days };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SCRIPT_MASTER: ScriptMaster[] = [
|
export const SCRIPT_MASTER: ScriptMaster[] = [
|
||||||
{ scriptCode: 'FD-CUM', scriptName: 'Cumulative Fixed Deposit', instrumentType: 'FD', isin: '', faceValuePerUnit: 1 },
|
{
|
||||||
|
scriptCode: 'FD-CUM',
|
||||||
|
scriptName: 'Cumulative Fixed Deposit',
|
||||||
|
instrumentType: 'FD',
|
||||||
|
isin: '',
|
||||||
|
faceValuePerUnit: 1,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100704'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'FD-MIP',
|
scriptCode: 'FD-MIP',
|
||||||
scriptName: 'Monthly Interest Payout FD',
|
scriptName: 'Monthly Interest Payout FD',
|
||||||
instrumentType: 'FD',
|
instrumentType: 'FD',
|
||||||
isin: '',
|
isin: '',
|
||||||
faceValuePerUnit: 1
|
faceValuePerUnit: 1,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100704'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'IN0020230012',
|
scriptCode: 'IN0020230012',
|
||||||
scriptName: '7.26% GOI 2033',
|
scriptName: '7.26% GOI 2033',
|
||||||
instrumentType: 'GOVT_SECURITY',
|
instrumentType: 'GOVT_SECURITY',
|
||||||
isin: 'IN0020230012',
|
isin: 'IN0020230012',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'GOI',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 7.26,
|
||||||
|
issueDate: '15-01-2023',
|
||||||
|
maturityDate: '15-01-2033'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'IN0020220089',
|
scriptCode: 'IN0020220089',
|
||||||
scriptName: '7.10% GOI 2029',
|
scriptName: '7.10% GOI 2029',
|
||||||
instrumentType: 'GOVT_SECURITY',
|
instrumentType: 'GOVT_SECURITY',
|
||||||
isin: 'IN0020220089',
|
isin: 'IN0020220089',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'GOI',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 7.1,
|
||||||
|
issueDate: '10-03-2022',
|
||||||
|
maturityDate: '10-03-2029'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptCode: 'IN0020210056',
|
||||||
|
scriptName: '6.79% GOI 2027',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
isin: 'IN0020210056',
|
||||||
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'GOI',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 6.79,
|
||||||
|
issueDate: '05-06-2021',
|
||||||
|
maturityDate: '05-06-2027'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptCode: 'IN0020240034',
|
||||||
|
scriptName: '7.18% GOI 2037',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
isin: 'IN0020240034',
|
||||||
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'GOI',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 7.18,
|
||||||
|
issueDate: '22-04-2024',
|
||||||
|
maturityDate: '22-04-2037'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptCode: 'MH-SDL-2031',
|
||||||
|
scriptName: '8.05% Maharashtra SDL 2031',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
isin: '',
|
||||||
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'STATE_GOVT',
|
||||||
|
glCode: '100702',
|
||||||
|
couponRate: 8.05,
|
||||||
|
issueDate: '15-02-2021',
|
||||||
|
maturityDate: '15-02-2031'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptCode: 'GJ-SDL-2026',
|
||||||
|
scriptName: '7.85% Gujarat SDL 2026',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
isin: '',
|
||||||
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'STATE_GOVT',
|
||||||
|
glCode: '100702',
|
||||||
|
couponRate: 7.85,
|
||||||
|
issueDate: '18-09-2016',
|
||||||
|
maturityDate: '18-09-2026'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'TBILL-364D',
|
scriptCode: 'TBILL-364D',
|
||||||
scriptName: '364 Days Treasury Bill',
|
scriptName: '364 Days Treasury Bill',
|
||||||
instrumentType: 'GOVT_SECURITY',
|
instrumentType: 'GOVT_SECURITY',
|
||||||
isin: '',
|
isin: '',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'TREASURY',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 0,
|
||||||
|
issueDate: '02-07-2026',
|
||||||
|
maturityDate: '02-07-2027'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptCode: 'TBILL-91D',
|
||||||
|
scriptName: '91 Days Treasury Bill',
|
||||||
|
instrumentType: 'GOVT_SECURITY',
|
||||||
|
isin: '',
|
||||||
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'TREASURY',
|
||||||
|
glCode: '100701',
|
||||||
|
couponRate: 0,
|
||||||
|
issueDate: '15-05-2026',
|
||||||
|
maturityDate: '13-08-2026'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'REC-7.5-2031',
|
scriptCode: 'REC-7.5-2031',
|
||||||
scriptName: 'REC Ltd Bond 7.50% 2031',
|
scriptName: 'REC Ltd Bond 7.50% 2031',
|
||||||
instrumentType: 'NON_SLR',
|
instrumentType: 'NON_SLR',
|
||||||
isin: 'INE020B08JN0',
|
isin: 'INE020B08JN0',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
couponRate: 7.5,
|
||||||
|
issueDate: '11-08-2021',
|
||||||
|
maturityDate: '11-08-2031',
|
||||||
|
companyName: 'REC Limited',
|
||||||
|
city: 'New Delhi'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'NABARD-7.35-2030',
|
scriptCode: 'NABARD-7.35-2030',
|
||||||
scriptName: 'NABARD Bond 7.35% 2030',
|
scriptName: 'NABARD Bond 7.35% 2030',
|
||||||
instrumentType: 'NON_SLR',
|
instrumentType: 'NON_SLR',
|
||||||
isin: 'INE261F09EU1',
|
isin: 'INE261F09EU1',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
couponRate: 7.35,
|
||||||
|
issueDate: '20-09-2020',
|
||||||
|
maturityDate: '20-09-2030',
|
||||||
|
companyName: 'NABARD',
|
||||||
|
city: 'Mumbai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'LTF-8.1-2028',
|
scriptCode: 'LTF-8.1-2028',
|
||||||
scriptName: 'L&T Finance NCD 8.10% 2028',
|
scriptName: 'L&T Finance NCD 8.10% 2028',
|
||||||
instrumentType: 'BOND',
|
instrumentType: 'BOND',
|
||||||
isin: 'INE027E07BR5',
|
isin: 'INE027E07BR5',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
couponRate: 8.1,
|
||||||
|
companyName: 'L&T Finance Ltd',
|
||||||
|
city: 'Mumbai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'BAF-7.95-2027',
|
scriptCode: 'BAF-7.95-2027',
|
||||||
scriptName: 'Bajaj Finance NCD 7.95% 2027',
|
scriptName: 'Bajaj Finance NCD 7.95% 2027',
|
||||||
instrumentType: 'BOND',
|
instrumentType: 'BOND',
|
||||||
isin: 'INE296A07SP0',
|
isin: 'INE296A07SP0',
|
||||||
faceValuePerUnit: 100
|
faceValuePerUnit: 100,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
couponRate: 7.95,
|
||||||
|
companyName: 'Bajaj Finance Ltd',
|
||||||
|
city: 'Pune'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'SBI-OVN',
|
scriptCode: 'SBI-OVN',
|
||||||
scriptName: 'SBI Overnight Fund',
|
scriptName: 'SBI Overnight Fund',
|
||||||
instrumentType: 'MUTUAL_FUND',
|
instrumentType: 'MUTUAL_FUND',
|
||||||
isin: 'INF200KA1Q40',
|
isin: 'INF200KA1Q40',
|
||||||
faceValuePerUnit: 1000
|
faceValuePerUnit: 1000,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100705',
|
||||||
|
companyName: 'SBI Mutual Fund',
|
||||||
|
city: 'Mumbai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'HDFC-LIQ',
|
scriptCode: 'HDFC-LIQ',
|
||||||
scriptName: 'HDFC Liquid Fund',
|
scriptName: 'HDFC Liquid Fund',
|
||||||
instrumentType: 'MUTUAL_FUND',
|
instrumentType: 'MUTUAL_FUND',
|
||||||
isin: 'INF179KA1WR9',
|
isin: 'INF179KA1WR9',
|
||||||
faceValuePerUnit: 1000
|
faceValuePerUnit: 1000,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100705',
|
||||||
|
companyName: 'HDFC Mutual Fund',
|
||||||
|
city: 'Mumbai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'SBIN',
|
scriptCode: 'SBIN',
|
||||||
scriptName: 'State Bank of India',
|
scriptName: 'State Bank of India',
|
||||||
instrumentType: 'SHARE',
|
instrumentType: 'SHARE',
|
||||||
isin: 'INE062A01020',
|
isin: 'INE062A01020',
|
||||||
faceValuePerUnit: 1
|
faceValuePerUnit: 1,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100706'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scriptCode: 'HDFCBANK',
|
scriptCode: 'HDFCBANK',
|
||||||
scriptName: 'HDFC Bank Ltd',
|
scriptName: 'HDFC Bank Ltd',
|
||||||
instrumentType: 'SHARE',
|
instrumentType: 'SHARE',
|
||||||
isin: 'INE040A01034',
|
isin: 'INE040A01034',
|
||||||
faceValuePerUnit: 1
|
faceValuePerUnit: 1,
|
||||||
|
slrType: 'NON_SLR',
|
||||||
|
glCode: '100706'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1561,23 +1964,164 @@ export const BROKER_MASTER: BrokerMaster[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface PartyMaster {
|
export interface PartyMaster {
|
||||||
|
/** Backend id once the party is saved (absent on the sample rows). */
|
||||||
|
id?: number;
|
||||||
partyCode: string;
|
partyCode: string;
|
||||||
partyName: string;
|
partyName: string;
|
||||||
partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY';
|
partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY';
|
||||||
panNo: string;
|
panNo?: string;
|
||||||
|
// Fields below follow the bank's existing CBS Bank / Party Master.
|
||||||
|
bondType?: InstrumentType;
|
||||||
|
address1?: string;
|
||||||
|
address2?: string;
|
||||||
|
address3?: string;
|
||||||
|
city?: string;
|
||||||
|
pincode?: string;
|
||||||
|
phone?: string;
|
||||||
|
fax?: string;
|
||||||
|
bankType?: string;
|
||||||
|
/** GL this party is mapped to. */
|
||||||
|
glAccountId?: number;
|
||||||
|
glCode?: string;
|
||||||
|
glName?: string;
|
||||||
|
/** Interest income GL of the FDs placed with this bank (Bond Type FD). */
|
||||||
|
interestGlAccountId?: number;
|
||||||
|
interestGlCode?: string;
|
||||||
|
interestGlName?: string;
|
||||||
|
/** Whether this bank deducts TDS from FD interest (some do, some do not), and at what rate (%). */
|
||||||
|
tdsApplicable?: boolean;
|
||||||
|
tdsRate?: number | null;
|
||||||
|
rbiCode?: string;
|
||||||
|
/** Exposure limit for this party (₹). */
|
||||||
|
limit?: number | null;
|
||||||
|
leiNo?: string;
|
||||||
|
// Counterparty's bank details
|
||||||
|
cpBankName?: string;
|
||||||
|
cpBranchName?: string;
|
||||||
|
cpAccountNo?: string;
|
||||||
|
cpIfsc?: string;
|
||||||
|
/** Share institutions (Bond Type SHARE): DCCB, State Co-op Bank, co-operative society or membership (NPCI, CCIL, ...). */
|
||||||
|
institutionType?: string;
|
||||||
|
/** Subscribed share capital of the institution (₹): our holding in one society is limited to 5% of it. */
|
||||||
|
subscribedCapital?: number | null;
|
||||||
|
/** Latest balance sheet received (DD-MM-YYYY). */
|
||||||
|
lastBalanceSheetDate?: string;
|
||||||
|
inLiquidation?: boolean;
|
||||||
|
/** Absent means ACTIVE. An INACTIVE party is not offered for new deals. */
|
||||||
|
status?: 'ACTIVE' | 'INACTIVE';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sample Bank Type options until the bank confirms its list. */
|
||||||
|
export const BANK_TYPE_OPTIONS = [
|
||||||
|
'SCHEDULED COMMERCIAL BANK',
|
||||||
|
'STATE CO-OPERATIVE BANK',
|
||||||
|
'DISTRICT CENTRAL CO-OPERATIVE BANK',
|
||||||
|
'URBAN CO-OPERATIVE BANK',
|
||||||
|
'RBI / GOVERNMENT',
|
||||||
|
'OTHER'
|
||||||
|
];
|
||||||
|
|
||||||
export const PARTY_MASTER: PartyMaster[] = [
|
export const PARTY_MASTER: PartyMaster[] = [
|
||||||
{ partyCode: 'PTY-001', partyName: 'Jila Sahakari Kendriya Bank', partyType: 'BANK', panNo: 'AABCJ1234K' },
|
{
|
||||||
{ partyCode: 'PTY-002', partyName: 'State Cooperative Bank Ltd', partyType: 'BANK', panNo: 'AABCS5678L' },
|
partyCode: 'PTY-001',
|
||||||
{ partyCode: 'PTY-003', partyName: 'RBI - SGL', partyType: 'GOVT', panNo: 'AAACR4849R' },
|
partyName: 'Jila Sahakari Kendriya Bank',
|
||||||
{ partyCode: 'PTY-004', partyName: 'REC Limited', partyType: 'CORPORATE', panNo: 'AAACR0641G' },
|
partyType: 'BANK',
|
||||||
{ partyCode: 'PTY-005', partyName: 'NABARD', partyType: 'CORPORATE', panNo: 'AAACN0272B' },
|
bondType: 'FD',
|
||||||
{ partyCode: 'PTY-006', partyName: 'L&T Finance Ltd', partyType: 'CORPORATE', panNo: 'AAACL0140P' },
|
glCode: '100704',
|
||||||
{ partyCode: 'PTY-007', partyName: 'SBI Mutual Fund', partyType: 'AMC', panNo: 'AAATS4325L' },
|
city: 'Vidisha',
|
||||||
{ partyCode: 'PTY-008', partyName: 'HDFC Mutual Fund', partyType: 'AMC', panNo: 'AAATH1234M' },
|
bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
|
||||||
{ partyCode: 'PTY-009', partyName: 'NSE/BSE', partyType: 'EXCHANGE', panNo: 'AAACN0028G' },
|
status: 'ACTIVE'
|
||||||
{ partyCode: 'PTY-010', partyName: 'District Cooperative Bank', partyType: 'COOP_SOCIETY', panNo: 'AABCD9988K' }
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-002',
|
||||||
|
partyName: 'State Cooperative Bank Ltd',
|
||||||
|
partyType: 'BANK',
|
||||||
|
bondType: 'FD',
|
||||||
|
glCode: '100704',
|
||||||
|
city: 'Bhopal',
|
||||||
|
bankType: 'STATE CO-OPERATIVE BANK',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-003',
|
||||||
|
partyName: 'RBI - SGL',
|
||||||
|
partyType: 'GOVT',
|
||||||
|
bondType: 'GOVT_SECURITY',
|
||||||
|
glCode: '100701',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'RBI / GOVERNMENT',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-004',
|
||||||
|
partyName: 'REC Limited',
|
||||||
|
partyType: 'CORPORATE',
|
||||||
|
bondType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
city: 'New Delhi',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-005',
|
||||||
|
partyName: 'NABARD',
|
||||||
|
partyType: 'CORPORATE',
|
||||||
|
bondType: 'NON_SLR',
|
||||||
|
glCode: '100703',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-006',
|
||||||
|
partyName: 'L&T Finance Ltd',
|
||||||
|
partyType: 'CORPORATE',
|
||||||
|
bondType: 'BOND',
|
||||||
|
glCode: '100703',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-007',
|
||||||
|
partyName: 'SBI Mutual Fund',
|
||||||
|
partyType: 'AMC',
|
||||||
|
bondType: 'MUTUAL_FUND',
|
||||||
|
glCode: '100705',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-008',
|
||||||
|
partyName: 'HDFC Mutual Fund',
|
||||||
|
partyType: 'AMC',
|
||||||
|
bondType: 'MUTUAL_FUND',
|
||||||
|
glCode: '100705',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-009',
|
||||||
|
partyName: 'NSE/BSE',
|
||||||
|
partyType: 'EXCHANGE',
|
||||||
|
bondType: 'SHARE',
|
||||||
|
glCode: '100706',
|
||||||
|
city: 'Mumbai',
|
||||||
|
bankType: 'OTHER',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyCode: 'PTY-010',
|
||||||
|
partyName: 'District Cooperative Bank',
|
||||||
|
partyType: 'COOP_SOCIETY',
|
||||||
|
bondType: 'SHARE',
|
||||||
|
glCode: '100706',
|
||||||
|
city: 'Vidisha',
|
||||||
|
bankType: 'DISTRICT CENTRAL CO-OPERATIVE BANK',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface SubTypeMaster {
|
export interface SubTypeMaster {
|
||||||
@@ -1593,7 +2137,11 @@ export const SUBTYPE_MASTER: SubTypeMaster[] = [
|
|||||||
{ subTypeCode: '4', subTypeName: 'EQUITY', instrumentType: 'SHARE' },
|
{ subTypeCode: '4', subTypeName: 'EQUITY', instrumentType: 'SHARE' },
|
||||||
{ subTypeCode: '5', subTypeName: 'CO-OP SHARE', instrumentType: 'SHARE' },
|
{ subTypeCode: '5', subTypeName: 'CO-OP SHARE', instrumentType: 'SHARE' },
|
||||||
{ subTypeCode: '6', subTypeName: 'DEBT - OVERNIGHT', instrumentType: 'MUTUAL_FUND' },
|
{ subTypeCode: '6', subTypeName: 'DEBT - OVERNIGHT', instrumentType: 'MUTUAL_FUND' },
|
||||||
{ subTypeCode: '7', subTypeName: 'DEBT - LIQUID', instrumentType: 'MUTUAL_FUND' }
|
{ subTypeCode: '7', subTypeName: 'DEBT - LIQUID', instrumentType: 'MUTUAL_FUND' },
|
||||||
|
{ subTypeCode: '8', subTypeName: 'DEBT - MONEY MARKET', instrumentType: 'MUTUAL_FUND' },
|
||||||
|
{ subTypeCode: '9', subTypeName: 'DEBT - BANKING & PSU', instrumentType: 'MUTUAL_FUND' },
|
||||||
|
{ subTypeCode: '10', subTypeName: 'DEBT - CORPORATE BOND', instrumentType: 'MUTUAL_FUND' },
|
||||||
|
{ subTypeCode: '11', subTypeName: 'DEBT - GILT', instrumentType: 'MUTUAL_FUND' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ControlParameter {
|
export interface ControlParameter {
|
||||||
@@ -1725,13 +2273,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[] = [
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pg">
|
||||||
|
<header class="pg-head">
|
||||||
|
<div>
|
||||||
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · DUE DATE DIARY</div>
|
||||||
|
<h1 class="pg-title">Due Date Diary</h1>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Coupons, 31 Mar / 30 Sep provisions, premium amortisation, maturities and FD interest due in a period, with what
|
||||||
|
is posted and what is still pending.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From</span><mifosx-date-ddmmyyyy [(ngModel)]="fromDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To</span><mifosx-date-ddmmyyyy [(ngModel)]="toDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Investment</span>
|
||||||
|
<select [(ngModel)]="instrument">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="SECURITY">Govt Security / Bond</option>
|
||||||
|
<option value="FD">FD with banks</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Status</span>
|
||||||
|
<select [(ngModel)]="status">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="POSTED">Posted</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="diary-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(7)">Next 7 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(30)">Next 30 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(90)">Next 90 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="showOverdue()">Overdue</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (error) {
|
||||||
|
<p class="diary-error">{{ error }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (diary; as d) {
|
||||||
|
<div class="diary-summary">
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Entries</span><span class="diary-tile-v">{{ rows.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Pending</span><span class="diary-tile-v">{{ d.pendingCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile" [class.diary-tile--bad]="d.overdueCount > 0">
|
||||||
|
<span class="diary-tile-l">Overdue</span><span class="diary-tile-v">{{ d.overdueCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Money expected (not yet received)</span
|
||||||
|
><span class="diary-tile-v">₹{{ d.expectedInflow | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">{{ d.fromDate }} to {{ d.toDate }} · as on {{ d.asOnDate }}</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Due</th>
|
||||||
|
<th>Investment</th>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of rows; track r.instrument + r.refId + r.kind + r.date) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.date }}</td>
|
||||||
|
<td>{{ kindLabels[r.kind] }}</td>
|
||||||
|
<td>{{ r.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ r.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.particulars }}</td>
|
||||||
|
<td class="num">
|
||||||
|
{{ r.amount !== null && r.amount !== undefined ? '₹' + (r.amount | number: '1.2-2') : '—' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="inv-badge" [ngClass]="'diary-badge--' + r.status.toLowerCase()">{{ statusLabel(r) }}</span>
|
||||||
|
@if (r.voucherNo) {
|
||||||
|
<br /><small>{{ r.voucherNo }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status !== 'POSTED') {
|
||||||
|
<a class="diary-open" [routerLink]="link(r).path" [queryParams]="link(r).query">Open →</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="pg-empty">Nothing falls due in this period.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.diary-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-error {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&--bad {
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile-l {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile-v {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--posted {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--due {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--overdue {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #b91c1c;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--upcoming {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-open {
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-total td {
|
||||||
|
font-weight: 700;
|
||||||
|
border-top: 2px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-pick {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--on td {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import { addPeriod, todayDDMMYYYY } from '../investment-data';
|
||||||
|
import { InvestmentDiaryDto, InvestmentDiaryRowDto, InvestmentService, apiErrorMessage } from '../investment.service';
|
||||||
|
|
||||||
|
type InstrumentFilter = '' | 'SECURITY' | 'FD';
|
||||||
|
type StatusFilter = '' | 'PENDING' | 'POSTED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due Date Diary for All Investment: coupons, 31 Mar / 30 Sep provisions, premium amortisation, maturities and FD
|
||||||
|
* payouts in a period, with what is posted, due or overdue, and a link to the screen that posts each one.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-investment-diary',
|
||||||
|
templateUrl: './investment-diary.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../investment-shared.scss',
|
||||||
|
'./investment-diary.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InvestmentDiaryComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
// Opens on everything still to be done: what is pending from the past year and what falls due in the next 30 days.
|
||||||
|
fromDate = addPeriod(todayDDMMYYYY(), -1, 0, 0);
|
||||||
|
toDate = addPeriod(todayDDMMYYYY(), 0, 0, 30);
|
||||||
|
instrument: InstrumentFilter = '';
|
||||||
|
status: StatusFilter = 'PENDING';
|
||||||
|
diary: InvestmentDiaryDto | null = null;
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
readonly kindLabels: Record<InvestmentDiaryRowDto['kind'], string> = {
|
||||||
|
COUPON: 'Coupon',
|
||||||
|
PROVISION: 'Interest provision',
|
||||||
|
MATURITY: 'Maturity / redemption',
|
||||||
|
AMORTISATION: 'Premium amortisation',
|
||||||
|
FD_PROVISION: 'FD interest provision',
|
||||||
|
FD_PAYOUT: 'FD interest payout',
|
||||||
|
FD_MATURITY: 'FD maturity'
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick ranges from today. */
|
||||||
|
setRange(days: number): void {
|
||||||
|
this.status = '';
|
||||||
|
this.fromDate = todayDDMMYYYY();
|
||||||
|
this.toDate = addPeriod(this.fromDate, 0, 0, days);
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What is still pending from the past (up to a year back) and today. */
|
||||||
|
showOverdue(): void {
|
||||||
|
this.toDate = todayDDMMYYYY();
|
||||||
|
this.fromDate = addPeriod(this.toDate, -1, 0, 0);
|
||||||
|
this.status = 'PENDING';
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (!this.fromDate || !this.toDate) {
|
||||||
|
this.error = 'Enter the From and To dates.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.investmentService.getDiary(this.fromDate, this.toDate).subscribe({
|
||||||
|
next: (d) => {
|
||||||
|
this.diary = d;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.diary = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows(): InvestmentDiaryRowDto[] {
|
||||||
|
return (this.diary?.rows ?? []).filter(
|
||||||
|
(r) =>
|
||||||
|
(!this.instrument || r.instrument === this.instrument) &&
|
||||||
|
(!this.status || (this.status === 'POSTED' ? r.status === 'POSTED' : r.status !== 'POSTED'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The screen that posts this entry. */
|
||||||
|
link(r: InvestmentDiaryRowDto): { path: string; query: Record<string, string | number> } {
|
||||||
|
if (r.instrument === 'FD') {
|
||||||
|
return { path: '/investment/fd', query: { tab: 'interest', fd: r.refId } };
|
||||||
|
}
|
||||||
|
const tab = r.kind === 'MATURITY' ? 'redemption' : r.kind === 'AMORTISATION' ? 'amortize' : 'interest';
|
||||||
|
return { path: '/investment/bonds', query: { tab, purchase: r.refId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel(r: InvestmentDiaryRowDto): string {
|
||||||
|
return r.status === 'DUE' ? 'Due today' : r.status.charAt(0) + r.status.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pg">
|
||||||
|
<header class="pg-head">
|
||||||
|
<div>
|
||||||
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||||
|
<h1 class="pg-title">Total Interest Received & Receivable</h1>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Interest received from Govt Securities, bonds and FDs in the period, and interest earned up to the To date that
|
||||||
|
has not come in yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="!report || generating" (click)="exportExcel()">
|
||||||
|
{{ generating ? 'Generating…' : 'Export to Excel' }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From</span><mifosx-date-ddmmyyyy [(ngModel)]="fromDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To (receivable as on)</span><mifosx-date-ddmmyyyy [(ngModel)]="toDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Investment</span>
|
||||||
|
<select [(ngModel)]="instrument">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="SECURITY">Govt Security / Bond</option>
|
||||||
|
<option value="FD">FD with banks</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="diary-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (error) {
|
||||||
|
<p class="diary-error">{{ error }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (report; as r) {
|
||||||
|
<div class="diary-summary">
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Received</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.received | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Receivable</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.receivable | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Of which provisioned (100603)</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.provisioned | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Total</span><span class="diary-tile-v">₹{{ totals.total | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">
|
||||||
|
{{ r.fromDate }} to {{ r.toDate }} · received in the period, receivable as on {{ r.toDate }}
|
||||||
|
</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Investment</th>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="num">Principal / Face Value</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
|
<th class="num col-in">Received</th>
|
||||||
|
<th class="num">Receivable</th>
|
||||||
|
<th class="num">Of which provisioned</th>
|
||||||
|
<th class="num">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (x of rows; track x.instrument + x.refId) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ x.particulars }}</td>
|
||||||
|
<td>{{ x.status }}</td>
|
||||||
|
<td class="num">₹{{ x.principal | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ x.rate !== null && x.rate !== undefined ? x.rate + '%' : '—' }}</td>
|
||||||
|
<td class="num col-in">₹{{ x.received | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.receivable | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.provisioned | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.total | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="pg-empty">No interest received or receivable in this period.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
@if (rows.length) {
|
||||||
|
<tfoot>
|
||||||
|
<tr class="report-total">
|
||||||
|
<td colspan="6">Total</td>
|
||||||
|
<td class="num col-in">₹{{ totals.received | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.receivable | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.provisioned | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.total | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import { parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
|
||||||
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentInterestReportDto,
|
||||||
|
InvestmentInterestReportRowDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
|
type InstrumentFilter = '' | 'SECURITY' | 'FD';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total Interest Received & Receivable: per Govt Security / bond deal and FD, the interest received in a period and the
|
||||||
|
* interest earned up to its last day that has not come in yet, with the part already provisioned in the books.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-investment-interest-report',
|
||||||
|
templateUrl: './investment-interest-report.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../investment-shared.scss',
|
||||||
|
'../investment-diary/investment-diary.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InvestmentInterestReportComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
/** Defaults to the current financial year, 1 April to today. */
|
||||||
|
fromDate = InvestmentInterestReportComponent.financialYearStart();
|
||||||
|
toDate = todayDDMMYYYY();
|
||||||
|
instrument: InstrumentFilter = '';
|
||||||
|
report: InvestmentInterestReportDto | null = null;
|
||||||
|
loading = false;
|
||||||
|
generating = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static financialYearStart(): string {
|
||||||
|
const d = parseDDMMYYYY(todayDDMMYYYY());
|
||||||
|
const year = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
|
||||||
|
return `01-04-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (!this.fromDate || !this.toDate) {
|
||||||
|
this.error = 'Enter the From and To dates.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.investmentService.getInterestReport(this.fromDate, this.toDate).subscribe({
|
||||||
|
next: (r) => {
|
||||||
|
this.report = r;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.report = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows(): InvestmentInterestReportRowDto[] {
|
||||||
|
return (this.report?.rows ?? []).filter((r) => !this.instrument || r.instrument === this.instrument);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Totals of the rows shown, so they follow the Investment filter. */
|
||||||
|
get totals(): { received: number; receivable: number; provisioned: number; total: number } {
|
||||||
|
return this.rows.reduce(
|
||||||
|
(t, r) => ({
|
||||||
|
received: t.received + r.received,
|
||||||
|
receivable: t.receivable + r.receivable,
|
||||||
|
provisioned: t.provisioned + r.provisioned,
|
||||||
|
total: t.total + r.total
|
||||||
|
}),
|
||||||
|
{ received: 0, receivable: 0, provisioned: 0, total: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportExcel(): Promise<void> {
|
||||||
|
if (!this.report) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.generating = true;
|
||||||
|
try {
|
||||||
|
const t = this.totals;
|
||||||
|
await exportWorkbook(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
sheetName: 'Interest Received',
|
||||||
|
title: 'Total Interest Received & Receivable',
|
||||||
|
subtitle: `${this.report.fromDate} to ${this.report.toDate}`,
|
||||||
|
headers: [
|
||||||
|
'Investment',
|
||||||
|
'Deal No',
|
||||||
|
'Particulars',
|
||||||
|
'Status',
|
||||||
|
'Principal / Face Value',
|
||||||
|
'Rate %',
|
||||||
|
'Received',
|
||||||
|
'Receivable',
|
||||||
|
'Of which provisioned',
|
||||||
|
'Total'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
14,
|
||||||
|
16,
|
||||||
|
40,
|
||||||
|
12,
|
||||||
|
18,
|
||||||
|
8,
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
18,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...this.rows.map((r) => [
|
||||||
|
r.instrument === 'FD' ? 'FD' : 'Govt Security / Bond',
|
||||||
|
r.dealNo,
|
||||||
|
r.particulars,
|
||||||
|
r.status,
|
||||||
|
r.principal,
|
||||||
|
r.rate ?? null,
|
||||||
|
r.received,
|
||||||
|
r.receivable,
|
||||||
|
r.provisioned,
|
||||||
|
r.total
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
t.received,
|
||||||
|
t.receivable,
|
||||||
|
t.provisioned,
|
||||||
|
t.total
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
`Interest-Received-Receivable-${this.report.toDate}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.generating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,38 +106,163 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PARTY MASTER -->
|
<!-- PARTY MASTER (layout follows the bank's existing CBS Bank / Party Master) -->
|
||||||
@if (activeTab === 'party') {
|
@if (activeTab === 'party') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Add Bank / Party</div>
|
<div class="pos-section-label">Bank / Party Master — Entry</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Party Name *</span><input type="text" [(ngModel)]="newParty.partyName"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Party Type *</span>
|
<span>* Bond Type</span>
|
||||||
<select [(ngModel)]="newParty.partyType">
|
<select [(ngModel)]="partyForm.bondType">
|
||||||
<option [ngValue]="undefined">-- Select --</option>
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
<option value="BANK">BANK</option>
|
@for (t of instrumentTypes; track t) {
|
||||||
<option value="CORPORATE">CORPORATE</option>
|
<option [value]="t">{{ t }}</option>
|
||||||
<option value="GOVT">GOVT</option>
|
}
|
||||||
<option value="AMC">AMC</option>
|
|
||||||
<option value="EXCHANGE">EXCHANGE</option>
|
|
||||||
<option value="COOP_SOCIETY">COOP SOCIETY</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"><span>PAN No</span><input type="text" [(ngModel)]="newParty.panNo" /></label>
|
<label class="inv-form-field"
|
||||||
|
><span>* Party Code</span><input type="text" [value]="partyForm.partyCode || nextPartyCode" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Party Name</span><input type="text" [(ngModel)]="partyForm.partyName"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 1 (Building / Flat No.)</span
|
||||||
|
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="partyForm.address1"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 2 (Street / Area)</span
|
||||||
|
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="partyForm.address2"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 3 (Locality / Landmark)</span
|
||||||
|
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="partyForm.address3"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="partyForm.city" /></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="partyForm.pincode"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="partyForm.phone" /></label>
|
||||||
|
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="partyForm.fax" /></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Bank Type</span>
|
||||||
|
<select [(ngModel)]="partyForm.bankType">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (b of bankTypeOptions; track b) {
|
||||||
|
<option [value]="b">{{ b }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* GL A/c No.</span>
|
||||||
|
<select [(ngModel)]="partyForm.glAccountId">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (g of partyGlOptions; track g.id) {
|
||||||
|
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@if (partyForm.bondType === 'FD') {
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Interest GL</span>
|
||||||
|
<select [(ngModel)]="partyForm.interestGlAccountId">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (g of interestGlOptions; track g.id) {
|
||||||
|
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Bank deducts TDS on FD interest?</span>
|
||||||
|
<select [ngModel]="partyForm.tdsApplicable ?? false" (ngModelChange)="partyForm.tdsApplicable = $event">
|
||||||
|
<option [ngValue]="false">No</option>
|
||||||
|
<option [ngValue]="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@if (partyForm.tdsApplicable) {
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* TDS rate (%)</span
|
||||||
|
><input type="number" min="0" max="99" step="0.01" [(ngModel)]="partyForm.tdsRate" placeholder="e.g. 10"
|
||||||
|
/></label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (partyForm.bondType === 'SHARE') {
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Institution Type</span>
|
||||||
|
<select [(ngModel)]="partyForm.institutionType">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (t of shareInstitutionTypes; track t.value) {
|
||||||
|
<option [value]="t.value">{{ t.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span
|
||||||
|
>{{ partyForm.institutionType === 'OTHER_COOP_SOCIETY' ? '* ' : '' }}Subscribed Share Capital (₹)</span
|
||||||
|
><input type="number" min="0" [(ngModel)]="partyForm.subscribedCapital"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Last Balance Sheet Date</span
|
||||||
|
><mifosx-date-ddmmyyyy [(ngModel)]="partyForm.lastBalanceSheetDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>In Liquidation?</span>
|
||||||
|
<select [ngModel]="partyForm.inLiquidation ?? false" (ngModelChange)="partyForm.inLiquidation = $event">
|
||||||
|
<option [ngValue]="false">No</option>
|
||||||
|
<option [ngValue]="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
<label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Limit (₹)</span><input type="number" min="0" [(ngModel)]="partyForm.limit"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>LEI No</span><input type="text" [(ngModel)]="partyForm.leiNo" /></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Status</span>
|
||||||
|
<select [(ngModel)]="partyForm.status">
|
||||||
|
<option [ngValue]="undefined">ACTIVE</option>
|
||||||
|
<option value="INACTIVE">INACTIVE</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="pos-section-label">Counterparty's Bank Details</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Name of Bank</span><input type="text" [(ngModel)]="partyForm.cpBankName"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Name of Branch</span><input type="text" [(ngModel)]="partyForm.cpBranchName"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Account No.</span><input type="text" [(ngModel)]="partyForm.cpAccountNo"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>IFSC</span><input type="text" maxlength="11" [(ngModel)]="partyForm.cpIfsc"
|
||||||
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addParty()">Save</button>
|
<label class="inv-form-field"
|
||||||
|
><span>Search Party Code</span><input type="text" placeholder="e.g. PTY-001" [(ngModel)]="partySearchCode"
|
||||||
|
/></label>
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveParty()">Save</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchParty()">Search</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Cancel</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropParty()">Drop</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelParty()">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Party Code</th>
|
<th>Party Code</th>
|
||||||
|
<th>Bond Type</th>
|
||||||
<th>Party Name</th>
|
<th>Party Name</th>
|
||||||
<th>Type</th>
|
<th>City</th>
|
||||||
<th>PAN No</th>
|
<th>Bank Type</th>
|
||||||
|
<th>GL A/c</th>
|
||||||
|
<th class="num">Limit</th>
|
||||||
|
<th>Counterparty Bank / IFSC</th>
|
||||||
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -146,9 +271,19 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ p.partyCode }}</span>
|
<span class="pg-chip">{{ p.partyCode }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ p.partyName }}</td>
|
<td>{{ p.bondType || '—' }}</td>
|
||||||
<td>{{ p.partyType }}</td>
|
<td>
|
||||||
<td>{{ p.panNo }}</td>
|
{{ p.partyName }}
|
||||||
|
@if (p.institutionType) {
|
||||||
|
<small>({{ p.institutionType }}{{ p.inLiquidation ? ', in liquidation' : '' }})</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.city || '—' }}</td>
|
||||||
|
<td>{{ p.bankType || '—' }}</td>
|
||||||
|
<td>{{ p.glCode ? p.glCode + ' — ' + glName(p.glCode) : '—' }}</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.status ?? 'ACTIVE' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -156,42 +291,180 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- SCRIPT MASTER -->
|
<!-- SCRIPT MASTER (layout follows the bank's existing CBS Script Master) -->
|
||||||
@if (activeTab === 'script') {
|
@if (activeTab === 'script') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Add Script</div>
|
<div class="pos-section-label">Script Master — Entry</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Script Code</span><input type="text" [(ngModel)]="newScript.scriptCode"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Script Name *</span><input type="text" [(ngModel)]="newScript.scriptName"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Instrument Type *</span>
|
<span>* Bond Type</span>
|
||||||
<select [(ngModel)]="newScript.instrumentType">
|
<select [(ngModel)]="scriptForm.instrumentType" [disabled]="!!scriptForm.scriptCode">
|
||||||
<option [ngValue]="undefined">-- Select --</option>
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
@for (t of instrumentTypes; track t) {
|
@for (t of instrumentTypes; track t) {
|
||||||
<option [value]="t">{{ t }}</option>
|
<option [value]="t">{{ t }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"><span>ISIN</span><input type="text" [(ngModel)]="newScript.isin" /></label>
|
<label class="inv-form-field">
|
||||||
|
<span>SLR</span>
|
||||||
|
<select [(ngModel)]="scriptForm.slrType">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (o of slrTypeOptions; track o.value) {
|
||||||
|
<option [value]="o.value">{{ o.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Face Value / Unit</span><input type="number" [(ngModel)]="newScript.faceValuePerUnit"
|
><span>* Script Code</span><input type="text" [value]="scriptForm.scriptCode || nextScriptCode" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Script Desc</span
|
||||||
|
><input type="text" placeholder="e.g. 7.50%GOI2034" [(ngModel)]="scriptForm.scriptName"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* GL A/c No.</span>
|
||||||
|
<select [(ngModel)]="scriptForm.glAccountId">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (g of scriptGlOptions; track g.id) {
|
||||||
|
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ scriptForm.instrumentType === 'SHARE' ? 'Face Value per Share' : 'Face Value per Unit' }}</span
|
||||||
|
><input type="number" min="0" step="0.01" placeholder="100" [(ngModel)]="scriptForm.faceValuePerUnit"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Int Rate %</span
|
||||||
|
><input type="number" step="0.01" min="0" [(ngModel)]="scriptForm.couponRate"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Issue Date</span
|
||||||
|
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.issueDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}Redum / Mat Date</span
|
||||||
|
><mifosx-date-ddmmyyyy [(ngModel)]="scriptForm.maturityDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Period (Year)</span><input type="text" [value]="scriptPeriod.years" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Period (Month)</span><input type="text" [value]="scriptPeriod.months" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Period (Days)</span><input type="text" [value]="scriptPeriod.days" readonly
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
|
@if (scriptIsRated) {
|
||||||
|
<div class="pos-section-label">Credit Rating (RBI: bonds "A" or higher, CP / CD "A2" or higher)</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Rating</span>
|
||||||
|
<select [(ngModel)]="scriptForm.rating">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (g of ratingOptions; track g.group) {
|
||||||
|
<optgroup [label]="g.group">
|
||||||
|
@for (r of g.values; track r) {
|
||||||
|
<option [value]="r">{{ r }}</option>
|
||||||
|
}
|
||||||
|
</optgroup>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Rating Agency</span>
|
||||||
|
<select [(ngModel)]="scriptForm.ratingAgency">
|
||||||
|
<option [ngValue]="undefined">-- Select --</option>
|
||||||
|
@for (a of ratingAgencyOptions; track a.value) {
|
||||||
|
<option [value]="a.value">{{ a.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Listing</span>
|
||||||
|
<select
|
||||||
|
[ngModel]="scriptForm.listed === false ? 'UNLISTED' : 'LISTED'"
|
||||||
|
(ngModelChange)="scriptForm.listed = $event !== 'UNLISTED'"
|
||||||
|
>
|
||||||
|
<option value="LISTED">Listed / proposed to be listed</option>
|
||||||
|
<option value="UNLISTED">Unlisted</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@if (scriptForm.rating) {
|
||||||
|
<p class="inv-hint" [class.inv-hint--bad]="!scriptRatingEligible">
|
||||||
|
{{
|
||||||
|
scriptRatingEligible
|
||||||
|
? 'Rating meets the RBI minimum; this security can be bought.'
|
||||||
|
: 'Below the RBI minimum rating, or the wrong scale (CP / CD up to one year: A1+ to A4; bond: AAA to D).'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<div class="pos-section-label">Company Details (Bond / Mutual Fund)</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Company Name</span><input type="text" [(ngModel)]="scriptForm.companyName"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 1 (Building / Flat No.)</span
|
||||||
|
><input type="text" placeholder="e.g. Shop No. 12, Shanti Complex" [(ngModel)]="scriptForm.address1"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 2 (Street / Area)</span
|
||||||
|
><input type="text" placeholder="e.g. MG Road, Near Bus Stand" [(ngModel)]="scriptForm.address2"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Address Line 3 (Locality / Landmark)</span
|
||||||
|
><input type="text" placeholder="e.g. Arera Colony" [(ngModel)]="scriptForm.address3"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>City</span><input type="text" [(ngModel)]="scriptForm.city" /></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Pincode</span><input type="text" maxlength="6" [(ngModel)]="scriptForm.pincode"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>Phone</span><input type="text" [(ngModel)]="scriptForm.phone" /></label>
|
||||||
|
<label class="inv-form-field"><span>Fax</span><input type="text" [(ngModel)]="scriptForm.fax" /></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Corp. Code</span><input type="text" [(ngModel)]="scriptForm.corpCode"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Status</span>
|
||||||
|
<select
|
||||||
|
[ngModel]="scriptForm.active === false ? 'INACTIVE' : 'ACTIVE'"
|
||||||
|
(ngModelChange)="scriptForm.active = $event !== 'INACTIVE'"
|
||||||
|
>
|
||||||
|
<option value="ACTIVE">ACTIVE</option>
|
||||||
|
<option value="INACTIVE">INACTIVE</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addScript()">Save</button>
|
<label class="inv-form-field"
|
||||||
|
><span>Search Script Code</span><input type="text" placeholder="e.g. G00001" [(ngModel)]="scriptSearchCode"
|
||||||
|
/></label>
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveScript()">Save</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchScript()">Search</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Cancel</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelScript()">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Script Code</th>
|
<th>Script Code</th>
|
||||||
<th>Script Name</th>
|
<th>Bond Type</th>
|
||||||
<th>Instrument</th>
|
<th>SLR</th>
|
||||||
|
<th>Script Desc</th>
|
||||||
|
<th>GL A/c</th>
|
||||||
<th>ISIN</th>
|
<th>ISIN</th>
|
||||||
<th class="num">Face Value/Unit</th>
|
<th class="num">Int Rate %</th>
|
||||||
|
<th>Issue Date</th>
|
||||||
|
<th>Maturity</th>
|
||||||
|
<th>Rating</th>
|
||||||
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -200,10 +473,16 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ s.scriptCode }}</span>
|
<span class="pg-chip">{{ s.scriptCode }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ s.scriptName }}</td>
|
|
||||||
<td>{{ s.instrumentType }}</td>
|
<td>{{ s.instrumentType }}</td>
|
||||||
|
<td>{{ s.slrType || '—' }}</td>
|
||||||
|
<td>{{ s.scriptName }}</td>
|
||||||
|
<td>{{ s.glCode || '—' }}</td>
|
||||||
<td>{{ s.isin || '—' }}</td>
|
<td>{{ s.isin || '—' }}</td>
|
||||||
<td class="num">{{ s.faceValuePerUnit }}</td>
|
<td class="num">{{ s.couponRate ?? '—' }}</td>
|
||||||
|
<td>{{ s.issueDate || '—' }}</td>
|
||||||
|
<td>{{ s.maturityDate || '—' }}</td>
|
||||||
|
<td>{{ s.rating ? s.rating + (s.listed === false ? ' · Unlisted' : '') : '—' }}</td>
|
||||||
|
<td>{{ s.active === false ? 'INACTIVE' : 'ACTIVE' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -361,21 +640,36 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Open Rate</span><input type="number" [(ngModel)]="newDailyRate.openRate" step="0.01"
|
><span>Open Rate</span><input type="number" [(ngModel)]="newDailyRate.openRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Close Rate *</span><input type="number" [(ngModel)]="newDailyRate.closeRate" step="0.01"
|
><span>Close Rate *</span><input type="number" [(ngModel)]="newDailyRate.closeRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>High Rate</span><input type="number" [(ngModel)]="newDailyRate.highRate" step="0.01"
|
><span>High Rate</span><input type="number" [(ngModel)]="newDailyRate.highRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Low Rate</span><input type="number" [(ngModel)]="newDailyRate.lowRate" step="0.01"
|
><span>Low Rate</span><input type="number" [(ngModel)]="newDailyRate.lowRate" step="0.0001"
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addDailyRate()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" (click)="addDailyRate()">Save</button>
|
||||||
|
<label class="pg-btn pg-btn--ghost">
|
||||||
|
{{ uploading ? 'Uploading…' : 'Upload CSV / Excel' }}
|
||||||
|
<input type="file" accept=".csv,.xlsx" hidden [disabled]="uploading" (change)="uploadDailyRates($event)" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
File columns: Script Code (or ISIN), Rate Date (DD-MM-YYYY), Close, Open, High, Low — first row is the header.
|
||||||
|
Use it for the day's market rates and mutual fund NAVs.
|
||||||
|
</p>
|
||||||
|
@if (uploadResults.length) {
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
@for (r of uploadResults; track r.row) {
|
||||||
|
<p class="inv-hint" [class.inv-hint--bad]="!r.ok">Row {{ r.row }}: {{ r.text }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -386,20 +680,29 @@
|
|||||||
<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>
|
||||||
<span class="pg-chip">{{ r.scriptCode }}</span>
|
<span class="pg-chip">{{ r.scriptCode }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ r.scriptName }}</td>
|
<td>{{ r.scriptName }}</td>
|
||||||
<td class="num">{{ r.openRate | number: '1.2-2' }}</td>
|
<!-- Mutual fund NAVs run to 4 decimals. -->
|
||||||
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.openRate | number: '1.2-4' }}</td>
|
||||||
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.highRate | number: '1.2-4' }}</td>
|
||||||
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
|
<td class="num">{{ r.lowRate | number: '1.2-4' }}</td>
|
||||||
|
<td class="num">{{ r.closeRate | number: '1.2-4' }}</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>
|
||||||
|
|||||||
@@ -6,20 +6,18 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import {
|
import {
|
||||||
SCRIPT_MASTER,
|
|
||||||
BROKER_MASTER,
|
BROKER_MASTER,
|
||||||
PARTY_MASTER,
|
|
||||||
SUBTYPE_MASTER,
|
SUBTYPE_MASTER,
|
||||||
CONTROL_PARAMETERS,
|
CONTROL_PARAMETERS,
|
||||||
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,
|
||||||
@@ -31,9 +29,26 @@ import {
|
|||||||
ScriptMarketDetail,
|
ScriptMarketDetail,
|
||||||
DailyScriptRate,
|
DailyScriptRate,
|
||||||
LienReason,
|
LienReason,
|
||||||
InstrumentType
|
InstrumentType,
|
||||||
|
BANK_TYPE_OPTIONS,
|
||||||
|
SLR_TYPE_OPTIONS,
|
||||||
|
INVESTMENT_GL_LOOKUP,
|
||||||
|
ELIGIBLE_RATINGS,
|
||||||
|
parseDDMMYYYY,
|
||||||
|
RATING_AGENCY_OPTIONS,
|
||||||
|
RATING_OPTIONS,
|
||||||
|
periodBetween
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentGlAccount,
|
||||||
|
InvestmentService,
|
||||||
|
SHARE_INSTITUTION_TYPES,
|
||||||
|
apiErrorMessage,
|
||||||
|
awaitingApproval,
|
||||||
|
sentForApproval,
|
||||||
|
toDailyScriptRate
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
type MasterTab =
|
type MasterTab =
|
||||||
| 'party'
|
| 'party'
|
||||||
@@ -51,6 +66,10 @@ type MasterTab =
|
|||||||
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
|
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
|
||||||
* lists, each with an inline Add-row form so the demo stays interactive without a backend.
|
* lists, each with an inline Add-row form so the demo stays interactive without a backend.
|
||||||
*/
|
*/
|
||||||
|
/** Detail GLs under the 100700 INVESTMENTS head; 100707 holds premium only, never a party's or script's GL. */
|
||||||
|
const INVESTMENT_GL_HEAD_PREFIX = '1007';
|
||||||
|
const PREMIUM_ON_INVESTMENTS_GL_CODE = '100707';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-investment-masters',
|
selector: 'mifosx-investment-masters',
|
||||||
templateUrl: './investment-masters.component.html',
|
templateUrl: './investment-masters.component.html',
|
||||||
@@ -65,18 +84,19 @@ type MasterTab =
|
|||||||
DateDdmmyyyyPickerComponent
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentMastersComponent {
|
export class InvestmentMastersComponent implements OnInit {
|
||||||
activeTab: MasterTab = 'party';
|
activeTab: MasterTab = 'party';
|
||||||
|
|
||||||
scripts: ScriptMaster[] = [...SCRIPT_MASTER];
|
scripts: ScriptMaster[] = [];
|
||||||
brokers: BrokerMaster[] = [...BROKER_MASTER];
|
brokers: BrokerMaster[] = [...BROKER_MASTER];
|
||||||
parties: PartyMaster[] = [...PARTY_MASTER];
|
parties: PartyMaster[] = [];
|
||||||
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
|
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
|
||||||
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
|
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
|
||||||
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[] = [
|
||||||
@@ -106,8 +126,24 @@ export class InvestmentMastersComponent {
|
|||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
newParty: Partial<PartyMaster> = {};
|
// Bank / Party Master and Script Master follow the bank's existing CBS screens (one entry form
|
||||||
newScript: Partial<ScriptMaster> = {};
|
// with Save / Search / Cancel / Drop / Exit) and are stored in the backend through
|
||||||
|
// InvestmentService; the other master tabs are still sample data.
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
partyForm: Partial<PartyMaster> = {};
|
||||||
|
partySearchCode = '';
|
||||||
|
scriptForm: Partial<ScriptMaster> = {};
|
||||||
|
scriptSearchCode = '';
|
||||||
|
saving = false;
|
||||||
|
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
||||||
|
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
|
||||||
|
readonly ratingOptions = RATING_OPTIONS;
|
||||||
|
readonly ratingAgencyOptions = RATING_AGENCY_OPTIONS;
|
||||||
|
readonly shareInstitutionTypes = SHARE_INSTITUTION_TYPES;
|
||||||
|
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
|
||||||
|
investmentGlOptions: InvestmentGlAccount[] = [];
|
||||||
|
/** Income GLs under 400400 INCOME FROM INVESTMENTS, for an FD bank's interest. */
|
||||||
|
interestGlOptions: InvestmentGlAccount[] = [];
|
||||||
newSubType: Partial<SubTypeMaster> = {};
|
newSubType: Partial<SubTypeMaster> = {};
|
||||||
newBroker: Partial<BrokerMaster> = {};
|
newBroker: Partial<BrokerMaster> = {};
|
||||||
newControlParam: Partial<ControlParameter> = {};
|
newControlParam: Partial<ControlParameter> = {};
|
||||||
@@ -116,43 +152,312 @@ export class InvestmentMastersComponent {
|
|||||||
newSglBank: Partial<SglBankMaster> = {};
|
newSglBank: Partial<SglBankMaster> = {};
|
||||||
newDailyRate: Partial<DailyScriptRate> = {};
|
newDailyRate: Partial<DailyScriptRate> = {};
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadDailyRates();
|
||||||
|
this.investmentService.getAssetGlAccounts().subscribe({
|
||||||
|
next: (gls) =>
|
||||||
|
(this.investmentGlOptions = gls.filter(
|
||||||
|
(g) => g.glCode.startsWith(INVESTMENT_GL_HEAD_PREFIX) && g.glCode !== PREMIUM_ON_INVESTMENTS_GL_CODE
|
||||||
|
)),
|
||||||
|
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
|
this.investmentService.getIncomeGlAccounts().subscribe({
|
||||||
|
next: (gls) => (this.interestGlOptions = gls.filter((g) => g.glCode.startsWith('4004'))),
|
||||||
|
error: (err) => this.toast(`Could not load income GLs: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
|
this.reloadMasters();
|
||||||
|
}
|
||||||
|
|
||||||
setTab(tab: MasterTab): void {
|
setTab(tab: MasterTab): void {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
addParty(): void {
|
/** Re-reads both masters from the backend (also refreshes the lists the Purchase screen uses). */
|
||||||
if (!this.newParty.partyName || !this.newParty.partyType) {
|
private reloadMasters(): void {
|
||||||
this.toast('Enter Party Name and Type.');
|
this.investmentService.syncSharedMasters().subscribe({
|
||||||
return;
|
next: ({ parties, scripts }) => {
|
||||||
}
|
this.parties = parties;
|
||||||
this.parties = [
|
this.scripts = scripts;
|
||||||
{
|
},
|
||||||
partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`,
|
error: (err) => this.toast(`Could not load masters: ${apiErrorMessage(err)}`)
|
||||||
panNo: '',
|
});
|
||||||
...this.newParty
|
|
||||||
} as PartyMaster,
|
|
||||||
...this.parties
|
|
||||||
];
|
|
||||||
this.newParty = {};
|
|
||||||
this.toast('Party/Bank master added.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addScript(): void {
|
glName(glCode?: string): string {
|
||||||
if (!this.newScript.scriptName || !this.newScript.instrumentType) {
|
return (
|
||||||
this.toast('Enter Script Name and Instrument Type.');
|
this.investmentGlOptions.find((g) => g.glCode === glCode)?.name ??
|
||||||
|
INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ??
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FD GLs are the bank-wise children of 100704 (codes 1007040x). */
|
||||||
|
private static isFdGl(glCode: string): boolean {
|
||||||
|
return glCode.startsWith('1007040');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An FD bank is mapped to its own FD GL; any other party to a non-FD investment GL. */
|
||||||
|
get partyGlOptions(): InvestmentGlAccount[] {
|
||||||
|
const fd = this.partyForm.bondType === 'FD';
|
||||||
|
return this.investmentGlOptions.filter((g) => InvestmentMastersComponent.isFdGl(g.glCode) === fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scripts are securities, never an FD with a bank. */
|
||||||
|
get scriptGlOptions(): InvestmentGlAccount[] {
|
||||||
|
return this.investmentGlOptions.filter((g) => !InvestmentMastersComponent.isFdGl(g.glCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Bank / Party Master --------------------
|
||||||
|
/** Preview of the code the backend will assign; the saved code always comes from the backend. */
|
||||||
|
get nextPartyCode(): string {
|
||||||
|
const nums = this.parties.map((p) => parseInt(p.partyCode.replace(/\D/g, ''), 10)).filter((n) => !isNaN(n));
|
||||||
|
return `PTY-${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveParty(): void {
|
||||||
|
const f = this.partyForm;
|
||||||
|
if (!f.bondType || !f.partyName || !f.glAccountId) {
|
||||||
|
this.toast('Enter Bond Type, Party Name and GL A/c No.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.scripts = [
|
if (f.bondType === 'FD' && !f.interestGlAccountId) {
|
||||||
{
|
this.toast('An FD bank needs its Interest GL as well.');
|
||||||
scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`,
|
return;
|
||||||
isin: '',
|
}
|
||||||
faceValuePerUnit: 100,
|
const tds = f.bondType === 'FD' && !!f.tdsApplicable;
|
||||||
...this.newScript
|
if (tds && !(Number(f.tdsRate) > 0 && Number(f.tdsRate) < 100)) {
|
||||||
} as ScriptMaster,
|
this.toast('Enter the TDS rate (%) this bank deducts on FD interest.');
|
||||||
...this.scripts
|
return;
|
||||||
];
|
}
|
||||||
this.newScript = {};
|
const share = f.bondType === 'SHARE';
|
||||||
this.toast('Script master added.');
|
if (share && !f.institutionType) {
|
||||||
|
this.toast('Select the Institution Type of this share institution.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (share && f.institutionType === 'OTHER_COOP_SOCIETY' && !(Number(f.subscribedCapital) > 0)) {
|
||||||
|
this.toast('Enter the subscribed share capital of the society (the 5% limit on one society).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
bondType: f.bondType,
|
||||||
|
partyName: f.partyName,
|
||||||
|
address1: f.address1 ?? '',
|
||||||
|
address2: f.address2 ?? '',
|
||||||
|
address3: f.address3 ?? '',
|
||||||
|
city: f.city ?? '',
|
||||||
|
pincode: f.pincode ?? '',
|
||||||
|
phone: f.phone ?? '',
|
||||||
|
fax: f.fax ?? '',
|
||||||
|
bankType: f.bankType ?? '',
|
||||||
|
glAccountId: Number(f.glAccountId),
|
||||||
|
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
|
||||||
|
tdsApplicable: tds,
|
||||||
|
tdsRate: tds ? Number(f.tdsRate) : null,
|
||||||
|
rbiCode: f.rbiCode ?? '',
|
||||||
|
limit: f.limit ?? null,
|
||||||
|
leiNo: f.leiNo ?? '',
|
||||||
|
cpBankName: f.cpBankName ?? '',
|
||||||
|
cpBranchName: f.cpBranchName ?? '',
|
||||||
|
cpAccountNo: f.cpAccountNo ?? '',
|
||||||
|
cpIfsc: f.cpIfsc ?? '',
|
||||||
|
// Shares only: who issued them (only co-operative / membership shares may be held), and what values them.
|
||||||
|
institutionType: share ? f.institutionType : '',
|
||||||
|
subscribedCapital: share && f.subscribedCapital ? Number(f.subscribedCapital) : null,
|
||||||
|
lastBalanceSheetDate: share ? (f.lastBalanceSheetDate ?? '') : '',
|
||||||
|
inLiquidation: share && !!f.inLiquidation,
|
||||||
|
active: f.status !== 'INACTIVE',
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
const request = f.id ? this.investmentService.updateParty(f.id, body) : this.investmentService.createParty(body);
|
||||||
|
this.saving = true;
|
||||||
|
request.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(f.id ? `Change to party ${f.partyCode}` : `New party ${f.partyName ?? ''}`)
|
||||||
|
: `Party ${res.resourceIdentifier ?? f.partyCode} ${f.id ? 'updated' : 'saved'}.`
|
||||||
|
);
|
||||||
|
this.partyForm = {};
|
||||||
|
this.reloadMasters();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchParty(): void {
|
||||||
|
const p = this.parties.find((x) => x.partyCode.toUpperCase() === this.partySearchCode.trim().toUpperCase());
|
||||||
|
if (!p) {
|
||||||
|
this.toast(`Party Code ${this.partySearchCode} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.partyForm = { ...p };
|
||||||
|
this.toast(`Party ${p.partyCode} loaded.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelParty(): void {
|
||||||
|
this.partyForm = {};
|
||||||
|
this.partySearchCode = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
dropParty(): void {
|
||||||
|
const f = this.partyForm;
|
||||||
|
if (!f.id) {
|
||||||
|
this.toast('Search a saved party first, then Drop.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.deleteParty(f.id).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res) ? sentForApproval(`Drop of party ${f.partyCode}`) : `Party ${f.partyCode} dropped.`
|
||||||
|
);
|
||||||
|
this.partyForm = {};
|
||||||
|
this.reloadMasters();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Script Master --------------------
|
||||||
|
/** Coupon / maturity terms only apply to debt securities. */
|
||||||
|
get scriptIsDebtSecurity(): boolean {
|
||||||
|
return [
|
||||||
|
'GOVT_SECURITY',
|
||||||
|
'NON_SLR',
|
||||||
|
'BOND'
|
||||||
|
].includes(this.scriptForm.instrumentType ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Corporate bonds, CPs and CDs carry a credit rating and a listed / unlisted flag (RBI non-SLR rules). */
|
||||||
|
get scriptIsRated(): boolean {
|
||||||
|
return [
|
||||||
|
'NON_SLR',
|
||||||
|
'BOND'
|
||||||
|
].includes(this.scriptForm.instrumentType ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
|
||||||
|
get scriptRatingEligible(): boolean {
|
||||||
|
const r = this.scriptForm.rating;
|
||||||
|
return !!r && ELIGIBLE_RATINGS.includes(r) && this.scriptIsShortTerm === /^A[1-4]\+?$/.test(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A CP / CD runs one year or less and takes a short-term rating (A1+ to A4); a bond takes a long-term one. */
|
||||||
|
get scriptIsShortTerm(): boolean {
|
||||||
|
const f = this.scriptForm;
|
||||||
|
if (!f.issueDate || !f.maturityDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const issue = parseDDMMYYYY(f.issueDate);
|
||||||
|
const limit = new Date(issue.getFullYear() + 1, issue.getMonth(), issue.getDate());
|
||||||
|
return parseDDMMYYYY(f.maturityDate) <= limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
|
||||||
|
get nextScriptCode(): string {
|
||||||
|
const prefixByType: Record<InstrumentType, string> = {
|
||||||
|
GOVT_SECURITY: 'G',
|
||||||
|
NON_SLR: 'N',
|
||||||
|
BOND: 'B',
|
||||||
|
MUTUAL_FUND: 'M',
|
||||||
|
FD: 'F',
|
||||||
|
SHARE: 'S'
|
||||||
|
};
|
||||||
|
const type = this.scriptForm.instrumentType;
|
||||||
|
if (!type) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const prefix = prefixByType[type];
|
||||||
|
const pattern = new RegExp(`^${prefix}(\\d{5})$`);
|
||||||
|
const nums = this.scripts
|
||||||
|
.map((s) => pattern.exec(s.scriptCode)?.[1])
|
||||||
|
.filter((n): n is string => !!n)
|
||||||
|
.map((n) => parseInt(n, 10));
|
||||||
|
return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(5, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scriptPeriod(): { years: number; months: number; days: number } {
|
||||||
|
return periodBetween(this.scriptForm.issueDate, this.scriptForm.maturityDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveScript(): void {
|
||||||
|
const f = this.scriptForm;
|
||||||
|
if (!f.instrumentType || !f.scriptName || !f.glAccountId) {
|
||||||
|
this.toast('Enter Bond Type, Script Desc and GL A/c No.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate || !f.maturityDate)) {
|
||||||
|
this.toast('Enter ISIN, Int Rate, Issue Date and Maturity Date for this security.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.scriptIsRated && !f.rating) {
|
||||||
|
this.toast('Enter the credit rating of this bond / CP / CD.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
bondType: f.instrumentType,
|
||||||
|
slrType: f.slrType ?? '',
|
||||||
|
scriptDesc: f.scriptName,
|
||||||
|
glAccountId: Number(f.glAccountId),
|
||||||
|
isin: f.isin ?? '',
|
||||||
|
intRate: f.couponRate ?? null,
|
||||||
|
// Units = face value / face value per unit (100 for G-Secs by default; bonds can be 1,000 or 10,00,000).
|
||||||
|
...(f.faceValuePerUnit ? { faceValuePerUnit: Number(f.faceValuePerUnit) } : {}),
|
||||||
|
issueDate: f.issueDate ?? '',
|
||||||
|
maturityDate: f.maturityDate ?? '',
|
||||||
|
companyName: f.companyName ?? '',
|
||||||
|
address1: f.address1 ?? '',
|
||||||
|
address2: f.address2 ?? '',
|
||||||
|
address3: f.address3 ?? '',
|
||||||
|
city: f.city ?? '',
|
||||||
|
pincode: f.pincode ?? '',
|
||||||
|
phone: f.phone ?? '',
|
||||||
|
fax: f.fax ?? '',
|
||||||
|
corpCode: f.corpCode ?? '',
|
||||||
|
rating: this.scriptIsRated ? (f.rating ?? '') : '',
|
||||||
|
ratingAgency: this.scriptIsRated ? (f.ratingAgency ?? '') : '',
|
||||||
|
listed: !this.scriptIsRated || f.listed !== false,
|
||||||
|
active: f.active !== false,
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
const request = f.id ? this.investmentService.updateScript(f.id, body) : this.investmentService.createScript(body);
|
||||||
|
this.saving = true;
|
||||||
|
request.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(f.id ? `Change to script ${f.scriptCode}` : `New script ${f.scriptName ?? ''}`)
|
||||||
|
: `Script ${res.resourceIdentifier ?? f.scriptCode} ${f.id ? 'updated' : 'saved'}.`
|
||||||
|
);
|
||||||
|
this.scriptForm = {};
|
||||||
|
this.reloadMasters();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchScript(): void {
|
||||||
|
const s = this.scripts.find((x) => x.scriptCode.toUpperCase() === this.scriptSearchCode.trim().toUpperCase());
|
||||||
|
if (!s) {
|
||||||
|
this.toast(`Script Code ${this.scriptSearchCode} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scriptForm = { ...s };
|
||||||
|
this.toast(`Script ${s.scriptCode} loaded.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelScript(): void {
|
||||||
|
this.scriptForm = {};
|
||||||
|
this.scriptSearchCode = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubType(): void {
|
addSubType(): void {
|
||||||
@@ -248,24 +553,163 @@ export class InvestmentMastersComponent {
|
|||||||
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))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Daily rate upload (CSV / Excel) --------------------
|
||||||
|
/** Result of the last upload, one line per row of the file. */
|
||||||
|
uploadResults: Array<{ row: number; text: string; ok: boolean }> = [];
|
||||||
|
uploading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the day's rates / NAVs from a CSV or Excel file (Jitendra Sir): columns Script Code (or ISIN), Rate Date
|
||||||
|
* (DD-MM-YYYY), Close, and optionally Open, High, Low; the first row is the header. Each row is saved on its own,
|
||||||
|
* so one bad row does not stop the rest.
|
||||||
|
*/
|
||||||
|
async uploadDailyRates(event: Event): Promise<void> {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
input.value = '';
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploadResults = [];
|
||||||
|
let rows: string[][];
|
||||||
|
try {
|
||||||
|
rows = file.name.toLowerCase().endsWith('.xlsx') ? await this.readXlsx(file) : this.readCsv(await file.text());
|
||||||
|
} catch {
|
||||||
|
this.toast('Could not read the file. Use CSV or Excel (.xlsx).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ''));
|
||||||
|
if (!data.length) {
|
||||||
|
this.toast('The file has no rate rows.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploading = true;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const [
|
||||||
|
code,
|
||||||
|
rateDate,
|
||||||
|
close,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low
|
||||||
|
] = data[i].map((c) => c.trim());
|
||||||
|
const script = this.scripts.find(
|
||||||
|
(s) =>
|
||||||
|
s.scriptCode.toUpperCase() === code.toUpperCase() || (!!s.isin && s.isin.toUpperCase() === code.toUpperCase())
|
||||||
|
);
|
||||||
|
const num = (v?: string) => (v ? Number(v.replace(/,/g, '')) : null);
|
||||||
|
if (!script?.id || !rateDate || num(close) == null || isNaN(num(close) as number)) {
|
||||||
|
this.uploadResults.push({
|
||||||
|
row: i + 2,
|
||||||
|
text: `${code || '?'}: script not found or date / close missing`,
|
||||||
|
ok: false
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
scriptId: script.id,
|
||||||
|
rateDate,
|
||||||
|
closeRate: num(close),
|
||||||
|
...(num(open) != null ? { openRate: num(open) } : {}),
|
||||||
|
...(num(high) != null ? { highRate: num(high) } : {}),
|
||||||
|
...(num(low) != null ? { lowRate: num(low) } : {}),
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await firstValueFrom(this.investmentService.createDailyRate(body, true));
|
||||||
|
this.uploadResults.push({ row: i + 2, text: `${script.scriptCode} ${rateDate}: saved`, ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
this.uploadResults.push({
|
||||||
|
row: i + 2,
|
||||||
|
text: `${script.scriptCode} ${rateDate}: ${apiErrorMessage(err)}`,
|
||||||
|
ok: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.uploading = false;
|
||||||
|
const saved = this.uploadResults.filter((r) => r.ok).length;
|
||||||
|
this.toast(`${saved} of ${data.length} rate(s) saved.`);
|
||||||
|
this.loadDailyRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readCsv(text: string): string[][] {
|
||||||
|
return text.split(/\r?\n/).map((line) => line.split(',').map((c) => c.replace(/^"|"$/g, '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readXlsx(file: File): Promise<string[][]> {
|
||||||
|
const ExcelJS = await import('exceljs');
|
||||||
|
const wb = new ExcelJS.Workbook();
|
||||||
|
await wb.xlsx.load(await file.arrayBuffer());
|
||||||
|
const sheet = wb.worksheets[0];
|
||||||
|
const rows: string[][] = [];
|
||||||
|
sheet.eachRow({ includeEmpty: false }, (row) => {
|
||||||
|
const cells: string[] = [];
|
||||||
|
for (let c = 1; c <= 6; c++) {
|
||||||
|
const v = row.getCell(c).value;
|
||||||
|
cells.push(
|
||||||
|
v instanceof Date
|
||||||
|
? `${String(v.getDate()).padStart(2, '0')}-${String(v.getMonth() + 1).padStart(2, '0')}-${v.getFullYear()}`
|
||||||
|
: v == null
|
||||||
|
? ''
|
||||||
|
: String(v)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.push(cells);
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 --------------------
|
||||||
@@ -279,46 +723,121 @@ export class InvestmentMastersComponent {
|
|||||||
return one(
|
return one(
|
||||||
[
|
[
|
||||||
'Party Code',
|
'Party Code',
|
||||||
|
'Bond Type',
|
||||||
'Party Name',
|
'Party Name',
|
||||||
'Type',
|
'Address',
|
||||||
'PAN No'
|
'City',
|
||||||
|
'Pincode',
|
||||||
|
'Phone',
|
||||||
|
'Fax',
|
||||||
|
'Bank Type',
|
||||||
|
'GL A/c No',
|
||||||
|
'RBI Code',
|
||||||
|
'Limit',
|
||||||
|
'LEI No',
|
||||||
|
'Counterparty Bank',
|
||||||
|
'Branch',
|
||||||
|
'Account No',
|
||||||
|
'IFSC',
|
||||||
|
'Status'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
12,
|
12,
|
||||||
|
14,
|
||||||
30,
|
30,
|
||||||
16,
|
36,
|
||||||
14
|
14,
|
||||||
|
10,
|
||||||
|
14,
|
||||||
|
14,
|
||||||
|
24,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
14,
|
||||||
|
22,
|
||||||
|
24,
|
||||||
|
20,
|
||||||
|
18,
|
||||||
|
14,
|
||||||
|
10
|
||||||
],
|
],
|
||||||
this.parties.map((p) => [
|
this.parties.map((p) => [
|
||||||
p.partyCode,
|
p.partyCode,
|
||||||
|
p.bondType ?? '',
|
||||||
p.partyName,
|
p.partyName,
|
||||||
p.partyType,
|
[
|
||||||
p.panNo
|
p.address1,
|
||||||
|
p.address2,
|
||||||
|
p.address3
|
||||||
|
]
|
||||||
|
.filter((x) => !!x)
|
||||||
|
.join(', '),
|
||||||
|
p.city ?? '',
|
||||||
|
p.pincode ?? '',
|
||||||
|
p.phone ?? '',
|
||||||
|
p.fax ?? '',
|
||||||
|
p.bankType ?? '',
|
||||||
|
p.glCode ?? '',
|
||||||
|
p.rbiCode ?? '',
|
||||||
|
p.limit ?? null,
|
||||||
|
p.leiNo ?? '',
|
||||||
|
p.cpBankName ?? '',
|
||||||
|
p.cpBranchName ?? '',
|
||||||
|
p.cpAccountNo ?? '',
|
||||||
|
p.cpIfsc ?? '',
|
||||||
|
p.status ?? 'ACTIVE'
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
case 'script':
|
case 'script':
|
||||||
return one(
|
return one(
|
||||||
[
|
[
|
||||||
'Script Code',
|
'Script Code',
|
||||||
'Script Name',
|
'Bond Type',
|
||||||
'Instrument',
|
'SLR',
|
||||||
|
'Script Desc',
|
||||||
|
'GL A/c No',
|
||||||
'ISIN',
|
'ISIN',
|
||||||
'Face Value/Unit'
|
'Int Rate %',
|
||||||
|
'Issue Date',
|
||||||
|
'Maturity Date',
|
||||||
|
'Period (Y/M/D)',
|
||||||
|
'Face Value/Unit',
|
||||||
|
'Company Name',
|
||||||
|
'Corp Code'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
14,
|
||||||
16,
|
16,
|
||||||
|
12,
|
||||||
30,
|
30,
|
||||||
|
12,
|
||||||
16,
|
16,
|
||||||
16,
|
10,
|
||||||
16
|
14,
|
||||||
|
14,
|
||||||
|
14,
|
||||||
|
14,
|
||||||
|
24,
|
||||||
|
12
|
||||||
],
|
],
|
||||||
this.scripts.map((s) => [
|
this.scripts.map((s) => {
|
||||||
s.scriptCode,
|
const p = periodBetween(s.issueDate, s.maturityDate);
|
||||||
s.scriptName,
|
return [
|
||||||
s.instrumentType,
|
s.scriptCode,
|
||||||
s.isin,
|
s.instrumentType,
|
||||||
s.faceValuePerUnit
|
s.slrType ?? '',
|
||||||
])
|
s.scriptName,
|
||||||
|
s.glCode ?? '',
|
||||||
|
s.isin,
|
||||||
|
s.couponRate ?? null,
|
||||||
|
s.issueDate ?? '',
|
||||||
|
s.maturityDate ?? '',
|
||||||
|
s.issueDate && s.maturityDate ? `${p.years}/${p.months}/${p.days}` : '',
|
||||||
|
s.faceValuePerUnit,
|
||||||
|
s.companyName ?? '',
|
||||||
|
s.corpCode ?? ''
|
||||||
|
];
|
||||||
|
})
|
||||||
);
|
);
|
||||||
case 'subtype':
|
case 'subtype':
|
||||||
return one(
|
return one(
|
||||||
|
|||||||
@@ -12,17 +12,26 @@
|
|||||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||||
<h1 class="pg-title">Investment Reports</h1>
|
<h1 class="pg-title">Investment Reports</h1>
|
||||||
<p class="pg-sub">
|
<p class="pg-sub">
|
||||||
Balance reports, ledgers, registers, maturity lists and the consolidated portfolio — all derived live from the
|
Balance reports, ledgers, registers, maturity lists and the consolidated portfolio from the saved deals, FDs and
|
||||||
same holdings/transactions used by the Entry screens.
|
their GL entries, as on {{ asOnDate }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-toolbar">
|
<div class="pg-toolbar">
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating || loading" (click)="exportExcel()">
|
||||||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@if (error) {
|
||||||
|
<div class="pg-card">
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="cctr-tabs">
|
<div class="cctr-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -113,7 +122,8 @@
|
|||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th class="num">Units</th>
|
<th class="num">Units</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th class="num">Market/Purchase Rate</th>
|
<th class="num">Book Value</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -124,7 +134,7 @@
|
|||||||
: activeReport === 'shareBalance'
|
: activeReport === 'shareBalance'
|
||||||
? shareHoldings
|
? shareHoldings
|
||||||
: fdHoldings;
|
: fdHoldings;
|
||||||
track h.certNo
|
track h.instrumentType + ':' + h.id
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -133,11 +143,24 @@
|
|||||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||||
<td class="num">{{ h.units | number }}</td>
|
<td class="num">{{ h.units | number }}</td>
|
||||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||||
<td class="num">{{ marketPrice(h.scriptCode) || h.purchaseRate }}</td>
|
<td class="num">₹{{ h.bookValue ?? h.faceValue | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">
|
||||||
|
@if (marketPrice(h.scriptCode); as mp) {
|
||||||
|
{{ mp }} <small>(market {{ marketRateDate(h.scriptCode) }})</small>
|
||||||
|
} @else {
|
||||||
|
{{ h.purchaseRate }} <small>(purchase)</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="pg-empty">
|
||||||
|
{{ activeReport === 'shareBalance' ? 'No share holdings.' : 'No holdings.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -145,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'bankInvList') {
|
@if (activeReport === 'bankInvList') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Bank Investment List</div>
|
<div class="pos-section-label">Bank Investment List — FDs held with each bank</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -241,8 +264,7 @@
|
|||||||
activeReport === 'fdLedger' ||
|
activeReport === 'fdLedger' ||
|
||||||
activeReport === 'bondLedger' ||
|
activeReport === 'bondLedger' ||
|
||||||
activeReport === 'shareLedger' ||
|
activeReport === 'shareLedger' ||
|
||||||
activeReport === 'amortizeLedger' ||
|
activeReport === 'amortizeLedger'
|
||||||
activeReport === 'dealSlip'
|
|
||||||
) {
|
) {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">
|
<div class="pos-section-label">
|
||||||
@@ -253,9 +275,7 @@
|
|||||||
? 'Govt/Bond Ledger'
|
? 'Govt/Bond Ledger'
|
||||||
: activeReport === 'shareLedger'
|
: activeReport === 'shareLedger'
|
||||||
? 'Share Ledger'
|
? 'Share Ledger'
|
||||||
: activeReport === 'amortizeLedger'
|
: 'Amortize Ledger'
|
||||||
? 'Amortize Ledger'
|
|
||||||
: 'Deal Slip Register'
|
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
@@ -277,10 +297,8 @@
|
|||||||
? bondTxns
|
? bondTxns
|
||||||
: activeReport === 'shareLedger'
|
: activeReport === 'shareLedger'
|
||||||
? shareTxns
|
? shareTxns
|
||||||
: activeReport === 'amortizeLedger'
|
: amortizeTxns;
|
||||||
? amortizeTxns
|
track $index
|
||||||
: dealSlips;
|
|
||||||
track t.voucherNo
|
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ t.txnDate }}</td>
|
||||||
@@ -301,9 +319,128 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (activeReport === 'dealSlip') {
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">Deal Slip Register — every deal serially numbered, cancelled ones included</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From (deal date)</span><mifosx-date-ddmmyyyy [(ngModel)]="slipFrom"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To (deal date)</span><mifosx-date-ddmmyyyy [(ngModel)]="slipTo"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
</div>
|
||||||
|
<div class="inv-form-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="slipLoading" (click)="loadDealSlips()">
|
||||||
|
{{ slipLoading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (slipError) {
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ slipError }}</p>
|
||||||
|
}
|
||||||
|
@if (slipGaps.length) {
|
||||||
|
<p class="inv-hint inv-hint--bad">
|
||||||
|
Missing slip numbers in this period: {{ slipGaps.join(', ') }}. Every number must be accounted for.
|
||||||
|
</p>
|
||||||
|
} @else if (slips.length) {
|
||||||
|
<p class="inv-hint">No gaps: every slip number in this period is accounted for.</p>
|
||||||
|
}
|
||||||
|
@if (slipsAwaited) {
|
||||||
|
<p class="inv-hint inv-hint--bad">
|
||||||
|
Counterparty confirmation still awaited on {{ slipsAwaited }} deal{{ slipsAwaited === 1 ? '' : 's' }}
|
||||||
|
(marked below).
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Slip No</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Deal Date / Time</th>
|
||||||
|
<th>Counterparty</th>
|
||||||
|
<th>Security</th>
|
||||||
|
<th class="num">Units</th>
|
||||||
|
<th class="num">Price</th>
|
||||||
|
<th class="num">Net Amount</th>
|
||||||
|
<th>Broker</th>
|
||||||
|
<th>Confirmation</th>
|
||||||
|
<th>Entered / Approved</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (s of slips; track s.slipNo) {
|
||||||
|
<tr [style.opacity]="s.status === 'CANCELLED' ? 0.6 : 1">
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ s.slipNo }}</span>
|
||||||
|
@if (s.side !== 'BUY') {
|
||||||
|
<br /><small>Deal {{ s.dealNo }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>{{ s.side }}</td>
|
||||||
|
<td>
|
||||||
|
{{ s.dealDate }}<br /><small
|
||||||
|
>{{ s.dealTime || '—' }} · {{ s.settlementType }} {{ s.settlementDate }}</small
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.counterparty || '—' }}</td>
|
||||||
|
<td>
|
||||||
|
{{ s.scriptCode }} {{ s.scriptDesc }}
|
||||||
|
@if (s.isin) {
|
||||||
|
<br /><small>{{ s.isin }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ s.units | number: '1.0-6' }}</td>
|
||||||
|
<td class="num">{{ s.price | number: '1.2-4' }}</td>
|
||||||
|
<td class="num">₹{{ s.netAmount | number: '1.2-2' }}</td>
|
||||||
|
<td>
|
||||||
|
@if (s.dealMode === 'BROKER') {
|
||||||
|
{{ s.brokerCode }}<br /><small
|
||||||
|
>{{ brokerName(s.brokerCode) }} · ₹{{ s.brokerage | number: '1.2-2' }}</small
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
Direct
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ confirmationLabel(s.confirmationMode) || '—' }}<br />
|
||||||
|
@if (s.confirmationReceivedOn) {
|
||||||
|
<small
|
||||||
|
>Received {{ s.confirmationReceivedOn
|
||||||
|
}}{{ s.confirmationRef ? ' · ' + s.confirmationRef : '' }}</small
|
||||||
|
>
|
||||||
|
} @else if (confirmationAwaited(s)) {
|
||||||
|
<small class="inv-hint--bad">Awaited</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ s.enteredBy || '—' }}<br /><small>{{ s.approvedBy ? 'Approved: ' + s.approvedBy : '' }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="'inv-badge inv-badge--' + s.status.toLowerCase()">{{
|
||||||
|
s.status === 'PENDING_SETTLEMENT' ? 'PENDING' : s.status
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printSlip(s)">PDF</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="13" class="pg-empty">{{ slipLoading ? 'Loading…' : 'No deals in this period.' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if (activeReport === 'intDueRegister') {
|
@if (activeReport === 'intDueRegister') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Interest Due Register (Non-Cumulative Holdings)</div>
|
<div class="pos-section-label">
|
||||||
|
Interest Due Register — interest earned up to {{ asOnDate }} and not received yet ·
|
||||||
|
<a routerLink="/investment/diary">Due Date Diary</a>
|
||||||
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -311,11 +448,11 @@
|
|||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th>Frequency</th>
|
<th>Frequency</th>
|
||||||
<th class="num">Rate</th>
|
<th class="num">Rate</th>
|
||||||
<th class="num col-in">Due Amount (as on {{ asOnDate }})</th>
|
<th class="num col-in">Receivable (as on {{ asOnDate }})</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of interestDueRegister; track h.certNo) {
|
@for (h of interestDueRegister; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -325,6 +462,10 @@
|
|||||||
<td class="num">{{ h.interestRate }}%</td>
|
<td class="num">{{ h.interestRate }}%</td>
|
||||||
<td class="num col-in">₹{{ h.dueAmount | number: '1.2-2' }}</td>
|
<td class="num col-in">₹{{ h.dueAmount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="pg-empty">No interest receivable.</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -344,7 +485,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of slrBondRegister; track h.certNo) {
|
@for (h of slrBondRegister; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -361,40 +502,56 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'pendingReceived') {
|
@if (activeReport === 'pendingReceived') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Received Interest / Dividend</div>
|
<div class="pos-section-label">
|
||||||
|
Interest Received — {{ receivedFrom }} to {{ asOnDate }} (coupons, FD payouts and FD interest paid at closure)
|
||||||
|
</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Investment</th>
|
||||||
<th>Cert No</th>
|
<th>Deal No</th>
|
||||||
<th>Particulars</th>
|
<th>Particulars</th>
|
||||||
<th class="num col-in">Amount</th>
|
<th class="num col-in">Received</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (t of receivedInterestTxns; track t.voucherNo) {
|
@for (x of receivedRows; track x.instrument + ':' + x.refId) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
<td>{{ t.certNo }}</td>
|
<td>
|
||||||
<td>{{ t.particulars }}</td>
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
</td>
|
||||||
|
<td>{{ x.particulars }}</td>
|
||||||
|
<td class="num col-in">₹{{ x.received | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="pg-empty">No interest received in this period.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@if (receivedRows.length) {
|
||||||
|
<tfoot>
|
||||||
|
<tr class="cctr-total-row">
|
||||||
|
<td colspan="3">Total</td>
|
||||||
|
<td class="num">₹{{ receivedTotal | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Pending Interest (No Interest Posted Yet)</div>
|
<div class="pos-section-label">Pending Interest — earned up to {{ asOnDate }}, not received yet</div>
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Script / Party</th>
|
<th>Script / Party</th>
|
||||||
<th class="num col-out">Accrued (Pending)</th>
|
<th class="num col-out">Receivable</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of pendingInterestHoldings; track h.certNo) {
|
@for (h of pendingInterestHoldings; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -487,11 +644,13 @@
|
|||||||
<th>Cert No</th>
|
<th>Cert No</th>
|
||||||
<th>Party</th>
|
<th>Party</th>
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
<th>Matured On</th>
|
<th>Maturity Date</th>
|
||||||
|
<th>Closed As</th>
|
||||||
|
<th>Closed On</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of maturedFdList; track h.certNo) {
|
@for (h of maturedFdList; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -499,10 +658,12 @@
|
|||||||
<td>{{ h.party }}</td>
|
<td>{{ h.party }}</td>
|
||||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||||
<td>{{ h.maturityDate }}</td>
|
<td>{{ h.maturityDate }}</td>
|
||||||
|
<td>{{ h.closureType || '—' }}</td>
|
||||||
|
<td>{{ h.closedOnDate || '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="pg-empty">No matured FDs.</td>
|
<td colspan="6" class="pg-empty">No matured FDs.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -524,7 +685,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of maturingList; track h.certNo) {
|
@for (h of maturingList; track h.instrumentType + ':' + h.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ h.certNo }}</span>
|
||||||
@@ -543,24 +704,12 @@
|
|||||||
@if (activeReport === 'maturityPattern') {
|
@if (activeReport === 'maturityPattern') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Maturity Pattern Register</div>
|
<div class="pos-section-label">Maturity Pattern Register</div>
|
||||||
<table class="pg-table">
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
<thead>
|
The maturity pattern (up to 1 year, 1–3, 3–5 and over 5 years) is the Bucket Wise Investment report.
|
||||||
<tr>
|
</p>
|
||||||
<th>Bucket</th>
|
<div class="inv-form-actions" style="padding: 8px 16px">
|
||||||
<th class="num">Holdings</th>
|
<a class="pg-btn pg-btn--primary" routerLink="/investment/bucket-report">Open Bucket Wise Investment</a>
|
||||||
<th class="num">Face Value</th>
|
</div>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (b of maturityBuckets; track b.label) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ b.label }}</td>
|
|
||||||
<td class="num">{{ b.count }}</td>
|
|
||||||
<td class="num">₹{{ b.faceValue | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (
|
@if (
|
||||||
@@ -601,7 +750,7 @@
|
|||||||
: activeReport === 'shareSecList'
|
: activeReport === 'shareSecList'
|
||||||
? shareSecList
|
? shareSecList
|
||||||
: mutualFundList;
|
: mutualFundList;
|
||||||
track h.certNo
|
track h.instrumentType + ':' + h.id
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -615,6 +764,12 @@
|
|||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="pg-empty">
|
||||||
|
{{ activeReport === 'shareSecList' ? 'No share holdings.' : 'No holdings.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -622,35 +777,35 @@
|
|||||||
}
|
}
|
||||||
@if (activeReport === 'shiftingReport') {
|
@if (activeReport === 'shiftingReport') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">
|
<div class="pos-section-label">Shifting Report — securities transferred to another script / category</div>
|
||||||
Shifting Report — SLR Securities with Market Deviation ≥ 1% (AFS/HTM review candidates)
|
|
||||||
</div>
|
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Date</th>
|
||||||
<th>Script</th>
|
<th>Deal No</th>
|
||||||
<th class="num">Purchase Rate</th>
|
<th>From</th>
|
||||||
<th class="num">Market Price</th>
|
<th>To</th>
|
||||||
<th class="num">Deviation %</th>
|
<th class="num">Units</th>
|
||||||
|
<th class="num">Book Value</th>
|
||||||
|
<th>Voucher</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of shiftingCandidates; track h.certNo) {
|
@for (x of transfers; track x.id) {
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{{ x.transferDate }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
</td>
|
|
||||||
<td>{{ h.scriptName }}</td>
|
|
||||||
<td class="num">{{ h.purchaseRate }}</td>
|
|
||||||
<td class="num">{{ h.marketPrice }}</td>
|
|
||||||
<td class="num" [class.col-in]="h.deviationPct > 0" [class.col-out]="h.deviationPct < 0">
|
|
||||||
{{ h.deviationPct }}%
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ x.fromScriptCode }} {{ x.fromScriptDesc }}</td>
|
||||||
|
<td>{{ x.toScriptCode }} {{ x.toScriptDesc }}</td>
|
||||||
|
<td class="num">{{ x.units | number: '1.0-4' }}</td>
|
||||||
|
<td class="num">₹{{ x.bookValue | number: '1.2-2' }}</td>
|
||||||
|
<td>{{ x.voucherNo || '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="pg-empty">No securities crossing the 1% deviation threshold today.</td>
|
<td colspan="7" class="pg-empty">No securities shifted.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -687,7 +842,7 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="cctr-total-row">
|
<tr class="cctr-total-row">
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td class="num"></td>
|
<td class="num">{{ portfolioTotalCount }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalFaceValue | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalFaceValue | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalAccrued | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalAccrued | number: '1.2-2' }}</td>
|
||||||
<td class="num">₹{{ portfolioTotalMaturity | number: '1.2-2' }}</td>
|
<td class="num">₹{{ portfolioTotalMaturity | number: '1.2-2' }}</td>
|
||||||
|
|||||||
@@ -6,35 +6,51 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { forkJoin, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
ALL_HOLDINGS,
|
|
||||||
ALL_TXNS,
|
|
||||||
FD_HOLDINGS,
|
|
||||||
GOVT_SECURITY_HOLDINGS,
|
|
||||||
NON_SLR_HOLDINGS,
|
|
||||||
BOND_HOLDINGS,
|
|
||||||
MUTUAL_FUND_HOLDINGS,
|
|
||||||
SHARE_HOLDINGS,
|
|
||||||
SCRIPT_MARKET_DETAIL,
|
|
||||||
InstrumentType,
|
InstrumentType,
|
||||||
InvestmentHolding,
|
InvestmentHolding,
|
||||||
InvestmentTxn,
|
InvestmentTxn,
|
||||||
INSTRUMENT_LABELS,
|
INSTRUMENT_LABELS,
|
||||||
accruedInterest,
|
|
||||||
maturityAmount,
|
|
||||||
daysToMaturity,
|
daysToMaturity,
|
||||||
|
parseDDMMYYYY,
|
||||||
todayDDMMYYYY
|
todayDDMMYYYY
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||||
|
import { brokerNameOf, saveDealSlipPdf } from '../deal-slip-pdf';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import {
|
||||||
|
InvestmentDailyRateDto,
|
||||||
|
InvestmentDealSlipDto,
|
||||||
|
InvestmentFdDto,
|
||||||
|
InvestmentFdScheduleDto,
|
||||||
|
InvestmentInterestReportRowDto,
|
||||||
|
InvestmentSecurityTransferDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage,
|
||||||
|
bookValueSoldByPurchase,
|
||||||
|
confirmationModeLabel,
|
||||||
|
toFdHolding,
|
||||||
|
toFdTxns,
|
||||||
|
toHolding,
|
||||||
|
toInterestTxn,
|
||||||
|
toPurchaseTxn,
|
||||||
|
toSaleTxn,
|
||||||
|
toTransferTxn
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ~20 legacy Investment reports, grouped into 4 tabs. Every figure here is derived from the
|
* ~20 legacy Investment reports in 4 tabs, on live data: the saved purchases and FDs as holdings, and their GL
|
||||||
* same ALL_HOLDINGS/ALL_TXNS arrays used by the Entry screens and Dashboard — so a report can
|
* entries (settlements, interest, sales, redemptions, amortisation, transfers, FD placements and closures) as the
|
||||||
* never drift out of sync with what a Purchase/Sell/Interest entry actually recorded.
|
* ledgers. Interest receivable comes from the Interest Received & Receivable report, market rates from the Daily
|
||||||
|
* Script Master.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-investment-reports',
|
selector: 'mifosx-investment-reports',
|
||||||
@@ -44,37 +60,265 @@ type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
|||||||
'../investment-shared.scss'
|
'../investment-shared.scss'
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule]
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterLink,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentReportsComponent {
|
export class InvestmentReportsComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
activeGroup: ReportGroup = 'balance';
|
activeGroup: ReportGroup = 'balance';
|
||||||
activeReport = 'govtSecBalance';
|
activeReport = 'govtSecBalance';
|
||||||
asOnDate = todayDDMMYYYY();
|
asOnDate = todayDDMMYYYY();
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
generating = false;
|
generating = false;
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
/** Saved purchases and FDs as holdings, and their GL entries, newest first. */
|
||||||
|
holdings: InvestmentHolding[] = [];
|
||||||
|
txns: InvestmentTxn[] = [];
|
||||||
|
/** Interest received in the financial year to the as-on date, per deal / FD (Interest Received & Receivable report). */
|
||||||
|
receivedRows: InvestmentInterestReportRowDto[] = [];
|
||||||
|
receivedFrom = '';
|
||||||
|
/**
|
||||||
|
* Deal Slip Register: every purchase, sale, refund, redemption and switch dated in the period, cancelled ones
|
||||||
|
* included, in slip number order (loaded when the report is opened).
|
||||||
|
*/
|
||||||
|
slips: InvestmentDealSlipDto[] = [];
|
||||||
|
slipFrom = '';
|
||||||
|
slipTo = todayDDMMYYYY();
|
||||||
|
slipLoading = false;
|
||||||
|
slipError = '';
|
||||||
|
/** Numbers missing between the first and last slip of each series in the period. */
|
||||||
|
slipGaps: string[] = [];
|
||||||
|
/** GOI category transfers (security shifted to another script / category). */
|
||||||
|
transfers: InvestmentSecurityTransferDto[] = [];
|
||||||
|
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
||||||
|
fdInterestLoaded = false;
|
||||||
|
private fds: InvestmentFdDto[] = [];
|
||||||
|
private receivable = new Map<string, number>();
|
||||||
|
private fdMaturity = new Map<number, number>();
|
||||||
|
private latestRate = new Map<string, number>();
|
||||||
|
private latestRateDate = new Map<string, string>();
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
const d = parseDDMMYYYY(this.asOnDate);
|
||||||
|
const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.fdInterestLoaded = false;
|
||||||
|
forkJoin({
|
||||||
|
purchases: this.investmentService.getPurchases(),
|
||||||
|
fds: this.investmentService.getFds(),
|
||||||
|
sales: this.investmentService.getSales(),
|
||||||
|
postings: this.investmentService.getInterestPostings(),
|
||||||
|
transfers: this.investmentService.getTransfers(),
|
||||||
|
interest: this.investmentService.getInterestReport(fyStart, this.asOnDate),
|
||||||
|
rates: this.investmentService.getDailyRates().pipe(catchError(() => of([] as InvestmentDailyRateDto[])))
|
||||||
|
}).subscribe({
|
||||||
|
next: (r) => {
|
||||||
|
this.fds = r.fds;
|
||||||
|
this.transfers = r.transfers;
|
||||||
|
// A holding is shown at the book value still held: cost less what settled sales and write-offs took off.
|
||||||
|
const sold = bookValueSoldByPurchase(r.sales);
|
||||||
|
this.holdings = [
|
||||||
|
...r.purchases.map((p) => ({ ...toHolding(p), bookValue: p.bookValue - (sold.get(p.id) ?? 0) })),
|
||||||
|
...r.fds.map(toFdHolding)
|
||||||
|
];
|
||||||
|
// Ledgers show what reached the GL: settled purchases and sales only.
|
||||||
|
this.txns = this.byDateDesc([
|
||||||
|
...r.purchases.filter((p) => !!p.glTransactionId).map(toPurchaseTxn),
|
||||||
|
...r.postings.map(toInterestTxn),
|
||||||
|
...r.sales.filter((x) => x.status === 'SETTLED').map(toSaleTxn),
|
||||||
|
...r.transfers.map(toTransferTxn),
|
||||||
|
...r.fds.flatMap((f) => toFdTxns(f, []))
|
||||||
|
]);
|
||||||
|
// Received = coupons, FD payouts and FD interest paid at closure, as the interest report counts them.
|
||||||
|
this.receivedRows = r.interest.rows.filter((x) => x.received > 0);
|
||||||
|
this.receivedFrom = r.interest.fromDate;
|
||||||
|
this.receivable = new Map(
|
||||||
|
r.interest.rows.map((x) => [
|
||||||
|
x.instrument + ':' + x.refId,
|
||||||
|
x.receivable
|
||||||
|
])
|
||||||
|
);
|
||||||
|
this.fdMaturity = new Map(
|
||||||
|
r.fds
|
||||||
|
.filter((f) => f.id != null)
|
||||||
|
.map((f) => [
|
||||||
|
f.id as number,
|
||||||
|
f.expectedMaturityAmount ?? f.principal ?? 0
|
||||||
|
])
|
||||||
|
);
|
||||||
|
// Latest close on or before the as-on date, per script.
|
||||||
|
const asOn = parseDDMMYYYY(this.asOnDate).getTime();
|
||||||
|
this.latestRate = new Map();
|
||||||
|
[...r.rates]
|
||||||
|
.filter((x) => parseDDMMYYYY(x.rateDate).getTime() <= asOn)
|
||||||
|
.sort((a, b) => parseDDMMYYYY(a.rateDate).getTime() - parseDDMMYYYY(b.rateDate).getTime())
|
||||||
|
.forEach((x) => {
|
||||||
|
this.latestRate.set(x.scriptCode, x.closeRate);
|
||||||
|
this.latestRateDate.set(x.scriptCode, x.rateDate);
|
||||||
|
});
|
||||||
|
this.loading = false;
|
||||||
|
if (this.activeReport === 'fdLedger') {
|
||||||
|
this.loadFdInterest();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds each FD's posted interest lines to the ledgers (one schedule call per FD). */
|
||||||
|
private loadFdInterest(): void {
|
||||||
|
if (this.fdInterestLoaded || !this.fds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fdInterestLoaded = true;
|
||||||
|
const withId = this.fds.filter((f) => f.id != null);
|
||||||
|
forkJoin(
|
||||||
|
withId.map((f) =>
|
||||||
|
this.investmentService
|
||||||
|
.getFdSchedule(f.id as number)
|
||||||
|
.pipe(catchError(() => of(null as InvestmentFdScheduleDto | null)))
|
||||||
|
)
|
||||||
|
).subscribe((schedules) => {
|
||||||
|
const lines: InvestmentTxn[] = [];
|
||||||
|
schedules.forEach((s, i) => {
|
||||||
|
if (s) {
|
||||||
|
lines.push(...toFdTxns(withId[i], s.rows).filter((t) => t.txnType === 'INTEREST'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.txns = this.byDateDesc([
|
||||||
|
...this.txns,
|
||||||
|
...lines
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byDateDesc(txns: InvestmentTxn[]): InvestmentTxn[] {
|
||||||
|
return txns
|
||||||
|
.filter((t) => !!t.txnDate)
|
||||||
|
.sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
setGroup(group: ReportGroup, defaultReport: string): void {
|
setGroup(group: ReportGroup, defaultReport: string): void {
|
||||||
this.activeGroup = group;
|
this.activeGroup = group;
|
||||||
this.activeReport = defaultReport;
|
this.setReport(defaultReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReport(report: string): void {
|
setReport(report: string): void {
|
||||||
this.activeReport = report;
|
this.activeReport = report;
|
||||||
|
if (report === 'fdLedger') {
|
||||||
|
this.loadFdInterest();
|
||||||
|
}
|
||||||
|
if (report === 'dealSlip' && !this.slips.length && !this.slipLoading) {
|
||||||
|
this.loadDealSlips();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deal Slip Register for the period (default: the financial year to date). */
|
||||||
|
loadDealSlips(): void {
|
||||||
|
if (!this.slipFrom) {
|
||||||
|
const d = parseDDMMYYYY(this.slipTo);
|
||||||
|
this.slipFrom = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||||
|
}
|
||||||
|
this.slipLoading = true;
|
||||||
|
this.slipError = '';
|
||||||
|
this.investmentService.getDealSlips(this.slipFrom, this.slipTo).subscribe({
|
||||||
|
next: (rows) => {
|
||||||
|
this.slips = rows;
|
||||||
|
this.slipGaps = this.findSlipGaps(rows);
|
||||||
|
this.slipLoading = false;
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
this.slips = [];
|
||||||
|
this.slipGaps = [];
|
||||||
|
this.slipLoading = false;
|
||||||
|
this.slipError = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Missing numbers in each series (purchases 202627000001…, sales 202627S00001…) between the first and the last
|
||||||
|
* slip shown: every slip must be accounted for, cancelled ones included.
|
||||||
|
*/
|
||||||
|
private findSlipGaps(rows: InvestmentDealSlipDto[]): string[] {
|
||||||
|
const series = new Map<string, { width: number; nos: number[] }>();
|
||||||
|
for (const r of rows) {
|
||||||
|
const m = /^(\d{6}S?)(\d+)$/.exec(r.slipNo);
|
||||||
|
if (!m) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const s = series.get(m[1]) ?? { width: m[2].length, nos: [] };
|
||||||
|
s.nos.push(Number(m[2]));
|
||||||
|
series.set(m[1], s);
|
||||||
|
}
|
||||||
|
const gaps: string[] = [];
|
||||||
|
series.forEach((s, prefix) => {
|
||||||
|
const nos = new Set(s.nos);
|
||||||
|
for (let n = Math.min(...s.nos) + 1; n < Math.max(...s.nos); n++) {
|
||||||
|
if (!nos.has(n)) {
|
||||||
|
gaps.push(prefix + String(n).padStart(s.width, '0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return gaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A deal settled without the counterparty's confirmation recorded (cancelled deals need none). */
|
||||||
|
confirmationAwaited(s: InvestmentDealSlipDto): boolean {
|
||||||
|
return s.status !== 'CANCELLED' && !s.confirmationReceivedOn && s.side !== 'REDEEM';
|
||||||
|
}
|
||||||
|
|
||||||
|
get slipsAwaited(): number {
|
||||||
|
return this.slips.filter((s) => this.confirmationAwaited(s)).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
printSlip(s: InvestmentDealSlipDto): void {
|
||||||
|
saveDealSlipPdf(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmationLabel(mode?: string): string {
|
||||||
|
return confirmationModeLabel(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
brokerName(code?: string): string {
|
||||||
|
return brokerNameOf(code) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
instrumentLabel(type: InstrumentType): string {
|
instrumentLabel(type: InstrumentType): string {
|
||||||
return INSTRUMENT_LABELS[type];
|
return INSTRUMENT_LABELS[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interest earned up to the as-on date and not yet received (Interest Received & Receivable report). */
|
||||||
accrued(h: InvestmentHolding): number {
|
accrued(h: InvestmentHolding): number {
|
||||||
return accruedInterest(h, this.asOnDate);
|
return this.receivable.get((h.instrumentType === 'FD' ? 'FD' : 'SECURITY') + ':' + h.id) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What comes back at maturity: the FD maturity amount, a security's face value, a fund's cost. */
|
||||||
maturity(h: InvestmentHolding): number {
|
maturity(h: InvestmentHolding): number {
|
||||||
return maturityAmount(h);
|
if (h.instrumentType === 'FD') {
|
||||||
|
return this.fdMaturity.get(h.id ?? -1) ?? h.faceValue;
|
||||||
|
}
|
||||||
|
return h.instrumentType === 'MUTUAL_FUND' || h.instrumentType === 'SHARE'
|
||||||
|
? (h.bookValue ?? h.faceValue)
|
||||||
|
: h.faceValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
daysLeft(h: InvestmentHolding): number {
|
daysLeft(h: InvestmentHolding): number {
|
||||||
@@ -85,23 +329,38 @@ export class InvestmentReportsComponent {
|
|||||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Latest close in the Daily Script Master on or before the as-on date. */
|
||||||
marketPrice(scriptCode: string): number | null {
|
marketPrice(scriptCode: string): number | null {
|
||||||
return SCRIPT_MARKET_DETAIL.find((m) => m.scriptCode === scriptCode)?.marketPrice ?? null;
|
return this.latestRate.get(scriptCode) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Date of that latest close. */
|
||||||
|
marketRateDate(scriptCode: string): string {
|
||||||
|
return this.latestRateDate.get(scriptCode) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private active(type?: InstrumentType): InvestmentHolding[] {
|
||||||
|
return this.holdings.filter((h) => h.status === 'ACTIVE' && (!type || h.instrumentType === type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ofType(type: InstrumentType): InvestmentHolding[] {
|
||||||
|
return this.holdings.filter((h) => h.instrumentType === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Balance Reports --------------------
|
// -------------------- Balance Reports --------------------
|
||||||
get govtSecHoldings(): InvestmentHolding[] {
|
get govtSecHoldings(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS;
|
return this.active('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
get shareHoldings(): InvestmentHolding[] {
|
get shareHoldings(): InvestmentHolding[] {
|
||||||
return SHARE_HOLDINGS;
|
return this.active('SHARE');
|
||||||
}
|
}
|
||||||
get fdHoldings(): InvestmentHolding[] {
|
get fdHoldings(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS;
|
return this.active('FD');
|
||||||
}
|
}
|
||||||
|
/** FDs held with each bank (principal) — securities bought from a dealer are not 'with' that bank. */
|
||||||
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
|
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
|
||||||
const map = new Map<string, { party: string; count: number; totalFaceValue: number }>();
|
const map = new Map<string, { party: string; count: number; totalFaceValue: number }>();
|
||||||
for (const h of ALL_HOLDINGS) {
|
for (const h of this.active('FD')) {
|
||||||
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
|
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
|
||||||
row.count += 1;
|
row.count += 1;
|
||||||
row.totalFaceValue += h.faceValue;
|
row.totalFaceValue += h.faceValue;
|
||||||
@@ -112,112 +371,65 @@ export class InvestmentReportsComponent {
|
|||||||
|
|
||||||
// -------------------- Ledgers & Registers --------------------
|
// -------------------- Ledgers & Registers --------------------
|
||||||
get fdTxns(): InvestmentTxn[] {
|
get fdTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.instrumentType === 'FD');
|
return this.txns.filter((t) => t.instrumentType === 'FD');
|
||||||
}
|
}
|
||||||
get bondTxns(): InvestmentTxn[] {
|
get bondTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => ([
|
return this.txns.filter((t) => ([
|
||||||
'GOVT_SECURITY',
|
'GOVT_SECURITY',
|
||||||
'NON_SLR',
|
'NON_SLR',
|
||||||
'BOND'
|
'BOND',
|
||||||
|
'MUTUAL_FUND'
|
||||||
] as InstrumentType[]).includes(t.instrumentType));
|
] as InstrumentType[]).includes(t.instrumentType));
|
||||||
}
|
}
|
||||||
get shareTxns(): InvestmentTxn[] {
|
get shareTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
return this.txns.filter((t) => t.instrumentType === 'SHARE');
|
||||||
}
|
}
|
||||||
get amortizeTxns(): InvestmentTxn[] {
|
get amortizeTxns(): InvestmentTxn[] {
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'AMORTIZE');
|
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
||||||
}
|
|
||||||
get dealSlips(): InvestmentTxn[] {
|
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'PURCHASE');
|
|
||||||
}
|
}
|
||||||
get slrBondRegister(): InvestmentHolding[] {
|
get slrBondRegister(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS.filter((h) => h.subType === 'SLR');
|
return this.active('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
|
/** Holdings with interest earned up to the as-on date and not received yet, with the amount. */
|
||||||
get interestDueRegister(): Array<InvestmentHolding & { dueAmount: number }> {
|
get interestDueRegister(): Array<InvestmentHolding & { dueAmount: number }> {
|
||||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE').map((h) => ({
|
return this.active()
|
||||||
...h,
|
.map((h) => ({ ...h, dueAmount: this.accrued(h) }))
|
||||||
dueAmount: this.accrued(h)
|
.filter((h) => h.dueAmount > 0);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
get receivedInterestTxns(): InvestmentTxn[] {
|
get receivedTotal(): number {
|
||||||
return ALL_TXNS.filter((t) => t.txnType === 'INTEREST' || t.txnType === 'DIVIDEND');
|
return this.receivedRows.reduce((t, x) => t + x.received, 0);
|
||||||
}
|
}
|
||||||
get pendingInterestHoldings(): InvestmentHolding[] {
|
get pendingInterestHoldings(): InvestmentHolding[] {
|
||||||
const receivedCerts = new Set(this.receivedInterestTxns.map((t) => t.certNo));
|
return this.active().filter((h) => this.accrued(h) > 0);
|
||||||
return ALL_HOLDINGS.filter(
|
|
||||||
(h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE' && !receivedCerts.has(h.certNo)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Maturity & Lists --------------------
|
// -------------------- Maturity & Lists --------------------
|
||||||
get maturedFdList(): InvestmentHolding[] {
|
get maturedFdList(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS.filter((h) => h.status === 'MATURED');
|
return this.ofType('FD').filter((h) => h.status === 'MATURED');
|
||||||
}
|
}
|
||||||
get maturingList(): InvestmentHolding[] {
|
get maturingList(): InvestmentHolding[] {
|
||||||
return [
|
return [
|
||||||
...FD_HOLDINGS,
|
...this.active('FD'),
|
||||||
...GOVT_SECURITY_HOLDINGS
|
...this.active('GOVT_SECURITY')
|
||||||
]
|
].sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
|
||||||
.filter((h) => h.status === 'ACTIVE')
|
|
||||||
.sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
|
|
||||||
}
|
|
||||||
get maturityBuckets(): Array<{ label: string; count: number; faceValue: number }> {
|
|
||||||
const buckets = [
|
|
||||||
{ label: '0-1 Year', min: -99999, max: 365, count: 0, faceValue: 0 },
|
|
||||||
{ label: '1-3 Years', min: 366, max: 1095, count: 0, faceValue: 0 },
|
|
||||||
{ label: '3-5 Years', min: 1096, max: 1825, count: 0, faceValue: 0 },
|
|
||||||
{ label: 'Above 5 Years', min: 1826, max: 999999, count: 0, faceValue: 0 }
|
|
||||||
];
|
|
||||||
for (const h of ALL_HOLDINGS.filter((x) => x.status === 'ACTIVE')) {
|
|
||||||
const days = this.daysLeft(h);
|
|
||||||
const bucket = buckets.find((b) => days >= b.min && days <= b.max);
|
|
||||||
if (bucket) {
|
|
||||||
bucket.count += 1;
|
|
||||||
bucket.faceValue += h.faceValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buckets;
|
|
||||||
}
|
}
|
||||||
get govtSecList(): InvestmentHolding[] {
|
get govtSecList(): InvestmentHolding[] {
|
||||||
return GOVT_SECURITY_HOLDINGS;
|
return this.ofType('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
get fdSecList(): InvestmentHolding[] {
|
get fdSecList(): InvestmentHolding[] {
|
||||||
return FD_HOLDINGS;
|
return this.ofType('FD');
|
||||||
}
|
}
|
||||||
get shareSecList(): InvestmentHolding[] {
|
get shareSecList(): InvestmentHolding[] {
|
||||||
return SHARE_HOLDINGS;
|
return this.ofType('SHARE');
|
||||||
}
|
}
|
||||||
get mutualFundList(): InvestmentHolding[] {
|
get mutualFundList(): InvestmentHolding[] {
|
||||||
return MUTUAL_FUND_HOLDINGS;
|
return this.ofType('MUTUAL_FUND');
|
||||||
}
|
|
||||||
get nonSlrList(): InvestmentHolding[] {
|
|
||||||
return NON_SLR_HOLDINGS;
|
|
||||||
}
|
|
||||||
get bondList(): InvestmentHolding[] {
|
|
||||||
return BOND_HOLDINGS;
|
|
||||||
}
|
|
||||||
/** Shifting Report — SLR securities whose market value has moved away from book value by
|
|
||||||
* more than 1%, a proxy for "category shift" candidates (AFS/HTM reclassification review). */
|
|
||||||
get shiftingCandidates(): Array<InvestmentHolding & { marketPrice: number; deviationPct: number }> {
|
|
||||||
return GOVT_SECURITY_HOLDINGS.map((h) => {
|
|
||||||
const mp = this.marketPrice(h.scriptCode);
|
|
||||||
if (mp === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const deviationPct = +(((mp - h.purchaseRate) / h.purchaseRate) * 100).toFixed(2);
|
|
||||||
return { ...h, marketPrice: mp, deviationPct };
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
(x): x is InvestmentHolding & { marketPrice: number; deviationPct: number } =>
|
|
||||||
x !== null && Math.abs(x.deviationPct) >= 1
|
|
||||||
)
|
|
||||||
.sort((a, b) => Math.abs(b.deviationPct) - Math.abs(a.deviationPct));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Portfolio --------------------
|
// -------------------- Portfolio --------------------
|
||||||
private readonly portfolioTypes: InstrumentType[] = [
|
private readonly portfolioTypes: InstrumentType[] = [
|
||||||
'FD',
|
|
||||||
'GOVT_SECURITY',
|
'GOVT_SECURITY',
|
||||||
|
'FD',
|
||||||
'NON_SLR',
|
'NON_SLR',
|
||||||
'BOND',
|
'BOND',
|
||||||
'MUTUAL_FUND',
|
'MUTUAL_FUND',
|
||||||
@@ -231,7 +443,7 @@ export class InvestmentReportsComponent {
|
|||||||
maturityValue: number;
|
maturityValue: number;
|
||||||
}> {
|
}> {
|
||||||
return this.portfolioTypes.map((type) => {
|
return this.portfolioTypes.map((type) => {
|
||||||
const holdings = ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
const holdings = this.active(type);
|
||||||
return {
|
return {
|
||||||
label: INSTRUMENT_LABELS[type],
|
label: INSTRUMENT_LABELS[type],
|
||||||
count: holdings.length,
|
count: holdings.length,
|
||||||
@@ -241,6 +453,9 @@ export class InvestmentReportsComponent {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
get portfolioTotalCount(): number {
|
||||||
|
return this.portfolioSummary.reduce((s, r) => s + r.count, 0);
|
||||||
|
}
|
||||||
get portfolioTotalFaceValue(): number {
|
get portfolioTotalFaceValue(): number {
|
||||||
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
|
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
|
||||||
}
|
}
|
||||||
@@ -290,7 +505,7 @@ export class InvestmentReportsComponent {
|
|||||||
h.purchaseRate,
|
h.purchaseRate,
|
||||||
h.interestRate,
|
h.interestRate,
|
||||||
h.purchaseDate,
|
h.purchaseDate,
|
||||||
h.maturityDate,
|
h.instrumentType === 'MUTUAL_FUND' ? '' : h.maturityDate,
|
||||||
this.accrued(h),
|
this.accrued(h),
|
||||||
this.maturity(h),
|
this.maturity(h),
|
||||||
h.status
|
h.status
|
||||||
@@ -413,17 +628,94 @@ export class InvestmentReportsComponent {
|
|||||||
this.amortizeTxns.map((t) => this.txnExportRow(t))
|
this.amortizeTxns.map((t) => this.txnExportRow(t))
|
||||||
);
|
);
|
||||||
case 'dealSlip':
|
case 'dealSlip':
|
||||||
return one(
|
return [
|
||||||
'Deal Slip',
|
{
|
||||||
'Deal Slip Register',
|
sheetName: 'Deal Slip Register',
|
||||||
this.txnExportHeaders,
|
title: 'Deal Slip Register',
|
||||||
this.txnExportWidths,
|
subtitle:
|
||||||
this.dealSlips.map((t) => this.txnExportRow(t))
|
`Deals dated ${this.slipFrom} to ${this.slipTo}` +
|
||||||
);
|
(this.slipGaps.length ? ` · missing numbers: ${this.slipGaps.join(', ')}` : ''),
|
||||||
|
headers: [
|
||||||
|
'Slip No',
|
||||||
|
'Type',
|
||||||
|
'Status',
|
||||||
|
'Deal Date',
|
||||||
|
'Deal Time',
|
||||||
|
'Settlement',
|
||||||
|
'Counterparty',
|
||||||
|
'Security',
|
||||||
|
'ISIN',
|
||||||
|
'Units',
|
||||||
|
'Price',
|
||||||
|
'Consideration',
|
||||||
|
'Brokerage',
|
||||||
|
'Net Amount',
|
||||||
|
'Direct / Broker',
|
||||||
|
'Broker',
|
||||||
|
'Confirmation Sent By',
|
||||||
|
'Confirmation Received',
|
||||||
|
'Confirmation Ref',
|
||||||
|
'Entered By',
|
||||||
|
'Approved By',
|
||||||
|
'Settled By',
|
||||||
|
'Cancelled By'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
16,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
|
12,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
|
28,
|
||||||
|
32,
|
||||||
|
14,
|
||||||
|
12,
|
||||||
|
10,
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
24,
|
||||||
|
18,
|
||||||
|
16,
|
||||||
|
18,
|
||||||
|
24,
|
||||||
|
24,
|
||||||
|
24,
|
||||||
|
24
|
||||||
|
],
|
||||||
|
rows: this.slips.map((s) => [
|
||||||
|
s.slipNo,
|
||||||
|
s.side,
|
||||||
|
s.status,
|
||||||
|
s.dealDate,
|
||||||
|
s.dealTime ?? '',
|
||||||
|
`${s.settlementType ?? ''} ${s.settlementDate}`.trim(),
|
||||||
|
s.counterparty ?? '',
|
||||||
|
`${s.scriptCode} ${s.scriptDesc}`,
|
||||||
|
s.isin ?? '',
|
||||||
|
s.units,
|
||||||
|
s.price ?? null,
|
||||||
|
s.consideration,
|
||||||
|
s.brokerage ?? 0,
|
||||||
|
s.netAmount,
|
||||||
|
s.dealMode === 'BROKER' ? 'Broker' : 'Direct',
|
||||||
|
s.brokerCode ? `${s.brokerCode} ${this.brokerName(s.brokerCode)}`.trim() : '',
|
||||||
|
this.confirmationLabel(s.confirmationMode),
|
||||||
|
s.confirmationReceivedOn ?? (this.confirmationAwaited(s) ? 'Awaited' : ''),
|
||||||
|
s.confirmationRef ?? '',
|
||||||
|
s.enteredBy ? `${s.enteredBy} ${s.enteredOn ?? ''}`.trim() : '',
|
||||||
|
s.approvedBy ? `${s.approvedBy} ${s.approvedOn ?? ''}`.trim() : '',
|
||||||
|
s.settledBy ? `${s.settledBy} ${s.settledOn ?? ''}`.trim() : '',
|
||||||
|
s.cancelledBy ? `${s.cancelledBy} ${s.cancelledOn ?? ''}`.trim() : ''
|
||||||
|
])
|
||||||
|
}
|
||||||
|
];
|
||||||
case 'intDueRegister':
|
case 'intDueRegister':
|
||||||
return one(
|
return one(
|
||||||
'Interest Due Register',
|
'Interest Due Register',
|
||||||
'Interest Due Register (Non-Cumulative Holdings)',
|
'Interest Due Register (interest earned, not yet received)',
|
||||||
[
|
[
|
||||||
...this.holdingsExportHeaders,
|
...this.holdingsExportHeaders,
|
||||||
'Due Amount'
|
'Due Amount'
|
||||||
@@ -449,11 +741,34 @@ export class InvestmentReportsComponent {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sheetName: 'Received Interest',
|
sheetName: 'Received Interest',
|
||||||
title: 'Received Interest / Dividend',
|
title: 'Interest Received',
|
||||||
subtitle,
|
subtitle: `${this.receivedFrom} to ${this.asOnDate}`,
|
||||||
headers: this.txnExportHeaders,
|
headers: [
|
||||||
widths: this.txnExportWidths,
|
'Investment',
|
||||||
rows: this.receivedInterestTxns.map((t) => this.txnExportRow(t))
|
'Deal No',
|
||||||
|
'Particulars',
|
||||||
|
'Received'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
14,
|
||||||
|
16,
|
||||||
|
44,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...this.receivedRows.map((x) => [
|
||||||
|
x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond',
|
||||||
|
x.dealNo,
|
||||||
|
x.particulars,
|
||||||
|
x.received
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
this.receivedTotal
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sheetName: 'Pending Interest',
|
sheetName: 'Pending Interest',
|
||||||
@@ -480,26 +795,6 @@ export class InvestmentReportsComponent {
|
|||||||
this.holdingsExportWidths,
|
this.holdingsExportWidths,
|
||||||
this.maturingList.map((h) => this.holdingsExportRow(h))
|
this.maturingList.map((h) => this.holdingsExportRow(h))
|
||||||
);
|
);
|
||||||
case 'maturityPattern':
|
|
||||||
return one(
|
|
||||||
'Maturity Pattern',
|
|
||||||
'Maturity Pattern Register',
|
|
||||||
[
|
|
||||||
'Bucket',
|
|
||||||
'Holdings',
|
|
||||||
'Face Value'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22,
|
|
||||||
14,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
this.maturityBuckets.map((b) => [
|
|
||||||
b.label,
|
|
||||||
b.count,
|
|
||||||
b.faceValue
|
|
||||||
])
|
|
||||||
);
|
|
||||||
case 'govtSecList':
|
case 'govtSecList':
|
||||||
return one(
|
return one(
|
||||||
'Govt Security List',
|
'Govt Security List',
|
||||||
@@ -535,27 +830,33 @@ export class InvestmentReportsComponent {
|
|||||||
case 'shiftingReport':
|
case 'shiftingReport':
|
||||||
return one(
|
return one(
|
||||||
'Shifting Report',
|
'Shifting Report',
|
||||||
'Shifting Report — SLR Market Deviation ≥ 1%',
|
'Shifting Report — securities transferred to another script / category',
|
||||||
[
|
[
|
||||||
'Cert No',
|
'Date',
|
||||||
'Script',
|
'Deal No',
|
||||||
'Purchase Rate',
|
'From',
|
||||||
'Market Price',
|
'To',
|
||||||
'Deviation %'
|
'Units',
|
||||||
|
'Book Value',
|
||||||
|
'Voucher No'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
14,
|
12,
|
||||||
30,
|
|
||||||
16,
|
16,
|
||||||
|
34,
|
||||||
|
34,
|
||||||
|
12,
|
||||||
16,
|
16,
|
||||||
14
|
20
|
||||||
],
|
],
|
||||||
this.shiftingCandidates.map((x) => [
|
this.transfers.map((x) => [
|
||||||
x.certNo,
|
x.transferDate,
|
||||||
x.scriptName,
|
x.dealNo,
|
||||||
x.purchaseRate,
|
`${x.fromScriptCode} ${x.fromScriptDesc}`,
|
||||||
x.marketPrice,
|
`${x.toScriptCode} ${x.toScriptDesc}`,
|
||||||
x.deviationPct
|
x.units,
|
||||||
|
x.bookValue,
|
||||||
|
x.voucherNo ?? ''
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
case 'portfolio':
|
case 'portfolio':
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { ShareInvestmentComponent } from './share-investment/share-investment.co
|
|||||||
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
||||||
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
||||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||||
|
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
||||||
|
import { InvestmentInterestReportComponent } from './investment-interest-report/investment-interest-report.component';
|
||||||
|
import { InvestmentBucketReportComponent } from './investment-bucket-report/investment-bucket-report.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -54,6 +57,21 @@ const routes: Routes = [
|
|||||||
component: InvestmentInterestComponent,
|
component: InvestmentInterestComponent,
|
||||||
data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' }
|
data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'diary',
|
||||||
|
component: InvestmentDiaryComponent,
|
||||||
|
data: { title: 'Due Date Diary', breadcrumb: 'Due Date Diary' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'interest-report',
|
||||||
|
component: InvestmentInterestReportComponent,
|
||||||
|
data: { title: 'Interest Received & Receivable', breadcrumb: 'Interest Received & Receivable' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bucket-report',
|
||||||
|
component: InvestmentBucketReportComponent,
|
||||||
|
data: { title: 'Bucket Wise Investment', breadcrumb: 'Bucket Wise Investment' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'reports',
|
path: 'reports',
|
||||||
component: InvestmentReportsComponent,
|
component: InvestmentReportsComponent,
|
||||||
|
|||||||
@@ -260,3 +260,14 @@
|
|||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inv-hint {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #15803d;
|
||||||
|
|
||||||
|
&--bad {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,8 @@
|
|||||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · SHARE</div>
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · SHARE</div>
|
||||||
<h1 class="pg-title">Share Entry</h1>
|
<h1 class="pg-title">Share Entry</h1>
|
||||||
<p class="pg-sub">
|
<p class="pg-sub">
|
||||||
Equity and cooperative-society share holdings — Purchase, Dividend, Sell and Write-off entries.
|
Shares of the DCCB / State Co-op Bank, co-operative societies in the area of operation, and membership shares
|
||||||
|
(NPCI, CCIL, SWIFT, Umbrella Organisation, IDPIC) — held as AFS. No other company's shares.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@if (activeTab === 'holdings') {
|
@if (activeTab === 'holdings') {
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
[class.pg-btn--active]="activeTab === 'dividend'"
|
[class.pg-btn--active]="activeTab === 'dividend'"
|
||||||
(click)="setTab('dividend')"
|
(click)="setTab('dividend')"
|
||||||
>
|
>
|
||||||
02 Share Interest / Dividend Entry
|
02 Share Dividend Entry
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
[class.pg-btn--active]="activeTab === 'sell'"
|
[class.pg-btn--active]="activeTab === 'sell'"
|
||||||
(click)="setTab('sell')"
|
(click)="setTab('sell')"
|
||||||
>
|
>
|
||||||
03 Share Sell Entry
|
03 Share Refund / Retirement
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -71,34 +72,97 @@
|
|||||||
@if (activeTab === 'holdings') {
|
@if (activeTab === 'holdings') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Share Holdings Register</div>
|
<div class="pos-section-label">Share Holdings Register</div>
|
||||||
|
@if (settleTarget?.kind === 'purchase') {
|
||||||
|
<div class="pos-section-label">
|
||||||
|
Settle {{ settleTarget?.label }} — confirmation from the institution (back office)
|
||||||
|
</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Confirmation received on</span
|
||||||
|
><mifosx-date-ddmmyyyy [(ngModel)]="settleConfirmation.receivedOn"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Confirmation / letter ref.</span
|
||||||
|
><input type="text" maxlength="100" [(ngModel)]="settleConfirmation.ref"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
Record the institution's confirmation (share certificate / letter) if it has come; leave it blank otherwise
|
||||||
|
and the Deal Slip Register shows it as awaited.
|
||||||
|
</p>
|
||||||
|
<div class="inv-form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="settlingDealId !== null"
|
||||||
|
(click)="confirmSettle()"
|
||||||
|
>
|
||||||
|
{{ settlingDealId !== null ? 'Settling…' : 'Settle and post to GL' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="settleTarget = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cert No</th>
|
<th>Deal / Cert No</th>
|
||||||
|
<th>Institution</th>
|
||||||
<th>Script</th>
|
<th>Script</th>
|
||||||
<th>Party</th>
|
<th class="num">Shares</th>
|
||||||
<th class="num">Units</th>
|
|
||||||
<th class="num">Rate</th>
|
|
||||||
<th class="num">Face Value</th>
|
<th class="num">Face Value</th>
|
||||||
|
<th class="num">Book Value</th>
|
||||||
|
<th class="num">Dividends</th>
|
||||||
<th>Purchase Date</th>
|
<th>Purchase Date</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (h of holdings; track h.certNo) {
|
@for (d of deals; track d.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ h.certNo }}</span>
|
<span class="pg-chip">{{ d.dealNo }}</span> <small>{{ d.certNo }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ h.scriptName }}</td>
|
|
||||||
<td>{{ h.party }}</td>
|
|
||||||
<td class="num">{{ h.units | number }}</td>
|
|
||||||
<td class="num">₹{{ h.purchaseRate | number: '1.2-2' }}</td>
|
|
||||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
|
||||||
<td>{{ h.purchaseDate }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
{{ d.partyName }}<br /><small
|
||||||
|
>{{ institutionLabel(dealParty(d)?.institutionType)
|
||||||
|
}}{{ dealParty(d)?.inLiquidation ? ' · in liquidation' : '' }}</small
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ d.scriptCode }} {{ d.scriptDesc }}</td>
|
||||||
|
<td class="num">{{ d.balanceUnits | number: '1.0-0' }}</td>
|
||||||
|
<td class="num">₹{{ faceHeld(d) | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ carryingValue(d) | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ dividendsReceived(d) | number: '1.2-2' }}</td>
|
||||||
|
<td>{{ d.settlementDate }}</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="statusClass(d.status)">{{ d.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (d.status === 'PENDING_SETTLEMENT') {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="settlingDealId !== null || settleTarget !== null"
|
||||||
|
(click)="openPurchaseSettle(d)"
|
||||||
|
>
|
||||||
|
Settle
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--ghost"
|
||||||
|
[disabled]="settlingDealId !== null"
|
||||||
|
(click)="dropDeal(d)"
|
||||||
|
>
|
||||||
|
Drop
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(d.dealNo)">Deal Slip</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="pg-empty">No share holdings yet.</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -106,143 +170,460 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- PURCHASE ENTRY -->
|
<!-- 01 PURCHASE ENTRY -->
|
||||||
@if (activeTab === 'purchase') {
|
@if (activeTab === 'purchase') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Share Purchase Entry</div>
|
<div class="pos-section-label">Investment - Share Purchase Entry (AFS)</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Cert No</span><input type="text" [(ngModel)]="purchaseForm.certNo" readonly
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Script Code</span><input type="text" [(ngModel)]="purchaseForm.scriptCode"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Script/Company Name *</span><input type="text" [(ngModel)]="purchaseForm.scriptName"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Party / Exchange</span><input type="text" [(ngModel)]="purchaseForm.party"
|
|
||||||
/></label>
|
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Sub Type</span>
|
<span>* Institution</span>
|
||||||
<select [(ngModel)]="purchaseForm.subType">
|
<select [(ngModel)]="purchaseForm.partyId" (ngModelChange)="refreshLimit()">
|
||||||
<option value="EQUITY">EQUITY</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
<option value="CO-OP SHARE">CO-OP SHARE</option>
|
@for (p of parties; track p.id) {
|
||||||
|
<option [ngValue]="p.id">{{ p.partyCode }} — {{ p.partyName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@if (purchaseParty; as p) {
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Institution Type</span><input type="text" [value]="institutionLabel(p.institutionType)" readonly
|
||||||
|
/></label>
|
||||||
|
}
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Share Script</span>
|
||||||
|
<select [(ngModel)]="purchaseForm.scriptId" (ngModelChange)="onScriptChange()">
|
||||||
|
<option [ngValue]="null">-- Select --</option>
|
||||||
|
@for (s of scripts; track s.id) {
|
||||||
|
<option [ngValue]="s.id">{{ s.scriptCode }} — {{ s.scriptDesc }}</option>
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>No. of Units *</span><input type="number" [(ngModel)]="purchaseForm.units" min="0"
|
><span>* Deal Date</span
|
||||||
/></label>
|
><mifosx-date-ddmmyyyy
|
||||||
<label class="inv-form-field"
|
[(ngModel)]="purchaseForm.dealDate"
|
||||||
><span>Purchase Rate *</span><input type="number" [(ngModel)]="purchaseForm.purchaseRate" min="0" step="0.01"
|
(ngModelChange)="refreshLimit()"
|
||||||
/></label>
|
></mifosx-date-ddmmyyyy
|
||||||
<label class="inv-form-field"
|
|
||||||
><span>Purchase Date *</span
|
|
||||||
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.purchaseDate"></mifosx-date-ddmmyyyy
|
|
||||||
></label>
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Settlement</span>
|
||||||
|
<select [(ngModel)]="purchaseForm.settlementType">
|
||||||
|
<option value="T+0">T+0</option>
|
||||||
|
<option value="T+1">T+1</option>
|
||||||
|
<option value="T+2">T+2</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Broker Code</span><input type="text" [(ngModel)]="purchaseForm.brokerCode"
|
><span>* No. of Shares</span
|
||||||
|
><input type="number" min="1" step="1" [(ngModel)]="purchaseForm.shares" (ngModelChange)="refreshLimit()"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>CR To A/c</span><input type="text" [(ngModel)]="purchaseForm.crToAc"
|
><span>Face Value per Share</span
|
||||||
|
><input
|
||||||
|
type="text"
|
||||||
|
[value]="purchaseScript ? '₹' + (purchaseScript.faceValuePerUnit | number: '1.2-2') : ''"
|
||||||
|
readonly
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Price per Share (₹)</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
[(ngModel)]="purchaseForm.pricePerShare"
|
||||||
|
(ngModelChange)="refreshLimit()"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Face Value</span><input type="text" [value]="'₹' + (purchaseFaceValue | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Amount Paid</span><input type="text" [value]="'₹' + (purchaseAmount | number: '1.2-2')" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Paid from (Bank GL)</span>
|
||||||
|
<select [(ngModel)]="purchaseForm.bankGlCode">
|
||||||
|
@for (b of bankOptions; track b.glCode) {
|
||||||
|
<option [value]="b.glCode">{{ b.glCode }} — {{ b.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Category</span><input type="text" value="AFS (Available For Sale)" readonly
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Confirmation to Institution</span>
|
||||||
|
<select [(ngModel)]="purchaseForm.confirmationMode">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (m of confirmationModes; track m.value) {
|
||||||
|
<option [value]="m.value">{{ m.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (shareLimit; as l) {
|
||||||
|
<div class="pos-section-label">
|
||||||
|
RBI Limit — shares of co-operative societies (owned funds as on {{ l.asOnDate }})
|
||||||
|
</div>
|
||||||
|
@if (!l.exempt) {
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Limit</th>
|
||||||
|
<th class="num">Allowed</th>
|
||||||
|
<th class="num">Now</th>
|
||||||
|
<th class="num">After this deal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
All co-operative societies — 2% of owned funds
|
||||||
|
{{
|
||||||
|
l.ownedFunds !== null && l.ownedFunds !== undefined
|
||||||
|
? '₹' + (l.ownedFunds | number: '1.2-2')
|
||||||
|
: '(not entered)'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
{{ l.coopLimit !== null && l.coopLimit !== undefined ? '₹' + (l.coopLimit | number: '1.2-2') : '—' }}
|
||||||
|
</td>
|
||||||
|
<td class="num">₹{{ l.coopOutstanding | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ l.coopAfter | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
@if (l.partyName) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ l.partyName }} — 5% of subscribed capital
|
||||||
|
{{
|
||||||
|
l.subscribedCapital !== null && l.subscribedCapital !== undefined
|
||||||
|
? '₹' + (l.subscribedCapital | number: '1.2-2')
|
||||||
|
: '(not entered)'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="num">
|
||||||
|
{{
|
||||||
|
l.institutionLimit !== null && l.institutionLimit !== undefined
|
||||||
|
? '₹' + (l.institutionLimit | number: '1.2-2')
|
||||||
|
: '—'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="num">₹{{ l.institutionOutstanding | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ l.institutionAfter | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
<p class="inv-hint" [class.inv-hint--bad]="!l.withinLimits">{{ l.message }}</p>
|
||||||
|
@if (!l.exempt && (l.ownedFunds === null || l.ownedFunds === undefined)) {
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Owned Funds as on {{ l.asOnDate }} (₹)</span
|
||||||
|
><input type="number" min="0" [(ngModel)]="ownedFunds" placeholder="Paid-up share capital + reserves"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Total Deposits as on {{ l.asOnDate }} (₹)</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[(ngModel)]="totalDeposits"
|
||||||
|
placeholder="Only if not entered on the FD screen"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<div class="inv-form-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveOwnedFunds()">
|
||||||
|
Save Owned Funds
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (shareLimitError) {
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ shareLimitError }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="pos-section-label">Accounting (posted on Settle, dated the settlement date)</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
Dr 100706 Investment in Shares / Cr {{ purchaseForm.bankGlCode }} ₹{{ purchaseAmount | number: '1.2-2' }}
|
||||||
|
</p>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelPurchase()">Cancel</button>
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelPurchase()">Cancel</button>
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="savePurchase()">Save</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="saving || (shareLimit !== null && !shareLimit.withinLimits)"
|
||||||
|
(click)="savePurchase()"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- DIVIDEND ENTRY -->
|
<!-- 02 DIVIDEND ENTRY -->
|
||||||
@if (activeTab === 'dividend') {
|
@if (activeTab === 'dividend') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Share Interest / Dividend Entry</div>
|
<div class="pos-section-label">Investment - Share Dividend Entry (income when received)</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Holding</span>
|
||||||
<select [(ngModel)]="dividendCertNo">
|
<select [(ngModel)]="dividend.purchaseId" (ngModelChange)="onDividendDealChange()">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldings; track h.certNo) {
|
@for (d of dividendDeals; track d.id) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="d.id">
|
||||||
|
{{ d.dealNo }} — {{ d.partyName }} ({{ d.balanceUnits | number: '1.0-0' }} shares)
|
||||||
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (dividendHolding; as h) {
|
|
||||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
|
||||||
}
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Dividend per Share *</span><input type="number" [(ngModel)]="dividendPerShare" min="0" step="0.01"
|
><span>* Date Received</span><mifosx-date-ddmmyyyy [(ngModel)]="dividend.dividendDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Dividend Amount (₹)</span><input type="number" min="0" step="0.01" [(ngModel)]="dividend.amount"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Received in (Bank GL)</span>
|
||||||
|
<select [(ngModel)]="dividend.bankGlCode">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (b of bankOptions; track b.glCode) {
|
||||||
|
<option [value]="b.glCode">{{ b.glCode }} — {{ b.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Total Dividend (computed)</span
|
><span>Remarks</span
|
||||||
><input type="text" [value]="'₹' + (dividendAmount | number: '1.2-2')" readonly
|
><input type="text" maxlength="500" [(ngModel)]="dividend.remarks" placeholder="e.g. Dividend for FY 2025-26"
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
Dr {{ dividend.bankGlCode || 'Bank' }} / Cr 400404 Dividend on Investments ₹{{
|
||||||
|
dividend.amount || 0 | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveDividend()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveDividend()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- SELL ENTRY -->
|
<!-- 03 REFUND / RETIREMENT -->
|
||||||
@if (activeTab === 'sell') {
|
@if (activeTab === 'sell') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Share Sell Entry</div>
|
<div class="pos-section-label">Investment - Share Refund / Retirement Entry</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Holding</span>
|
||||||
<select [(ngModel)]="sellCertNo">
|
<select [(ngModel)]="refund.purchaseId" (ngModelChange)="onRefundDealChange()">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldings; track h.certNo) {
|
@for (d of activeDeals; track d.id) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="d.id">
|
||||||
|
{{ d.dealNo }} — {{ d.partyName }} ({{ d.balanceUnits | number: '1.0-0' }} shares)
|
||||||
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (sellHolding; as h) {
|
|
||||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
|
||||||
}
|
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Units to Sell *</span><input type="number" [(ngModel)]="sellUnits" min="0"
|
><span>* Refund Date</span
|
||||||
|
><mifosx-date-ddmmyyyy
|
||||||
|
[(ngModel)]="refund.dealDate"
|
||||||
|
(ngModelChange)="refreshRefundQuote()"
|
||||||
|
></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>* Shares Refunded</span
|
||||||
|
><input type="number" min="1" step="1" [(ngModel)]="refund.units" (ngModelChange)="refreshRefundQuote()"
|
||||||
/></label>
|
/></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Sell Rate *</span><input type="number" [(ngModel)]="sellRate" min="0" step="0.01"
|
><span>* Price per Share (₹)</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
[(ngModel)]="refund.pricePerShare"
|
||||||
|
(ngModelChange)="refreshRefundQuote()"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>* Received in (Bank GL)</span>
|
||||||
|
<select [(ngModel)]="refund.bankGlCode" (ngModelChange)="refreshRefundQuote()">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (b of bankOptions; track b.glCode) {
|
||||||
|
<option [value]="b.glCode">{{ b.glCode }} — {{ b.glName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Consideration (computed)</span
|
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="refund.remarks"
|
||||||
><input type="text" [value]="'₹' + (sellConsideration | number: '1.2-2')" readonly
|
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Confirmation to Institution</span>
|
||||||
|
<select [(ngModel)]="refund.confirmationMode">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (m of confirmationModes; track m.value) {
|
||||||
|
<option [value]="m.value">{{ m.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@if (refundQuote; as q) {
|
||||||
|
<table class="pg-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Amount received</td>
|
||||||
|
<td class="num">₹{{ q.consideration | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Book value taken off (Cr 100706)</td>
|
||||||
|
<td class="num">₹{{ q.bookValueSold | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ q.profitLoss >= 0 ? 'Profit (Cr 400405)' : 'Loss (Dr 500605)' }}</td>
|
||||||
|
<td class="num">₹{{ (q.profitLoss >= 0 ? q.profitLoss : -q.profitLoss) | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Settlement date</td>
|
||||||
|
<td class="num">{{ q.settlementDate }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
@if (refundQuoteError) {
|
||||||
|
<p class="inv-hint inv-hint--bad">{{ refundQuoteError }}</p>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving || !refundQuote" (click)="saveRefund()">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pos-section-label">Pending Refunds (posted on Settle)</div>
|
||||||
|
@if (settleTarget?.kind === 'sale') {
|
||||||
|
<div class="pos-section-label">
|
||||||
|
Settle {{ settleTarget?.label }} — confirmation from the institution (back office)
|
||||||
|
</div>
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Confirmation received on</span
|
||||||
|
><mifosx-date-ddmmyyyy [(ngModel)]="settleConfirmation.receivedOn"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>Confirmation / letter ref.</span
|
||||||
|
><input type="text" maxlength="100" [(ngModel)]="settleConfirmation.ref"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p class="inv-hint">
|
||||||
|
Record the institution's confirmation (share certificate / letter) if it has come; leave it blank otherwise
|
||||||
|
and the Deal Slip Register shows it as awaited.
|
||||||
|
</p>
|
||||||
|
<div class="inv-form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="busySaleId !== null"
|
||||||
|
(click)="confirmSettle()"
|
||||||
|
>
|
||||||
|
{{ busySaleId !== null ? 'Settling…' : 'Settle and post to GL' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="settleTarget = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sale No / Deal No</th>
|
||||||
|
<th>Institution</th>
|
||||||
|
<th class="num">Shares</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
<th class="num">Profit / Loss</th>
|
||||||
|
<th>Settlement Date</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (s of pendingRefunds; track s.id) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ s.saleNo || s.dealNo }}</span>
|
||||||
|
@if (s.saleNo) {
|
||||||
|
<br /><small>Deal {{ s.dealNo }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>{{ s.partyName }}</td>
|
||||||
|
<td class="num">{{ s.units | number: '1.0-0' }}</td>
|
||||||
|
<td class="num">₹{{ s.netAmount | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">
|
||||||
|
{{ s.profitLoss < 0 ? 'Loss' : 'Profit' }} ₹{{
|
||||||
|
(s.profitLoss < 0 ? -s.profitLoss : s.profitLoss) | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td>{{ s.settlementDate }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--primary"
|
||||||
|
[disabled]="busySaleId !== null || settleTarget !== null"
|
||||||
|
(click)="openRefundSettle(s)"
|
||||||
|
>
|
||||||
|
Settle
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pg-btn pg-btn--ghost"
|
||||||
|
[disabled]="busySaleId !== null"
|
||||||
|
(click)="dropRefund(s)"
|
||||||
|
>
|
||||||
|
Drop
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(s.saleNo)">Deal Slip</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="pg-empty">No pending refunds.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- WRITE OFF ENTRY -->
|
<!-- 04 WRITE OFF ENTRY -->
|
||||||
@if (activeTab === 'writeoff') {
|
@if (activeTab === 'writeoff') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">Investment - Share Write Off Entry</div>
|
<div class="pos-section-label">Investment - Share Write Off Entry</div>
|
||||||
<div class="inv-form-grid">
|
<div class="inv-form-grid">
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>Cert No *</span>
|
<span>* Holding</span>
|
||||||
<select [(ngModel)]="writeoffCertNo">
|
<select [(ngModel)]="writeoffPurchaseId">
|
||||||
<option value="">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of activeHoldings; track h.certNo) {
|
@for (d of activeDeals; track d.id) {
|
||||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
<option [ngValue]="d.id">{{ d.dealNo }} — {{ d.partyName }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@if (writeoffHolding; as h) {
|
@if (writeoffDeal; as d) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Face Value to be Written Off</span
|
><span>Book Value</span><input type="text" [value]="'₹' + (carryingValue(d) | number: '1.2-2')" readonly
|
||||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
|
||||||
/></label>
|
/></label>
|
||||||
}
|
}
|
||||||
<label class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
<label class="inv-form-field"
|
||||||
|
><span>* Amount to Write Off (₹)</span><input type="number" min="0" step="0.01" [(ngModel)]="writeoffAmount"
|
||||||
|
/></label>
|
||||||
|
<label class="inv-form-field"><span>* Reason</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
||||||
</div>
|
</div>
|
||||||
|
@if (writeoffDeal) {
|
||||||
|
<p class="inv-hint">
|
||||||
|
@if (writeoffInLiquidation) {
|
||||||
|
The institution is in liquidation: the shares may be written off in full; they stay held at nil until the
|
||||||
|
liquidator pays.
|
||||||
|
} @else {
|
||||||
|
Only part of the book value can be written off; enter a refund to close the holding.
|
||||||
|
}
|
||||||
|
Dr 500605 Loss on Investments / Cr 100706 Investment in Shares.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveWriteoff()">Save</button>
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="saving" (click)="saveWriteoff()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -262,7 +643,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (t of txns; track t.voucherNo) {
|
@for (t of txns; track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.txnDate }}</td>
|
<td>{{ t.txnDate }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -273,6 +654,10 @@
|
|||||||
<td>{{ t.particulars }}</td>
|
<td>{{ t.particulars }}</td>
|
||||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="pg-empty">No share transactions yet.</td>
|
||||||
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -6,31 +6,73 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { forkJoin } from 'rxjs';
|
||||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
import { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data';
|
import { InvestmentTxn, SETTLEMENT_BANK_GL_OPTIONS, todayDDMMYYYY } from '../investment-data';
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import { saveDealSlipPdf } from '../deal-slip-pdf';
|
||||||
|
import {
|
||||||
|
InvestmentDividendDto,
|
||||||
|
InvestmentPartyDto,
|
||||||
|
InvestmentPurchaseDto,
|
||||||
|
InvestmentSaleDto,
|
||||||
|
InvestmentScriptDto,
|
||||||
|
InvestmentService,
|
||||||
|
InvestmentShareLimitDto,
|
||||||
|
SHARE_INSTITUTION_TYPES,
|
||||||
|
CONFIRMATION_MODES,
|
||||||
|
SettleConfirmation,
|
||||||
|
apiErrorMessage,
|
||||||
|
awaitingApproval,
|
||||||
|
sentForApproval,
|
||||||
|
toPurchaseTxn,
|
||||||
|
toSaleTxn
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff';
|
type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff';
|
||||||
|
|
||||||
|
/** Shares are always held as AFS. */
|
||||||
|
const SHARE_CATEGORY = 'AFS (Available For Sale)';
|
||||||
|
|
||||||
interface SharePurchaseForm {
|
interface SharePurchaseForm {
|
||||||
certNo: string;
|
partyId: number | null;
|
||||||
scriptCode: string;
|
scriptId: number | null;
|
||||||
scriptName: string;
|
dealDate: string;
|
||||||
party: string;
|
settlementType: string;
|
||||||
subType: string;
|
shares: number | null;
|
||||||
|
pricePerShare: number | null;
|
||||||
|
bankGlCode: string;
|
||||||
|
/** How the deal was confirmed to the institution (deal slip). */
|
||||||
|
confirmationMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShareRefundForm {
|
||||||
|
purchaseId: number | null;
|
||||||
|
dealDate: string;
|
||||||
units: number | null;
|
units: number | null;
|
||||||
purchaseRate: number | null;
|
pricePerShare: number | null;
|
||||||
purchaseDate: string;
|
bankGlCode: string;
|
||||||
brokerCode: string;
|
remarks: string;
|
||||||
crToAc: string;
|
confirmationMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShareDividendForm {
|
||||||
|
purchaseId: number | null;
|
||||||
|
dividendDate: string;
|
||||||
|
amount: number | null;
|
||||||
|
bankGlCode: string;
|
||||||
|
remarks: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Entry treasury screen — Holdings register + Purchase / Dividend / Sell / Write-off
|
* Share Entry — shares a UCB may hold under the RBI directions: of its DCCB / State Co-op Bank, of co-operative
|
||||||
* entry tabs (legacy "02 Share Entry" menu). Dummy in-memory data; Save appends locally.
|
* societies in its area of operation, and membership shares of NPCI / CCIL / SWIFT, the Umbrella Organisation and
|
||||||
|
* IDPIC; no other company's shares. Always AFS. A purchase posts on Settle (Dr 100706 / Cr bank); a dividend is income
|
||||||
|
* when it is received (Dr bank / Cr 400404); a refund or retirement of shares is entered as a sale at the price paid
|
||||||
|
* back; shares of an institution in liquidation can be written off in full.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-share-investment',
|
selector: 'mifosx-share-investment',
|
||||||
@@ -46,214 +88,662 @@ interface SharePurchaseForm {
|
|||||||
DateDdmmyyyyPickerComponent
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ShareInvestmentComponent {
|
export class ShareInvestmentComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
activeTab: ShareTab = 'holdings';
|
activeTab: ShareTab = 'holdings';
|
||||||
asOnDate = todayDDMMYYYY();
|
asOnDate = todayDDMMYYYY();
|
||||||
|
|
||||||
holdings: InvestmentHolding[] = [...SHARE_HOLDINGS];
|
/** Share deals saved in the backend (dropped ones left out). */
|
||||||
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
deals: InvestmentPurchaseDto[] = [];
|
||||||
|
/** Refunds, retirements and write-offs of the share deals. */
|
||||||
|
sales: InvestmentSaleDto[] = [];
|
||||||
|
dividends: InvestmentDividendDto[] = [];
|
||||||
|
/** Every share institution, for the institution type and liquidation flag of a deal. */
|
||||||
|
private partyById = new Map<number, InvestmentPartyDto>();
|
||||||
|
/** Active share institutions and share scripts, offered for a new purchase. */
|
||||||
|
parties: InvestmentPartyDto[] = [];
|
||||||
|
scripts: InvestmentScriptDto[] = [];
|
||||||
|
readonly bankOptions = SETTLEMENT_BANK_GL_OPTIONS;
|
||||||
|
/** GL id for a GL code — the APIs take ids, the forms work with codes. */
|
||||||
|
private glIdByCode = new Map<string, number>();
|
||||||
|
|
||||||
generating = false;
|
generating = false;
|
||||||
|
saving = false;
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
purchaseForm: SharePurchaseForm = this.blankPurchaseForm();
|
purchaseForm: SharePurchaseForm = this.blankPurchaseForm();
|
||||||
dividendCertNo = '';
|
shareLimit: InvestmentShareLimitDto | null = null;
|
||||||
dividendPerShare: number | null = null;
|
shareLimitError = '';
|
||||||
sellCertNo = '';
|
ownedFunds: number | null = null;
|
||||||
sellUnits: number | null = null;
|
totalDeposits: number | null = null;
|
||||||
sellRate: number | null = null;
|
refund: ShareRefundForm = this.blankRefund();
|
||||||
writeoffCertNo = '';
|
refundQuote: InvestmentSaleDto | null = null;
|
||||||
|
refundQuoteError = '';
|
||||||
|
dividend: ShareDividendForm = this.blankDividend();
|
||||||
|
writeoffPurchaseId: number | null = null;
|
||||||
|
writeoffAmount: number | null = null;
|
||||||
writeoffReason = '';
|
writeoffReason = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.investmentService.getAssetGlAccounts().subscribe({
|
||||||
|
next: (gls) =>
|
||||||
|
(this.glIdByCode = new Map(
|
||||||
|
gls.map((g) => [
|
||||||
|
g.glCode,
|
||||||
|
g.id
|
||||||
|
])
|
||||||
|
)),
|
||||||
|
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
|
forkJoin({
|
||||||
|
parties: this.investmentService.getParties(),
|
||||||
|
scripts: this.investmentService.getScripts()
|
||||||
|
}).subscribe({
|
||||||
|
next: ({ parties, scripts }) => {
|
||||||
|
const shareParties = parties.filter((p) => p.bondType === 'SHARE');
|
||||||
|
this.partyById = new Map(
|
||||||
|
shareParties.map((p) => [
|
||||||
|
p.id,
|
||||||
|
p
|
||||||
|
])
|
||||||
|
);
|
||||||
|
this.parties = shareParties.filter((p) => p.active && !!p.institutionType);
|
||||||
|
this.scripts = scripts.filter((s) => s.bondType === 'SHARE' && s.active);
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
setTab(tab: ShareTab): void {
|
setTab(tab: ShareTab): void {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
|
if (tab === 'purchase' && !this.shareLimit) {
|
||||||
|
this.refreshLimit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHoldings(): InvestmentHolding[] {
|
/** Holdings, refunds and dividends as saved in the backend. */
|
||||||
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
private load(): void {
|
||||||
|
forkJoin({
|
||||||
|
purchases: this.investmentService.getPurchases(),
|
||||||
|
sales: this.investmentService.getSales(),
|
||||||
|
dividends: this.investmentService.getDividends()
|
||||||
|
}).subscribe({
|
||||||
|
next: ({ purchases, sales, dividends }) => {
|
||||||
|
this.deals = purchases.filter((p) => p.bondType === 'SHARE' && p.status !== 'DROPPED');
|
||||||
|
this.sales = sales.filter((s) => s.bondType === 'SHARE');
|
||||||
|
this.dividends = dividends;
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(`Could not load share holdings: ${apiErrorMessage(err)}`)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Holdings --------------------
|
||||||
|
institutionLabel(type?: string): string {
|
||||||
|
return SHARE_INSTITUTION_TYPES.find((t) => t.value === type)?.label ?? type ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
dealParty(d: InvestmentPurchaseDto): InvestmentPartyDto | undefined {
|
||||||
|
return this.partyById.get(d.partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Face value of the shares still held after any refund. */
|
||||||
|
faceHeld(d: InvestmentPurchaseDto): number {
|
||||||
|
return d.units ? Math.round((d.faceValue * d.balanceUnits * 100) / d.units) / 100 : d.faceValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Book value as it stands: cost less what refunds and write-offs have taken off. */
|
||||||
|
carryingValue(d: InvestmentPurchaseDto): number {
|
||||||
|
const taken = this.sales
|
||||||
|
.filter((x) => x.purchaseId === d.id && x.status === 'SETTLED')
|
||||||
|
.reduce((sum, x) => sum + x.bookValueSold, 0);
|
||||||
|
return Math.round((d.bookValue - taken) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
dividendsReceived(d: InvestmentPurchaseDto): number {
|
||||||
|
return this.dividends.filter((x) => x.purchaseId === d.id).reduce((sum, x) => sum + x.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeDeals(): InvestmentPurchaseDto[] {
|
||||||
|
return this.deals.filter((d) => d.status === 'ACTIVE');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A dividend for a year the shares were held can come in after they were refunded. */
|
||||||
|
get dividendDeals(): InvestmentPurchaseDto[] {
|
||||||
|
return this.deals.filter((d) => d.status === 'ACTIVE' || d.status === 'SOLD');
|
||||||
|
}
|
||||||
|
|
||||||
|
get pendingRefunds(): InvestmentSaleDto[] {
|
||||||
|
return this.sales.filter((x) => x.status === 'PENDING_SETTLEMENT');
|
||||||
|
}
|
||||||
|
|
||||||
|
statusClass(status: string): string {
|
||||||
|
return `inv-badge inv-badge--${status.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Settle with the institution's confirmation (back office) --------------------
|
||||||
|
/** The deal or refund the back office is settling, with the confirmation it records (both optional). */
|
||||||
|
settleTarget: { kind: 'purchase' | 'sale'; id: number; label: string } | null = null;
|
||||||
|
settleConfirmation: SettleConfirmation = {};
|
||||||
|
readonly confirmationModes = CONFIRMATION_MODES;
|
||||||
|
|
||||||
|
openPurchaseSettle(d: InvestmentPurchaseDto): void {
|
||||||
|
this.settleTarget = { kind: 'purchase', id: d.id, label: `deal ${d.dealNo}` };
|
||||||
|
this.settleConfirmation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
openRefundSettle(s: InvestmentSaleDto): void {
|
||||||
|
if (!s.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settleTarget = { kind: 'sale', id: s.id, label: `refund ${s.saleNo ?? ''} on deal ${s.dealNo}` };
|
||||||
|
this.settleConfirmation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmSettle(): void {
|
||||||
|
const t = this.settleTarget;
|
||||||
|
if (!t) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t.kind === 'purchase') {
|
||||||
|
const d = this.deals.find((x) => x.id === t.id);
|
||||||
|
if (d) {
|
||||||
|
this.settleDeal(d, this.settleConfirmation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const s = this.sales.find((x) => x.id === t.id);
|
||||||
|
if (s) {
|
||||||
|
this.settleRefund(s, this.settleConfirmation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downloads the deal slip (PDF) of a purchase deal number or a refund's sale number. */
|
||||||
|
printDealSlip(slipNo?: string): void {
|
||||||
|
if (!slipNo) {
|
||||||
|
this.toast('This deal has no deal slip number.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.getDealSlip(slipNo).subscribe({
|
||||||
|
next: (rows) => (rows.length ? saveDealSlipPdf(rows[0]) : this.toast(`Deal slip ${slipNo} not found.`)),
|
||||||
|
error: (err) => this.toast(apiErrorMessage(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
||||||
|
settlingDealId: number | null = null;
|
||||||
|
|
||||||
|
/** Posts a pending deal to GL dated its settlement date: Dr 100706 Investment in Shares / Cr bank. */
|
||||||
|
settleDeal(d: InvestmentPurchaseDto, confirmation: SettleConfirmation = {}): void {
|
||||||
|
if (this.settlingDealId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settlingDealId = d.id;
|
||||||
|
this.investmentService.settlePurchase(d.id, confirmation).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.settlingDealId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(`Settlement of deal ${d.dealNo}`)
|
||||||
|
: `Deal ${d.dealNo} settled and posted to GL (transaction ${res.transactionId}).`
|
||||||
|
);
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.settlingDealId = null;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dropDeal(d: InvestmentPurchaseDto): void {
|
||||||
|
if (this.settlingDealId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settlingDealId = d.id;
|
||||||
|
this.investmentService.deletePurchase(d.id).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.settlingDealId = null;
|
||||||
|
this.toast(awaitingApproval(res) ? sentForApproval(`Drop of deal ${d.dealNo}`) : `Deal ${d.dealNo} dropped.`);
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.settlingDealId = null;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- 01 Purchase --------------------
|
||||||
private blankPurchaseForm(): SharePurchaseForm {
|
private blankPurchaseForm(): SharePurchaseForm {
|
||||||
return {
|
return {
|
||||||
certNo: `SH-${6000 + this.holdings.length + 1}`,
|
partyId: null,
|
||||||
scriptCode: '',
|
scriptId: null,
|
||||||
scriptName: '',
|
dealDate: todayDDMMYYYY(),
|
||||||
party: 'NSE/BSE',
|
settlementType: 'T+0',
|
||||||
subType: 'EQUITY',
|
shares: null,
|
||||||
units: null,
|
pricePerShare: null,
|
||||||
purchaseRate: null,
|
bankGlCode: SETTLEMENT_BANK_GL_OPTIONS[0].glCode,
|
||||||
purchaseDate: this.asOnDate,
|
confirmationMode: ''
|
||||||
brokerCode: '',
|
|
||||||
crToAc: '310070'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get purchaseScript(): InvestmentScriptDto | undefined {
|
||||||
|
return this.scripts.find((s) => s.id === this.purchaseForm.scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get purchaseParty(): InvestmentPartyDto | undefined {
|
||||||
|
return this.parties.find((p) => p.id === this.purchaseForm.partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get purchaseFaceValue(): number {
|
||||||
|
const s = this.purchaseScript;
|
||||||
|
return s && this.purchaseForm.shares ? Math.round(this.purchaseForm.shares * s.faceValuePerUnit * 100) / 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cost of the shares: what Settle debits to 100706. */
|
||||||
|
get purchaseAmount(): number {
|
||||||
|
const f = this.purchaseForm;
|
||||||
|
return f.shares && f.pricePerShare ? Math.round(f.shares * f.pricePerShare * 100) / 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Co-operative shares are taken at face value: the price defaults to it. */
|
||||||
|
onScriptChange(): void {
|
||||||
|
this.purchaseForm.pricePerShare = this.purchaseScript?.faceValuePerUnit ?? null;
|
||||||
|
this.refreshLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Each limit request is numbered so a slower, older response cannot overwrite the latest one. */
|
||||||
|
private limitSeq = 0;
|
||||||
|
|
||||||
|
/** Where the deal stands against the RBI limits on shares (2% of owned funds; 5% of one society's capital). */
|
||||||
|
refreshLimit(): void {
|
||||||
|
const f = this.purchaseForm;
|
||||||
|
const seq = ++this.limitSeq;
|
||||||
|
this.shareLimitError = '';
|
||||||
|
if (!f.dealDate) {
|
||||||
|
this.shareLimit = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.investmentService.getShareLimit(f.partyId, f.dealDate, this.purchaseAmount).subscribe({
|
||||||
|
next: (l) => {
|
||||||
|
if (seq === this.limitSeq) {
|
||||||
|
this.shareLimit = l;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
if (seq === this.limitSeq) {
|
||||||
|
this.shareLimit = null;
|
||||||
|
this.shareLimitError = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Owned funds (paid-up share capital + reserves) as on 31 March, kept with that year's total deposits. */
|
||||||
|
saveOwnedFunds(): void {
|
||||||
|
const asOn = this.shareLimit?.asOnDate;
|
||||||
|
if (!asOn || !this.ownedFunds || this.ownedFunds <= 0) {
|
||||||
|
this.toast(`Enter the owned funds as on ${asOn ?? '31 March'}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saving = true;
|
||||||
|
this.investmentService
|
||||||
|
.setDepositBase({
|
||||||
|
asOnDate: asOn,
|
||||||
|
ownedFunds: this.ownedFunds,
|
||||||
|
...(this.totalDeposits ? { totalDeposits: this.totalDeposits } : {}),
|
||||||
|
locale: 'en',
|
||||||
|
dateFormat: 'dd-MM-yyyy'
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res) ? sentForApproval(`Owned funds as on ${asOn}`) : `Owned funds as on ${asOn} saved.`
|
||||||
|
);
|
||||||
|
this.refreshLimit();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
savePurchase(): void {
|
savePurchase(): void {
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
if (!f.scriptName || !f.units || !f.purchaseRate) {
|
const script = this.purchaseScript;
|
||||||
this.toast('Please fill Script Name, No. of Units and Purchase Rate.');
|
const crGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
|
if (!f.partyId || !script || !f.shares || !f.pricePerShare || !f.dealDate) {
|
||||||
|
this.toast('Select the institution and the share script, and enter the number of shares and price per share.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const faceValue = f.units * f.purchaseRate;
|
if (!Number.isInteger(f.shares)) {
|
||||||
const holding: InvestmentHolding = {
|
this.toast('Enter a whole number of shares.');
|
||||||
certNo: f.certNo,
|
return;
|
||||||
instrumentType: 'SHARE',
|
}
|
||||||
scriptCode: f.scriptCode || f.scriptName,
|
if (!crGlAccountId) {
|
||||||
scriptName: f.scriptName,
|
this.toast(`GL ${f.bankGlCode} was not found in the chart of accounts.`);
|
||||||
party: f.party,
|
return;
|
||||||
subType: f.subType,
|
}
|
||||||
faceValue,
|
const body = {
|
||||||
purchaseRate: f.purchaseRate,
|
bondType: 'SHARE',
|
||||||
units: f.units,
|
scriptId: script.id,
|
||||||
purchaseDate: f.purchaseDate,
|
partyId: f.partyId,
|
||||||
maturityDate: f.purchaseDate,
|
dealDate: f.dealDate,
|
||||||
interestRate: 0,
|
dealTime: '',
|
||||||
interestFreq: 'YEARLY',
|
settlementType: f.settlementType,
|
||||||
glCode: '310060',
|
faceValue: this.purchaseFaceValue,
|
||||||
glName: 'INVESTMENT IN SHARES',
|
purchaseRate: f.pricePerShare,
|
||||||
crToAc: f.crToAc,
|
category: SHARE_CATEGORY,
|
||||||
brokerCode: f.brokerCode,
|
crGlAccountId,
|
||||||
status: 'ACTIVE'
|
subType: '',
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
};
|
};
|
||||||
this.holdings = [
|
this.saving = true;
|
||||||
holding,
|
this.investmentService.createPurchase(body).subscribe({
|
||||||
...this.holdings
|
next: (res) => {
|
||||||
];
|
this.saving = false;
|
||||||
this.txns = [
|
this.toast(
|
||||||
{
|
awaitingApproval(res)
|
||||||
certNo: holding.certNo,
|
? sentForApproval('Share purchase')
|
||||||
instrumentType: 'SHARE',
|
: `Deal ${res.resourceIdentifier} saved — pending settlement. Settle it from Holdings to post it.`
|
||||||
txnType: 'PURCHASE',
|
);
|
||||||
voucherNo: `INV/PUR/${holding.certNo}`,
|
this.purchaseForm = this.blankPurchaseForm();
|
||||||
txnDate: f.purchaseDate,
|
this.shareLimit = null;
|
||||||
amount: faceValue,
|
this.activeTab = 'holdings';
|
||||||
particulars: `Purchase of ${f.units} shares - ${holding.scriptName}`
|
this.load();
|
||||||
},
|
},
|
||||||
...this.txns
|
error: (err) => {
|
||||||
];
|
this.saving = false;
|
||||||
this.toast(`Share holding ${holding.certNo} saved successfully.`);
|
this.toast(apiErrorMessage(err));
|
||||||
this.purchaseForm = this.blankPurchaseForm();
|
}
|
||||||
this.activeTab = 'holdings';
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPurchase(): void {
|
cancelPurchase(): void {
|
||||||
this.purchaseForm = this.blankPurchaseForm();
|
this.purchaseForm = this.blankPurchaseForm();
|
||||||
|
this.shareLimit = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Dividend --------------------
|
// -------------------- 02 Dividend --------------------
|
||||||
get dividendHolding(): InvestmentHolding | undefined {
|
private blankDividend(): ShareDividendForm {
|
||||||
return this.holdings.find((h) => h.certNo === this.dividendCertNo);
|
return { purchaseId: null, dividendDate: todayDDMMYYYY(), amount: null, bankGlCode: '', remarks: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
get dividendAmount(): number {
|
get dividendDeal(): InvestmentPurchaseDto | undefined {
|
||||||
const h = this.dividendHolding;
|
return this.deals.find((d) => d.id === this.dividend.purchaseId);
|
||||||
if (!h || !this.dividendPerShare) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.round(h.units * this.dividendPerShare);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDividendDealChange(): void {
|
||||||
|
this.dividend.bankGlCode = this.dividendDeal?.crGlCode ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dr bank / Cr 400404 Dividend on Investments, dated the day the dividend was received. */
|
||||||
saveDividend(): void {
|
saveDividend(): void {
|
||||||
const h = this.dividendHolding;
|
const f = this.dividend;
|
||||||
if (!h || !this.dividendPerShare) {
|
const d = this.dividendDeal;
|
||||||
this.toast('Select a holding and enter dividend per share.');
|
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
|
if (!d || !f.amount || f.amount <= 0 || !f.dividendDate || !bankGlAccountId) {
|
||||||
|
this.toast('Select the holding and the bank, and enter the dividend date and amount received.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const amt = this.dividendAmount;
|
const amount = f.amount;
|
||||||
this.txns = [
|
this.saving = true;
|
||||||
{
|
this.investmentService
|
||||||
certNo: h.certNo,
|
.postDividend(d.id, {
|
||||||
instrumentType: 'SHARE',
|
dividendDate: f.dividendDate,
|
||||||
txnType: 'DIVIDEND',
|
amount,
|
||||||
voucherNo: `INV/DIV/${h.certNo}-${this.txns.length + 1}`,
|
bankGlAccountId,
|
||||||
txnDate: this.asOnDate,
|
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||||
amount: amt,
|
dateFormat: 'dd-MM-yyyy',
|
||||||
particulars: `Dividend received - ${h.scriptName} (₹${this.dividendPerShare}/share)`
|
locale: 'en'
|
||||||
},
|
})
|
||||||
...this.txns
|
.subscribe({
|
||||||
];
|
next: (res) => {
|
||||||
this.toast(`Dividend of ₹${amt.toLocaleString('en-IN')} recorded for ${h.certNo}.`);
|
this.saving = false;
|
||||||
this.dividendCertNo = '';
|
this.toast(
|
||||||
this.dividendPerShare = null;
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(`Dividend of ₹${amount} on deal ${d.dealNo}`)
|
||||||
|
: `Dividend of ₹${amount} on deal ${d.dealNo} posted — voucher ${res.resourceIdentifier ?? ''}.`
|
||||||
|
);
|
||||||
|
this.dividend = this.blankDividend();
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Sell --------------------
|
// -------------------- 03 Refund / Retirement --------------------
|
||||||
get sellHolding(): InvestmentHolding | undefined {
|
private blankRefund(): ShareRefundForm {
|
||||||
return this.holdings.find((h) => h.certNo === this.sellCertNo);
|
return {
|
||||||
|
purchaseId: null,
|
||||||
|
dealDate: todayDDMMYYYY(),
|
||||||
|
units: null,
|
||||||
|
pricePerShare: null,
|
||||||
|
bankGlCode: '',
|
||||||
|
remarks: '',
|
||||||
|
confirmationMode: ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get sellConsideration(): number {
|
get refundDeal(): InvestmentPurchaseDto | undefined {
|
||||||
if (!this.sellUnits || !this.sellRate) {
|
return this.deals.find((d) => d.id === this.refund.purchaseId);
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.round(this.sellUnits * this.sellRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSell(): void {
|
/** The institution usually pays back the face value of the shares. */
|
||||||
const h = this.sellHolding;
|
onRefundDealChange(): void {
|
||||||
if (!h || !this.sellUnits || !this.sellRate || this.sellUnits > h.units) {
|
const d = this.refundDeal;
|
||||||
this.toast('Select a holding and enter valid units (≤ held) and sell rate.');
|
this.refund.units = d ? d.balanceUnits : null;
|
||||||
|
this.refund.pricePerShare = d && d.units ? Math.round((d.faceValue / d.units) * 100) / 100 : null;
|
||||||
|
this.refund.bankGlCode = d?.crGlCode ?? '';
|
||||||
|
this.refreshRefundQuote();
|
||||||
|
}
|
||||||
|
|
||||||
|
private refundQuoteSeq = 0;
|
||||||
|
|
||||||
|
/** Amount received, book value taken off and profit / loss, worked out by the backend as Settle will post them. */
|
||||||
|
refreshRefundQuote(): void {
|
||||||
|
const f = this.refund;
|
||||||
|
const seq = ++this.refundQuoteSeq;
|
||||||
|
this.refundQuote = null;
|
||||||
|
this.refundQuoteError = '';
|
||||||
|
if (!f.purchaseId || !f.units || !f.pricePerShare || !f.dealDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const consideration = this.sellConsideration;
|
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
h.units -= this.sellUnits;
|
this.investmentService
|
||||||
h.faceValue = Math.round(h.units * h.purchaseRate);
|
.quoteSale({
|
||||||
if (h.units === 0) {
|
purchaseId: f.purchaseId,
|
||||||
h.status = 'SOLD';
|
dealDate: f.dealDate,
|
||||||
|
settlementType: 'T+0',
|
||||||
|
units: f.units,
|
||||||
|
saleRate: f.pricePerShare,
|
||||||
|
...(bankGlAccountId ? { bankGlAccountId } : {})
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (q) => {
|
||||||
|
if (seq === this.refundQuoteSeq) {
|
||||||
|
this.refundQuote = q;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
if (seq === this.refundQuoteSeq) {
|
||||||
|
this.refundQuoteError = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saves the refund pending; Settle posts Dr bank / Cr 100706, with any difference to 400405 / 500605. */
|
||||||
|
saveRefund(): void {
|
||||||
|
const f = this.refund;
|
||||||
|
const d = this.refundDeal;
|
||||||
|
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||||
|
if (!d || !f.units || !f.pricePerShare || !bankGlAccountId) {
|
||||||
|
this.toast('Select the holding and the bank, and enter the shares refunded and the price per share.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.txns = [
|
const body = {
|
||||||
{
|
purchaseId: d.id,
|
||||||
certNo: h.certNo,
|
// The institution itself pays the shares back.
|
||||||
instrumentType: 'SHARE',
|
partyId: d.partyId,
|
||||||
txnType: 'SELL',
|
dealDate: f.dealDate,
|
||||||
voucherNo: `INV/SEL/${h.certNo}-${this.txns.length + 1}`,
|
dealTime: '',
|
||||||
txnDate: this.asOnDate,
|
settlementType: 'T+0',
|
||||||
amount: consideration,
|
units: f.units,
|
||||||
particulars: `Sold ${this.sellUnits} shares of ${h.scriptName} @ ${this.sellRate}`
|
saleRate: f.pricePerShare,
|
||||||
|
bankGlAccountId,
|
||||||
|
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
|
dateFormat: 'dd-MM-yyyy',
|
||||||
|
locale: 'en'
|
||||||
|
};
|
||||||
|
this.saving = true;
|
||||||
|
this.investmentService.createSale(body).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res) ? sentForApproval('Share refund') : 'Refund saved — settle it below to post it.'
|
||||||
|
);
|
||||||
|
this.refund = this.blankRefund();
|
||||||
|
this.refundQuote = null;
|
||||||
|
this.load();
|
||||||
},
|
},
|
||||||
...this.txns
|
error: (err) => {
|
||||||
];
|
this.saving = false;
|
||||||
this.toast(`Sold ${this.sellUnits} shares. Consideration ₹${consideration.toLocaleString('en-IN')}.`);
|
this.toast(apiErrorMessage(err));
|
||||||
this.sellCertNo = '';
|
}
|
||||||
this.sellUnits = null;
|
});
|
||||||
this.sellRate = null;
|
|
||||||
this.activeTab = 'holdings';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Write-off --------------------
|
/** The pending refund being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
||||||
get writeoffHolding(): InvestmentHolding | undefined {
|
busySaleId: number | null = null;
|
||||||
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
|
|
||||||
|
settleRefund(s: InvestmentSaleDto, confirmation: SettleConfirmation = {}): void {
|
||||||
|
if (!s.id || this.busySaleId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.busySaleId = s.id;
|
||||||
|
this.investmentService.settleSale(s.id, confirmation).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.busySaleId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(`Settlement of the refund on deal ${s.dealNo}`)
|
||||||
|
: `Refund on deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`
|
||||||
|
);
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.busySaleId = null;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropRefund(s: InvestmentSaleDto): void {
|
||||||
|
if (!s.id || this.busySaleId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.busySaleId = s.id;
|
||||||
|
this.investmentService.deleteSale(s.id).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.busySaleId = null;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(`Drop of the pending refund on deal ${s.dealNo}`)
|
||||||
|
: `Pending refund on deal ${s.dealNo} dropped.`
|
||||||
|
);
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.busySaleId = null;
|
||||||
|
this.toast(apiErrorMessage(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- 04 Write Off --------------------
|
||||||
|
get writeoffDeal(): InvestmentPurchaseDto | undefined {
|
||||||
|
return this.deals.find((d) => d.id === this.writeoffPurchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shares of an institution in liquidation may be written off in full; otherwise only part of the book value. */
|
||||||
|
get writeoffInLiquidation(): boolean {
|
||||||
|
const d = this.writeoffDeal;
|
||||||
|
return !!d && !!this.dealParty(d)?.inLiquidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dr 500605 Loss on Investments / Cr 100706 Investment in Shares. */
|
||||||
saveWriteoff(): void {
|
saveWriteoff(): void {
|
||||||
const h = this.writeoffHolding;
|
const d = this.writeoffDeal;
|
||||||
if (!h || !this.writeoffReason) {
|
const amount = this.writeoffAmount;
|
||||||
this.toast('Select a holding and enter a write-off reason.');
|
if (!d || !amount || amount <= 0 || !this.writeoffReason.trim()) {
|
||||||
|
this.toast('Select the holding, enter the amount and the reason.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
h.status = 'WRITTEN_OFF';
|
const book = this.carryingValue(d);
|
||||||
this.txns = [
|
if (this.writeoffInLiquidation ? amount > book : amount >= book) {
|
||||||
{
|
this.toast(
|
||||||
certNo: h.certNo,
|
this.writeoffInLiquidation
|
||||||
instrumentType: 'SHARE',
|
? `The write-off cannot exceed the book value ₹${book}.`
|
||||||
txnType: 'WRITE_OFF',
|
: 'The write-off must be less than the book value; only shares of an institution in liquidation can be written off in full.'
|
||||||
voucherNo: `INV/WOF/${h.certNo}`,
|
);
|
||||||
txnDate: this.asOnDate,
|
return;
|
||||||
amount: h.faceValue,
|
}
|
||||||
particulars: `Written off - ${this.writeoffReason}`
|
this.saving = true;
|
||||||
|
this.investmentService.writeOff(d.id, { amount, remarks: this.writeoffReason.trim(), locale: 'en' }).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.toast(
|
||||||
|
awaitingApproval(res)
|
||||||
|
? sentForApproval(`Write-off of ₹${amount} on deal ${d.dealNo}`)
|
||||||
|
: `Deal ${d.dealNo}: ₹${amount} written off — voucher ${res.resourceIdentifier ?? ''}.`
|
||||||
|
);
|
||||||
|
this.writeoffPurchaseId = null;
|
||||||
|
this.writeoffAmount = null;
|
||||||
|
this.writeoffReason = '';
|
||||||
|
this.load();
|
||||||
},
|
},
|
||||||
...this.txns
|
error: (err) => {
|
||||||
];
|
this.saving = false;
|
||||||
this.toast(`${h.certNo} written off.`);
|
this.toast(apiErrorMessage(err));
|
||||||
this.writeoffCertNo = '';
|
}
|
||||||
this.writeoffReason = '';
|
});
|
||||||
this.activeTab = 'holdings';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusClass(h: InvestmentHolding): string {
|
// -------------------- Transaction log --------------------
|
||||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
/** Purchases, dividends, refunds and write-offs of the share deals, latest first. */
|
||||||
|
get txns(): InvestmentTxn[] {
|
||||||
|
const rows: InvestmentTxn[] = [
|
||||||
|
...this.deals.map(toPurchaseTxn),
|
||||||
|
...this.sales.filter((x) => x.status === 'SETTLED').map(toSaleTxn),
|
||||||
|
...this.dividends.map((x) => ({
|
||||||
|
certNo: x.certNo,
|
||||||
|
instrumentType: 'SHARE' as const,
|
||||||
|
txnType: 'DIVIDEND' as const,
|
||||||
|
voucherNo: x.voucherNo ?? `DIV/${x.dealNo}/${x.id}`,
|
||||||
|
txnDate: x.dividendDate,
|
||||||
|
amount: x.amount,
|
||||||
|
particulars: `Dividend - Deal ${x.dealNo} - ${x.scriptDesc} - ${x.partyName}${x.remarks ? ' - ' + x.remarks : ''}`,
|
||||||
|
glCode: x.bankGlCode,
|
||||||
|
glName: `${x.bankGlName} / Cr 400404 (txn ${x.glTransactionId})`
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
const key = (d: string) => d.split('-').reverse().join('');
|
||||||
|
return rows.sort((a, b) => key(b.txnDate).localeCompare(key(a.txnDate)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Export to Excel (Holdings register) --------------------
|
// -------------------- Export to Excel (Holdings register) --------------------
|
||||||
@@ -267,34 +757,43 @@ export class ShareInvestmentComponent {
|
|||||||
title: 'Share Holdings Register',
|
title: 'Share Holdings Register',
|
||||||
subtitle: `As on ${this.asOnDate}`,
|
subtitle: `As on ${this.asOnDate}`,
|
||||||
headers: [
|
headers: [
|
||||||
|
'Deal No',
|
||||||
'Cert No',
|
'Cert No',
|
||||||
|
'Institution',
|
||||||
|
'Institution Type',
|
||||||
'Script',
|
'Script',
|
||||||
'Party',
|
'Shares',
|
||||||
'Units',
|
|
||||||
'Purchase Rate',
|
|
||||||
'Face Value',
|
'Face Value',
|
||||||
|
'Book Value',
|
||||||
|
'Dividends Received',
|
||||||
'Purchase Date',
|
'Purchase Date',
|
||||||
'Status'
|
'Status'
|
||||||
],
|
],
|
||||||
widths: [
|
widths: [
|
||||||
14,
|
14,
|
||||||
|
14,
|
||||||
|
28,
|
||||||
24,
|
24,
|
||||||
22,
|
24,
|
||||||
12,
|
10,
|
||||||
14,
|
14,
|
||||||
16,
|
|
||||||
14,
|
14,
|
||||||
12
|
14,
|
||||||
|
14,
|
||||||
|
18
|
||||||
],
|
],
|
||||||
rows: this.holdings.map((h) => [
|
rows: this.deals.map((d) => [
|
||||||
h.certNo,
|
d.dealNo,
|
||||||
h.scriptName,
|
d.certNo,
|
||||||
h.party,
|
d.partyName,
|
||||||
h.units,
|
this.institutionLabel(this.dealParty(d)?.institutionType),
|
||||||
h.purchaseRate,
|
`${d.scriptCode} ${d.scriptDesc}`,
|
||||||
h.faceValue,
|
d.balanceUnits,
|
||||||
h.purchaseDate,
|
this.faceHeld(d),
|
||||||
h.status
|
this.carryingValue(d),
|
||||||
|
this.dividendsReceived(d),
|
||||||
|
d.settlementDate,
|
||||||
|
d.status
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -315,6 +814,6 @@ export class ShareInvestmentComponent {
|
|||||||
if (this.toastTimer) {
|
if (this.toastTimer) {
|
||||||
clearTimeout(this.toastTimer);
|
clearTimeout(this.toastTimer);
|
||||||
}
|
}
|
||||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
this.toastTimer = setTimeout(() => (this.toastVisible = false), 4000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user