feat(investment): share investment screens on live data
This commit is contained in:
@@ -1566,10 +1566,10 @@
|
||||
</label>
|
||||
</div>
|
||||
<p class="inv-hint">
|
||||
Each security still held is valued at the latest Daily Script Master rate / NAV on or before the date. Gains and
|
||||
losses are netted within Govt Securities, Corporate Bonds and Mutual Funds separately; a net loss is provided in
|
||||
the Investment Depreciation Reserve and a net gain is ignored. The book value does not change. HTM is not
|
||||
valued.
|
||||
Each security still held is valued at the latest Daily Script Master rate / NAV on or before the date;
|
||||
co-operative shares at face value, ₹1 or nil as RBI directs. Gains and losses are netted within Govt Securities,
|
||||
Corporate Bonds, Mutual Funds and Shares separately; a net loss is provided in the Investment Depreciation
|
||||
Reserve and a net gain is ignored. The book value does not change. HTM is not valued.
|
||||
</p>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" [disabled]="valuationLoading" (click)="previewValuation()">
|
||||
@@ -1658,7 +1658,12 @@
|
||||
<span class="pg-chip">{{ l.dealNo }}</span>
|
||||
</td>
|
||||
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
|
||||
<td>{{ valuationClassLabels[l.assetClass] }}</td>
|
||||
<td>
|
||||
{{ valuationClassLabels[l.assetClass] }}
|
||||
@if (l.valuationBasis) {
|
||||
<br /><small>{{ l.valuationBasis }}{{ l.npi ? ' · NPI' : '' }}</small>
|
||||
}
|
||||
</td>
|
||||
<td class="num">{{ l.units | number: '1.0-6' }}</td>
|
||||
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
|
||||
@@ -1734,7 +1739,12 @@
|
||||
<td>
|
||||
<span class="pg-chip">{{ l.dealNo }}</span>
|
||||
</td>
|
||||
<td>{{ l.scriptCode }} {{ l.scriptDesc }}</td>
|
||||
<td>
|
||||
{{ l.scriptCode }} {{ l.scriptDesc }}
|
||||
@if (l.valuationBasis) {
|
||||
<br /><small>{{ l.valuationBasis }}{{ l.npi ? ' · NPI' : '' }}</small>
|
||||
}
|
||||
</td>
|
||||
<td class="num">{{ l.units | number: '1.0-6' }}</td>
|
||||
<td class="num">₹{{ l.bookValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ l.rate | number: '1.2-4' }}</td>
|
||||
|
||||
@@ -1792,7 +1792,8 @@ export class BondInvestmentComponent implements OnInit {
|
||||
readonly valuationClassLabels: Record<string, string> = {
|
||||
GOVT_SECURITY: 'Govt Securities',
|
||||
CORPORATE_BOND: 'Corporate Bonds',
|
||||
MUTUAL_FUND: 'Mutual Funds'
|
||||
MUTUAL_FUND: 'Mutual Funds',
|
||||
SHARE: 'Shares'
|
||||
};
|
||||
|
||||
loadValuationRuns(): void {
|
||||
|
||||
@@ -2000,6 +2000,13 @@ export interface PartyMaster {
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -186,6 +186,33 @@
|
||||
/></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"
|
||||
@@ -245,7 +272,12 @@
|
||||
<span class="pg-chip">{{ p.partyCode }}</span>
|
||||
</td>
|
||||
<td>{{ p.bondType || '—' }}</td>
|
||||
<td>{{ p.partyName }}</td>
|
||||
<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>
|
||||
@@ -302,7 +334,7 @@
|
||||
><span>{{ scriptIsDebtSecurity ? '* ' : '' }}ISIN</span><input type="text" [(ngModel)]="scriptForm.isin"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value per Unit</span
|
||||
><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"
|
||||
|
||||
@@ -43,6 +43,7 @@ import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||
import {
|
||||
InvestmentGlAccount,
|
||||
InvestmentService,
|
||||
SHARE_INSTITUTION_TYPES,
|
||||
apiErrorMessage,
|
||||
awaitingApproval,
|
||||
sentForApproval,
|
||||
@@ -138,6 +139,7 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
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. */
|
||||
@@ -227,6 +229,15 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
this.toast('Enter the TDS rate (%) this bank deducts on FD interest.');
|
||||
return;
|
||||
}
|
||||
const share = f.bondType === 'SHARE';
|
||||
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,
|
||||
@@ -249,7 +260,13 @@ export class InvestmentMastersComponent implements OnInit {
|
||||
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);
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="pg-empty">
|
||||
{{ activeReport === 'shareBalance' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
||||
{{ activeReport === 'shareBalance' ? 'No share holdings.' : 'No holdings.' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -656,7 +656,7 @@
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">
|
||||
{{ activeReport === 'shareSecList' ? 'Share entry is not live yet.' : 'No holdings.' }}
|
||||
{{ activeReport === 'shareSecList' ? 'No share holdings.' : 'No holdings.' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -73,13 +73,71 @@ export interface InvestmentPartyDto {
|
||||
cpBranchName?: string;
|
||||
cpAccountNo?: string;
|
||||
cpIfsc?: string;
|
||||
/** Share institutions only: STATE_COOP_BANK, DCCB, OTHER_COOP_SOCIETY or MEMBERSHIP_MIC_UO. */
|
||||
institutionType?: ShareInstitutionType;
|
||||
/** The institution's subscribed share capital (the 5% limit on one co-operative society). */
|
||||
subscribedCapital?: number;
|
||||
/** Latest balance sheet received (DD-MM-YYYY); older than 18 months values its shares at ₹1. */
|
||||
lastBalanceSheetDate?: string;
|
||||
inLiquidation?: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/** Institutions whose shares a UCB may hold (RBI): no shares of other companies. */
|
||||
export type ShareInstitutionType = 'STATE_COOP_BANK' | 'DCCB' | 'OTHER_COOP_SOCIETY' | 'MEMBERSHIP_MIC_UO';
|
||||
|
||||
export const SHARE_INSTITUTION_TYPES: Array<{ value: ShareInstitutionType; label: string }> = [
|
||||
{ value: 'DCCB', label: 'District Central Co-op Bank (DCCB)' },
|
||||
{ value: 'STATE_COOP_BANK', label: 'State Co-op Bank' },
|
||||
{ value: 'OTHER_COOP_SOCIETY', label: 'Co-operative society (area of operation)' },
|
||||
{ value: 'MEMBERSHIP_MIC_UO', label: 'Membership: NPCI / CCIL / SWIFT / Umbrella Org / IDPIC' }
|
||||
];
|
||||
|
||||
/** Where a share purchase stands against the RBI limits (2% of owned funds, 5% of a society's capital). */
|
||||
export interface InvestmentShareLimitDto {
|
||||
asOnDate: string;
|
||||
institutionType?: ShareInstitutionType;
|
||||
/** Membership shares may exceed the limits. */
|
||||
exempt: boolean;
|
||||
ownedFunds?: number;
|
||||
coopLimit?: number;
|
||||
coopOutstanding: number;
|
||||
coopAfter: number;
|
||||
partyName?: string;
|
||||
subscribedCapital?: number;
|
||||
institutionLimit?: number;
|
||||
institutionOutstanding: number;
|
||||
institutionAfter: number;
|
||||
newAmount: number;
|
||||
withinLimits: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** A dividend received on a share holding, with its voucher (dates DD-MM-YYYY). */
|
||||
export interface InvestmentDividendDto {
|
||||
id: number;
|
||||
purchaseId: number;
|
||||
dealNo: string;
|
||||
certNo: string;
|
||||
scriptCode: string;
|
||||
scriptDesc: string;
|
||||
partyId: number;
|
||||
partyName: string;
|
||||
dividendDate: string;
|
||||
amount: number;
|
||||
bankGlCode: string;
|
||||
bankGlName: string;
|
||||
remarks?: string;
|
||||
glTransactionId?: string;
|
||||
voucherNo?: string;
|
||||
}
|
||||
|
||||
/** Where an FD stands against the RBI limits on deposits with other banks (20% overall, 5% with one bank). */
|
||||
export interface InvestmentFdLimitDto {
|
||||
asOnDate: string;
|
||||
totalDeposits?: number;
|
||||
/** Owned funds (paid-up share capital + reserves) as on the same 31 March: the 2% limit on co-operative shares. */
|
||||
ownedFunds?: number;
|
||||
glTotalDeposits: number;
|
||||
grossLimit?: number;
|
||||
fdOutstanding: number;
|
||||
@@ -447,9 +505,12 @@ export class InvestmentService {
|
||||
return this.http.get<InvestmentSaleDto[]>('/investment/sales');
|
||||
}
|
||||
|
||||
/** What a sale would come to before it is saved (dealDate dd-MM-yyyy). */
|
||||
/** What a sale would come to before it is saved (dealDate dd-MM-yyyy). Silent: the sale and refund screens show the error under the form. */
|
||||
quoteSale(params: Record<string, string | number>): Observable<InvestmentSaleDto> {
|
||||
return this.http.get<InvestmentSaleDto>('/investment/sales/quote', { params });
|
||||
return this.http.get<InvestmentSaleDto>('/investment/sales/quote', {
|
||||
params,
|
||||
context: new HttpContext().set(SILENT_HTTP_ERROR, true)
|
||||
});
|
||||
}
|
||||
|
||||
createSale(body: object): Observable<{ resourceId?: number; resourceIdentifier?: string }> {
|
||||
@@ -592,6 +653,30 @@ export class InvestmentService {
|
||||
return this.http.get<InvestmentNonSlrLimitDto>('/investment/purchases/non-slr-limit', { params });
|
||||
}
|
||||
|
||||
/** RBI limits for a share purchase of `amount` from an institution on a deal date (DD-MM-YYYY). */
|
||||
getShareLimit(partyId: number | null, dealDate: string, amount: number): Observable<InvestmentShareLimitDto> {
|
||||
const params: Record<string, string> = { dealDate, amount: String(amount || 0) };
|
||||
if (partyId) {
|
||||
params['partyId'] = String(partyId);
|
||||
}
|
||||
return this.http.get<InvestmentShareLimitDto>('/investment/purchases/share-limit', { params });
|
||||
}
|
||||
|
||||
getDividends(): Observable<InvestmentDividendDto[]> {
|
||||
return this.http.get<InvestmentDividendDto[]>('/investment/purchases/dividends');
|
||||
}
|
||||
|
||||
/** Records a dividend received: Dr bank / Cr 400404 Dividend on Investments. */
|
||||
postDividend(
|
||||
purchaseId: number,
|
||||
body: object
|
||||
): Observable<{ resourceIdentifier?: string; rollbackTransaction?: boolean }> {
|
||||
return this.http.post<{ resourceIdentifier?: string; rollbackTransaction?: boolean }>(
|
||||
`/investment/purchases/${purchaseId}/dividends`,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
createPurchase(body: object): Observable<{ resourceId: number; resourceIdentifier: string }> {
|
||||
return this.http.post<{ resourceId: number; resourceIdentifier: string }>('/investment/purchases', body);
|
||||
}
|
||||
@@ -744,6 +829,10 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
|
||||
cpBranchName: p.cpBranchName,
|
||||
cpAccountNo: p.cpAccountNo,
|
||||
cpIfsc: p.cpIfsc,
|
||||
institutionType: p.institutionType,
|
||||
subscribedCapital: p.subscribedCapital ?? null,
|
||||
lastBalanceSheetDate: p.lastBalanceSheetDate,
|
||||
inLiquidation: p.inLiquidation ?? false,
|
||||
status: p.active ? 'ACTIVE' : 'INACTIVE'
|
||||
};
|
||||
}
|
||||
@@ -1150,7 +1239,7 @@ export interface InvestmentValuationDto {
|
||||
missingRates: string[];
|
||||
postable: boolean;
|
||||
classes: Array<{
|
||||
assetClass: 'GOVT_SECURITY' | 'CORPORATE_BOND' | 'MUTUAL_FUND';
|
||||
assetClass: 'GOVT_SECURITY' | 'CORPORATE_BOND' | 'MUTUAL_FUND' | 'SHARE';
|
||||
bookValue: number;
|
||||
marketValue: number;
|
||||
netDifference: number;
|
||||
@@ -1170,6 +1259,10 @@ export interface InvestmentValuationDto {
|
||||
rateDate: string;
|
||||
marketValue: number;
|
||||
difference: number;
|
||||
/** How a share was valued (face value, nil, ₹1); empty for a market rate. */
|
||||
valuationBasis?: string;
|
||||
/** Shares of an institution whose latest balance sheet is over 18 months old. */
|
||||
npi?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · SHARE</div>
|
||||
<h1 class="pg-title">Share Entry</h1>
|
||||
<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>
|
||||
</div>
|
||||
@if (activeTab === 'holdings') {
|
||||
@@ -47,7 +48,7 @@
|
||||
[class.pg-btn--active]="activeTab === 'dividend'"
|
||||
(click)="setTab('dividend')"
|
||||
>
|
||||
02 Share Interest / Dividend Entry
|
||||
02 Share Dividend Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -55,7 +56,7 @@
|
||||
[class.pg-btn--active]="activeTab === 'sell'"
|
||||
(click)="setTab('sell')"
|
||||
>
|
||||
03 Share Sell Entry
|
||||
03 Share Refund / Retirement
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -74,31 +75,63 @@
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Deal / Cert No</th>
|
||||
<th>Institution</th>
|
||||
<th>Script</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Shares</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Book Value</th>
|
||||
<th class="num">Dividends</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of holdings; track h.certNo) {
|
||||
@for (d of deals; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
<span class="pg-chip">{{ d.dealNo }}</span> <small>{{ d.certNo }}</small>
|
||||
</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>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
{{ d.partyName }}<br /><small
|
||||
>{{ institutionLabel(dealParty(d)?.institutionType)
|
||||
}}{{ dealParty(d)?.inLiquidation ? ' · in liquidation' : '' }}</small
|
||||
>
|
||||
</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"
|
||||
(click)="settleDeal(d)"
|
||||
>
|
||||
Settle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="settlingDealId !== null"
|
||||
(click)="dropDeal(d)"
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="10" class="pg-empty">No share holdings yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -106,143 +139,408 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PURCHASE ENTRY -->
|
||||
<!-- 01 PURCHASE ENTRY -->
|
||||
@if (activeTab === 'purchase') {
|
||||
<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">
|
||||
<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">
|
||||
<span>Sub Type</span>
|
||||
<select [(ngModel)]="purchaseForm.subType">
|
||||
<option value="EQUITY">EQUITY</option>
|
||||
<option value="CO-OP SHARE">CO-OP SHARE</option>
|
||||
<span>* Institution</span>
|
||||
<select [(ngModel)]="purchaseForm.partyId" (ngModelChange)="refreshLimit()">
|
||||
<option [ngValue]="null">-- Select --</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>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>No. of Units *</span><input type="number" [(ngModel)]="purchaseForm.units" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Rate *</span><input type="number" [(ngModel)]="purchaseForm.purchaseRate" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Date *</span
|
||||
><mifosx-date-ddmmyyyy [(ngModel)]="purchaseForm.purchaseDate"></mifosx-date-ddmmyyyy
|
||||
><span>* Deal Date</span
|
||||
><mifosx-date-ddmmyyyy
|
||||
[(ngModel)]="purchaseForm.dealDate"
|
||||
(ngModelChange)="refreshLimit()"
|
||||
></mifosx-date-ddmmyyyy
|
||||
></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"
|
||||
><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 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 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>
|
||||
</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">
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- DIVIDEND ENTRY -->
|
||||
<!-- 02 DIVIDEND ENTRY -->
|
||||
@if (activeTab === 'dividend') {
|
||||
<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">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="dividendCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
<span>* Holding</span>
|
||||
<select [(ngModel)]="dividend.purchaseId" (ngModelChange)="onDividendDealChange()">
|
||||
<option [ngValue]="null">-- Select --</option>
|
||||
@for (d of dividendDeals; track d.id) {
|
||||
<option [ngValue]="d.id">
|
||||
{{ d.dealNo }} — {{ d.partyName }} ({{ d.balanceUnits | number: '1.0-0' }} shares)
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</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"
|
||||
><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 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"
|
||||
><span>Total Dividend (computed)</span
|
||||
><input type="text" [value]="'₹' + (dividendAmount | number: '1.2-2')" readonly
|
||||
><span>Remarks</span
|
||||
><input type="text" maxlength="500" [(ngModel)]="dividend.remarks" placeholder="e.g. Dividend for FY 2025-26"
|
||||
/></label>
|
||||
</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">
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- SELL ENTRY -->
|
||||
<!-- 03 REFUND / RETIREMENT -->
|
||||
@if (activeTab === 'sell') {
|
||||
<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">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="sellCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
<span>* Holding</span>
|
||||
<select [(ngModel)]="refund.purchaseId" (ngModelChange)="onRefundDealChange()">
|
||||
<option [ngValue]="null">-- Select --</option>
|
||||
@for (d of activeDeals; track d.id) {
|
||||
<option [ngValue]="d.id">
|
||||
{{ d.dealNo }} — {{ d.partyName }} ({{ d.balanceUnits | number: '1.0-0' }} shares)
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</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"
|
||||
><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 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 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"
|
||||
><span>Consideration (computed)</span
|
||||
><input type="text" [value]="'₹' + (sellConsideration | number: '1.2-2')" readonly
|
||||
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="refund.remarks"
|
||||
/></label>
|
||||
</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">
|
||||
<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 class="pos-section-label">Pending Refunds (posted on Settle)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>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.dealNo }}</span>
|
||||
</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"
|
||||
(click)="settleRefund(s)"
|
||||
>
|
||||
Settle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="busySaleId !== null"
|
||||
(click)="dropRefund(s)"
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="pg-empty">No pending refunds.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- WRITE OFF ENTRY -->
|
||||
<!-- 04 WRITE OFF ENTRY -->
|
||||
@if (activeTab === 'writeoff') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Write Off Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="writeoffCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
<span>* Holding</span>
|
||||
<select [(ngModel)]="writeoffPurchaseId">
|
||||
<option [ngValue]="null">-- Select --</option>
|
||||
@for (d of activeDeals; track d.id) {
|
||||
<option [ngValue]="d.id">{{ d.dealNo }} — {{ d.partyName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (writeoffHolding; as h) {
|
||||
@if (writeoffDeal; as d) {
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value to be Written Off</span
|
||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
><span>Book Value</span><input type="text" [value]="'₹' + (carryingValue(d) | number: '1.2-2')" readonly
|
||||
/></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>
|
||||
@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">
|
||||
<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>
|
||||
}
|
||||
@@ -262,7 +560,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of txns; track t.voucherNo) {
|
||||
@for (t of txns; track $index) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
@@ -273,6 +571,10 @@
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No share transactions yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -6,31 +6,67 @@
|
||||
* 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 { FormsModule } from '@angular/forms';
|
||||
import { forkJoin } from 'rxjs';
|
||||
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 {
|
||||
InvestmentDividendDto,
|
||||
InvestmentPartyDto,
|
||||
InvestmentPurchaseDto,
|
||||
InvestmentSaleDto,
|
||||
InvestmentScriptDto,
|
||||
InvestmentService,
|
||||
InvestmentShareLimitDto,
|
||||
SHARE_INSTITUTION_TYPES,
|
||||
apiErrorMessage,
|
||||
awaitingApproval,
|
||||
sentForApproval,
|
||||
toPurchaseTxn,
|
||||
toSaleTxn
|
||||
} from '../investment.service';
|
||||
|
||||
type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff';
|
||||
|
||||
/** Shares are always held as AFS. */
|
||||
const SHARE_CATEGORY = 'AFS (Available For Sale)';
|
||||
|
||||
interface SharePurchaseForm {
|
||||
certNo: string;
|
||||
scriptCode: string;
|
||||
scriptName: string;
|
||||
party: string;
|
||||
subType: string;
|
||||
partyId: number | null;
|
||||
scriptId: number | null;
|
||||
dealDate: string;
|
||||
settlementType: string;
|
||||
shares: number | null;
|
||||
pricePerShare: number | null;
|
||||
bankGlCode: string;
|
||||
}
|
||||
|
||||
interface ShareRefundForm {
|
||||
purchaseId: number | null;
|
||||
dealDate: string;
|
||||
units: number | null;
|
||||
purchaseRate: number | null;
|
||||
purchaseDate: string;
|
||||
brokerCode: string;
|
||||
crToAc: string;
|
||||
pricePerShare: number | null;
|
||||
bankGlCode: string;
|
||||
remarks: 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
|
||||
* entry tabs (legacy "02 Share Entry" menu). Dummy in-memory data; Save appends locally.
|
||||
* Share Entry — shares a UCB may hold under the RBI directions: of its DCCB / State Co-op Bank, of co-operative
|
||||
* 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({
|
||||
selector: 'mifosx-share-investment',
|
||||
@@ -46,214 +82,607 @@ interface SharePurchaseForm {
|
||||
DateDdmmyyyyPickerComponent
|
||||
]
|
||||
})
|
||||
export class ShareInvestmentComponent {
|
||||
export class ShareInvestmentComponent implements OnInit {
|
||||
private readonly investmentService = inject(InvestmentService);
|
||||
|
||||
activeTab: ShareTab = 'holdings';
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
holdings: InvestmentHolding[] = [...SHARE_HOLDINGS];
|
||||
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
||||
/** Share deals saved in the backend (dropped ones left out). */
|
||||
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;
|
||||
saving = false;
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
purchaseForm: SharePurchaseForm = this.blankPurchaseForm();
|
||||
dividendCertNo = '';
|
||||
dividendPerShare: number | null = null;
|
||||
sellCertNo = '';
|
||||
sellUnits: number | null = null;
|
||||
sellRate: number | null = null;
|
||||
writeoffCertNo = '';
|
||||
shareLimit: InvestmentShareLimitDto | null = null;
|
||||
shareLimitError = '';
|
||||
ownedFunds: number | null = null;
|
||||
totalDeposits: number | null = null;
|
||||
refund: ShareRefundForm = this.blankRefund();
|
||||
refundQuote: InvestmentSaleDto | null = null;
|
||||
refundQuoteError = '';
|
||||
dividend: ShareDividendForm = this.blankDividend();
|
||||
writeoffPurchaseId: number | null = null;
|
||||
writeoffAmount: number | null = null;
|
||||
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 {
|
||||
this.activeTab = tab;
|
||||
if (tab === 'purchase' && !this.shareLimit) {
|
||||
this.refreshLimit();
|
||||
}
|
||||
}
|
||||
|
||||
get activeHoldings(): InvestmentHolding[] {
|
||||
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
||||
/** Holdings, refunds and dividends as saved in the backend. */
|
||||
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()}`;
|
||||
}
|
||||
|
||||
/** 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): void {
|
||||
if (this.settlingDealId !== null) {
|
||||
return;
|
||||
}
|
||||
this.settlingDealId = d.id;
|
||||
this.investmentService.settlePurchase(d.id).subscribe({
|
||||
next: (res) => {
|
||||
this.settlingDealId = 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 {
|
||||
return {
|
||||
certNo: `SH-${6000 + this.holdings.length + 1}`,
|
||||
scriptCode: '',
|
||||
scriptName: '',
|
||||
party: 'NSE/BSE',
|
||||
subType: 'EQUITY',
|
||||
units: null,
|
||||
purchaseRate: null,
|
||||
purchaseDate: this.asOnDate,
|
||||
brokerCode: '',
|
||||
crToAc: '310070'
|
||||
partyId: null,
|
||||
scriptId: null,
|
||||
dealDate: todayDDMMYYYY(),
|
||||
settlementType: 'T+0',
|
||||
shares: null,
|
||||
pricePerShare: null,
|
||||
bankGlCode: SETTLEMENT_BANK_GL_OPTIONS[0].glCode
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
const f = this.purchaseForm;
|
||||
if (!f.scriptName || !f.units || !f.purchaseRate) {
|
||||
this.toast('Please fill Script Name, No. of Units and Purchase Rate.');
|
||||
const script = this.purchaseScript;
|
||||
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;
|
||||
}
|
||||
const faceValue = f.units * f.purchaseRate;
|
||||
const holding: InvestmentHolding = {
|
||||
certNo: f.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
scriptCode: f.scriptCode || f.scriptName,
|
||||
scriptName: f.scriptName,
|
||||
party: f.party,
|
||||
subType: f.subType,
|
||||
faceValue,
|
||||
purchaseRate: f.purchaseRate,
|
||||
units: f.units,
|
||||
purchaseDate: f.purchaseDate,
|
||||
maturityDate: f.purchaseDate,
|
||||
interestRate: 0,
|
||||
interestFreq: 'YEARLY',
|
||||
glCode: '310060',
|
||||
glName: 'INVESTMENT IN SHARES',
|
||||
crToAc: f.crToAc,
|
||||
brokerCode: f.brokerCode,
|
||||
status: 'ACTIVE'
|
||||
if (!Number.isInteger(f.shares)) {
|
||||
this.toast('Enter a whole number of shares.');
|
||||
return;
|
||||
}
|
||||
if (!crGlAccountId) {
|
||||
this.toast(`GL ${f.bankGlCode} was not found in the chart of accounts.`);
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
bondType: 'SHARE',
|
||||
scriptId: script.id,
|
||||
partyId: f.partyId,
|
||||
dealDate: f.dealDate,
|
||||
dealTime: '',
|
||||
settlementType: f.settlementType,
|
||||
faceValue: this.purchaseFaceValue,
|
||||
purchaseRate: f.pricePerShare,
|
||||
category: SHARE_CATEGORY,
|
||||
crGlAccountId,
|
||||
subType: '',
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
};
|
||||
this.holdings = [
|
||||
holding,
|
||||
...this.holdings
|
||||
];
|
||||
this.txns = [
|
||||
{
|
||||
certNo: holding.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'PURCHASE',
|
||||
voucherNo: `INV/PUR/${holding.certNo}`,
|
||||
txnDate: f.purchaseDate,
|
||||
amount: faceValue,
|
||||
particulars: `Purchase of ${f.units} shares - ${holding.scriptName}`
|
||||
this.saving = true;
|
||||
this.investmentService.createPurchase(body).subscribe({
|
||||
next: (res) => {
|
||||
this.saving = false;
|
||||
this.toast(
|
||||
awaitingApproval(res)
|
||||
? sentForApproval('Share purchase')
|
||||
: `Deal ${res.resourceIdentifier} saved — pending settlement. Settle it from Holdings to post it.`
|
||||
);
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.shareLimit = null;
|
||||
this.activeTab = 'holdings';
|
||||
this.load();
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Share holding ${holding.certNo} saved successfully.`);
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.activeTab = 'holdings';
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelPurchase(): void {
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.shareLimit = null;
|
||||
}
|
||||
|
||||
// -------------------- Dividend --------------------
|
||||
get dividendHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.dividendCertNo);
|
||||
// -------------------- 02 Dividend --------------------
|
||||
private blankDividend(): ShareDividendForm {
|
||||
return { purchaseId: null, dividendDate: todayDDMMYYYY(), amount: null, bankGlCode: '', remarks: '' };
|
||||
}
|
||||
|
||||
get dividendAmount(): number {
|
||||
const h = this.dividendHolding;
|
||||
if (!h || !this.dividendPerShare) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(h.units * this.dividendPerShare);
|
||||
get dividendDeal(): InvestmentPurchaseDto | undefined {
|
||||
return this.deals.find((d) => d.id === this.dividend.purchaseId);
|
||||
}
|
||||
|
||||
onDividendDealChange(): void {
|
||||
this.dividend.bankGlCode = this.dividendDeal?.crGlCode ?? '';
|
||||
}
|
||||
|
||||
/** Dr bank / Cr 400404 Dividend on Investments, dated the day the dividend was received. */
|
||||
saveDividend(): void {
|
||||
const h = this.dividendHolding;
|
||||
if (!h || !this.dividendPerShare) {
|
||||
this.toast('Select a holding and enter dividend per share.');
|
||||
const f = this.dividend;
|
||||
const d = this.dividendDeal;
|
||||
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;
|
||||
}
|
||||
const amt = this.dividendAmount;
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'DIVIDEND',
|
||||
voucherNo: `INV/DIV/${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: amt,
|
||||
particulars: `Dividend received - ${h.scriptName} (₹${this.dividendPerShare}/share)`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Dividend of ₹${amt.toLocaleString('en-IN')} recorded for ${h.certNo}.`);
|
||||
this.dividendCertNo = '';
|
||||
this.dividendPerShare = null;
|
||||
const amount = f.amount;
|
||||
this.saving = true;
|
||||
this.investmentService
|
||||
.postDividend(d.id, {
|
||||
dividendDate: f.dividendDate,
|
||||
amount,
|
||||
bankGlAccountId,
|
||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.saving = false;
|
||||
this.toast(
|
||||
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 --------------------
|
||||
get sellHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.sellCertNo);
|
||||
// -------------------- 03 Refund / Retirement --------------------
|
||||
private blankRefund(): ShareRefundForm {
|
||||
return {
|
||||
purchaseId: null,
|
||||
dealDate: todayDDMMYYYY(),
|
||||
units: null,
|
||||
pricePerShare: null,
|
||||
bankGlCode: '',
|
||||
remarks: ''
|
||||
};
|
||||
}
|
||||
|
||||
get sellConsideration(): number {
|
||||
if (!this.sellUnits || !this.sellRate) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(this.sellUnits * this.sellRate);
|
||||
get refundDeal(): InvestmentPurchaseDto | undefined {
|
||||
return this.deals.find((d) => d.id === this.refund.purchaseId);
|
||||
}
|
||||
|
||||
saveSell(): void {
|
||||
const h = this.sellHolding;
|
||||
if (!h || !this.sellUnits || !this.sellRate || this.sellUnits > h.units) {
|
||||
this.toast('Select a holding and enter valid units (≤ held) and sell rate.');
|
||||
/** The institution usually pays back the face value of the shares. */
|
||||
onRefundDealChange(): void {
|
||||
const d = this.refundDeal;
|
||||
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;
|
||||
}
|
||||
const consideration = this.sellConsideration;
|
||||
h.units -= this.sellUnits;
|
||||
h.faceValue = Math.round(h.units * h.purchaseRate);
|
||||
if (h.units === 0) {
|
||||
h.status = 'SOLD';
|
||||
const bankGlAccountId = this.glIdByCode.get(f.bankGlCode);
|
||||
this.investmentService
|
||||
.quoteSale({
|
||||
purchaseId: f.purchaseId,
|
||||
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 = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'SELL',
|
||||
voucherNo: `INV/SEL/${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: consideration,
|
||||
particulars: `Sold ${this.sellUnits} shares of ${h.scriptName} @ ${this.sellRate}`
|
||||
const body = {
|
||||
purchaseId: d.id,
|
||||
// The institution itself pays the shares back.
|
||||
partyId: d.partyId,
|
||||
dealDate: f.dealDate,
|
||||
dealTime: '',
|
||||
settlementType: 'T+0',
|
||||
units: f.units,
|
||||
saleRate: f.pricePerShare,
|
||||
bankGlAccountId,
|
||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||
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
|
||||
];
|
||||
this.toast(`Sold ${this.sellUnits} shares. Consideration ₹${consideration.toLocaleString('en-IN')}.`);
|
||||
this.sellCertNo = '';
|
||||
this.sellUnits = null;
|
||||
this.sellRate = null;
|
||||
this.activeTab = 'holdings';
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Write-off --------------------
|
||||
get writeoffHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
|
||||
/** The pending refund being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
||||
busySaleId: number | null = null;
|
||||
|
||||
settleRefund(s: InvestmentSaleDto): void {
|
||||
if (!s.id || this.busySaleId !== null) {
|
||||
return;
|
||||
}
|
||||
this.busySaleId = s.id;
|
||||
this.investmentService.settleSale(s.id).subscribe({
|
||||
next: (res) => {
|
||||
this.busySaleId = 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 {
|
||||
const h = this.writeoffHolding;
|
||||
if (!h || !this.writeoffReason) {
|
||||
this.toast('Select a holding and enter a write-off reason.');
|
||||
const d = this.writeoffDeal;
|
||||
const amount = this.writeoffAmount;
|
||||
if (!d || !amount || amount <= 0 || !this.writeoffReason.trim()) {
|
||||
this.toast('Select the holding, enter the amount and the reason.');
|
||||
return;
|
||||
}
|
||||
h.status = 'WRITTEN_OFF';
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'WRITE_OFF',
|
||||
voucherNo: `INV/WOF/${h.certNo}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: h.faceValue,
|
||||
particulars: `Written off - ${this.writeoffReason}`
|
||||
const book = this.carryingValue(d);
|
||||
if (this.writeoffInLiquidation ? amount > book : amount >= book) {
|
||||
this.toast(
|
||||
this.writeoffInLiquidation
|
||||
? `The write-off cannot exceed the book value ₹${book}.`
|
||||
: 'The write-off must be less than the book value; only shares of an institution in liquidation can be written off in full.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
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
|
||||
];
|
||||
this.toast(`${h.certNo} written off.`);
|
||||
this.writeoffCertNo = '';
|
||||
this.writeoffReason = '';
|
||||
this.activeTab = 'holdings';
|
||||
error: (err) => {
|
||||
this.saving = false;
|
||||
this.toast(apiErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
statusClass(h: InvestmentHolding): string {
|
||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||
// -------------------- Transaction log --------------------
|
||||
/** 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) --------------------
|
||||
@@ -267,34 +696,43 @@ export class ShareInvestmentComponent {
|
||||
title: 'Share Holdings Register',
|
||||
subtitle: `As on ${this.asOnDate}`,
|
||||
headers: [
|
||||
'Deal No',
|
||||
'Cert No',
|
||||
'Institution',
|
||||
'Institution Type',
|
||||
'Script',
|
||||
'Party',
|
||||
'Units',
|
||||
'Purchase Rate',
|
||||
'Shares',
|
||||
'Face Value',
|
||||
'Book Value',
|
||||
'Dividends Received',
|
||||
'Purchase Date',
|
||||
'Status'
|
||||
],
|
||||
widths: [
|
||||
14,
|
||||
14,
|
||||
28,
|
||||
24,
|
||||
22,
|
||||
12,
|
||||
24,
|
||||
10,
|
||||
14,
|
||||
16,
|
||||
14,
|
||||
12
|
||||
14,
|
||||
14,
|
||||
18
|
||||
],
|
||||
rows: this.holdings.map((h) => [
|
||||
h.certNo,
|
||||
h.scriptName,
|
||||
h.party,
|
||||
h.units,
|
||||
h.purchaseRate,
|
||||
h.faceValue,
|
||||
h.purchaseDate,
|
||||
h.status
|
||||
rows: this.deals.map((d) => [
|
||||
d.dealNo,
|
||||
d.certNo,
|
||||
d.partyName,
|
||||
this.institutionLabel(this.dealParty(d)?.institutionType),
|
||||
`${d.scriptCode} ${d.scriptDesc}`,
|
||||
d.balanceUnits,
|
||||
this.faceHeld(d),
|
||||
this.carryingValue(d),
|
||||
this.dividendsReceived(d),
|
||||
d.settlementDate,
|
||||
d.status
|
||||
])
|
||||
}
|
||||
],
|
||||
@@ -315,6 +753,6 @@ export class ShareInvestmentComponent {
|
||||
if (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