feat: add bond and share investment management modules with report and deal slip generation
This commit is contained in:
@@ -128,6 +128,34 @@
|
|||||||
@if (activeTab === 'holdings') {
|
@if (activeTab === 'holdings') {
|
||||||
<div class="pg-card">
|
<div class="pg-card">
|
||||||
<div class="pos-section-label">{{ instrumentLabel(bondType) }} Holdings Register</div>
|
<div class="pos-section-label">{{ instrumentLabel(bondType) }} Holdings Register</div>
|
||||||
|
@if (settleTarget?.kind === 'purchase') {
|
||||||
|
<div class="pos-section-label">Settle {{ settleTarget?.label }} — counterparty confirmation (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 / contract note ref.</span
|
||||||
|
><input type="text" maxlength="100" [(ngModel)]="settleConfirmation.ref"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Record the counterparty's confirmation or contract note 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>
|
||||||
@@ -173,14 +201,19 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pg-btn pg-btn--primary"
|
class="pg-btn pg-btn--primary"
|
||||||
[disabled]="settlingDealId !== null"
|
[disabled]="settlingDealId !== null || settleTarget !== null"
|
||||||
(click)="settleDeal(h)"
|
(click)="openPurchaseSettle(h)"
|
||||||
>
|
>
|
||||||
{{ settlingDealId === h.id ? 'Settling…' : 'Settle (' + h.settlementDate + ')' }}
|
{{ settlingDealId === h.id ? 'Settling…' : 'Settle (' + h.settlementDate + ')' }}
|
||||||
</button>
|
</button>
|
||||||
} @else if (h.glTransactionId) {
|
} @else if (h.glTransactionId) {
|
||||||
<span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span>
|
<span class="pg-chip" title="Journal entry transaction">GL {{ h.glTransactionId }}</span>
|
||||||
}
|
}
|
||||||
|
@if (h.dealNo) {
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(h.dealNo)">
|
||||||
|
Deal Slip
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
@@ -238,6 +271,15 @@
|
|||||||
><span>* Deal Time (HH:mm:ss)</span
|
><span>* Deal Time (HH:mm:ss)</span
|
||||||
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Confirmation to Counterparty</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>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Settlement Type</span>
|
<span>* Settlement Type</span>
|
||||||
<select [(ngModel)]="purchaseForm.settlementType" (ngModelChange)="onSettlementTypeChange()">
|
<select [(ngModel)]="purchaseForm.settlementType" (ngModelChange)="onSettlementTypeChange()">
|
||||||
@@ -1092,6 +1134,24 @@
|
|||||||
><span>Brokerage</span
|
><span>Brokerage</span
|
||||||
><input type="number" min="0" [(ngModel)]="sale.brokerage" (change)="refreshSaleQuote()"
|
><input type="number" min="0" [(ngModel)]="sale.brokerage" (change)="refreshSaleQuote()"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Broker</span>
|
||||||
|
<select [(ngModel)]="sale.brokerCode">
|
||||||
|
<option value="">-- Direct deal --</option>
|
||||||
|
@for (b of brokerOptions; track b.brokerCode) {
|
||||||
|
<option [value]="b.brokerCode">{{ b.brokerCode }} — {{ b.brokerName }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Confirmation to Counterparty</span>
|
||||||
|
<select [(ngModel)]="sale.confirmationMode">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@for (m of confirmationModes; track m.value) {
|
||||||
|
<option [value]="m.value">{{ m.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inv-form-field">
|
<label class="inv-form-field">
|
||||||
<span>* Amount received into (Bank)</span>
|
<span>* Amount received into (Bank)</span>
|
||||||
<select [(ngModel)]="sale.bankGlCode" (ngModelChange)="refreshSaleQuote()">
|
<select [(ngModel)]="sale.bankGlCode" (ngModelChange)="refreshSaleQuote()">
|
||||||
@@ -1185,10 +1245,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pos-section-label">Pending Sales — settle on or after the settlement date</div>
|
<div class="pos-section-label">Pending Sales — settle on or after the settlement date</div>
|
||||||
|
@if (settleTarget?.kind === 'sale') {
|
||||||
|
<div class="pos-section-label">Settle {{ settleTarget?.label }} — counterparty confirmation (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 / contract note ref.</span
|
||||||
|
><input type="text" maxlength="100" [(ngModel)]="settleConfirmation.ref"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p class="pg-sub" style="padding: 0 16px">
|
||||||
|
Record the counterparty's confirmation or contract note 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">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Deal No</th>
|
<th>Sale No / Deal No</th>
|
||||||
<th>Script</th>
|
<th>Script</th>
|
||||||
<th>Buyer</th>
|
<th>Buyer</th>
|
||||||
<th class="num">Units</th>
|
<th class="num">Units</th>
|
||||||
@@ -1203,7 +1291,10 @@
|
|||||||
@for (s of pendingSalesForType; track s.id) {
|
@for (s of pendingSalesForType; track s.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ s.dealNo }}</span>
|
<span class="pg-chip">{{ s.saleNo || s.dealNo }}</span>
|
||||||
|
@if (s.saleNo) {
|
||||||
|
<br /><small>Deal {{ s.dealNo }}</small>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ s.scriptCode }} — {{ s.scriptDesc }}</td>
|
<td>{{ s.scriptCode }} — {{ s.scriptDesc }}</td>
|
||||||
<td>{{ s.partyName }}</td>
|
<td>{{ s.partyName }}</td>
|
||||||
@@ -1216,8 +1307,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pg-btn pg-btn--primary"
|
class="pg-btn pg-btn--primary"
|
||||||
[disabled]="busySaleId !== null"
|
[disabled]="busySaleId !== null || settleTarget !== null"
|
||||||
(click)="settleSale(s)"
|
(click)="openSaleSettle(s)"
|
||||||
>
|
>
|
||||||
{{ busySaleId === s.id ? 'Settling…' : 'Settle' }}
|
{{ busySaleId === s.id ? 'Settling…' : 'Settle' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -1229,6 +1320,7 @@
|
|||||||
>
|
>
|
||||||
Drop
|
Drop
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(s.saleNo)">Deal Slip</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
todayDDMMYYYY
|
todayDDMMYYYY
|
||||||
} from '../investment-data';
|
} from '../investment-data';
|
||||||
import { exportWorkbook } from '../investment-export';
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import { saveDealSlipPdf } from '../deal-slip-pdf';
|
||||||
import {
|
import {
|
||||||
InvestmentDailyRateDto,
|
InvestmentDailyRateDto,
|
||||||
InvestmentInterestDueDto,
|
InvestmentInterestDueDto,
|
||||||
@@ -69,6 +70,8 @@ import {
|
|||||||
InvestmentAmortisationDto,
|
InvestmentAmortisationDto,
|
||||||
InvestmentAmortisationDueDto,
|
InvestmentAmortisationDueDto,
|
||||||
InvestmentSaleDto,
|
InvestmentSaleDto,
|
||||||
|
CONFIRMATION_MODES,
|
||||||
|
SettleConfirmation,
|
||||||
InvestmentSecurityTransferDto,
|
InvestmentSecurityTransferDto,
|
||||||
InvestmentNonSlrLimitDto,
|
InvestmentNonSlrLimitDto,
|
||||||
InvestmentService,
|
InvestmentService,
|
||||||
@@ -117,6 +120,8 @@ interface BondPurchaseForm {
|
|||||||
premiumAmt: number | null;
|
premiumAmt: number | null;
|
||||||
brokerage: number | null;
|
brokerage: number | null;
|
||||||
brokerName: string;
|
brokerName: string;
|
||||||
|
/** How the deal was confirmed to the counterparty (deal slip). */
|
||||||
|
confirmationMode: string;
|
||||||
tradeNo: string;
|
tradeNo: string;
|
||||||
orderNo: string;
|
orderNo: string;
|
||||||
recommAuthority: string;
|
recommAuthority: string;
|
||||||
@@ -553,6 +558,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
premiumAmt: null,
|
premiumAmt: null,
|
||||||
brokerage: null,
|
brokerage: null,
|
||||||
brokerName: '',
|
brokerName: '',
|
||||||
|
confirmationMode: '',
|
||||||
tradeNo: '',
|
tradeNo: '',
|
||||||
orderNo: '',
|
orderNo: '',
|
||||||
recommAuthority: '',
|
recommAuthority: '',
|
||||||
@@ -999,6 +1005,7 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
crGlAccountId,
|
crGlAccountId,
|
||||||
subType: f.subType || '',
|
subType: f.subType || '',
|
||||||
otherDetails: this.purchaseOtherDetails(),
|
otherDetails: this.purchaseOtherDetails(),
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
dateFormat: 'dd-MM-yyyy',
|
dateFormat: 'dd-MM-yyyy',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
};
|
};
|
||||||
@@ -1107,18 +1114,71 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
|
this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Settle with the counterparty's confirmation (back office) --------------------
|
||||||
|
/** The deal the back office is settling, with the counterparty's confirmation it records (both optional). */
|
||||||
|
settleTarget: { kind: 'purchase' | 'sale'; id: number; label: string } | null = null;
|
||||||
|
settleConfirmation: SettleConfirmation = {};
|
||||||
|
readonly confirmationModes = CONFIRMATION_MODES;
|
||||||
|
|
||||||
|
openPurchaseSettle(h: InvestmentHolding): void {
|
||||||
|
if (!h.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settleTarget = { kind: 'purchase', id: h.id, label: `deal ${h.dealNo}` };
|
||||||
|
this.settleConfirmation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
openSaleSettle(s: InvestmentSaleDto): void {
|
||||||
|
if (!s.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settleTarget = { kind: 'sale', id: s.id, label: `sale ${s.saleNo ?? ''} of deal ${s.dealNo}` };
|
||||||
|
this.settleConfirmation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmSettle(): void {
|
||||||
|
const t = this.settleTarget;
|
||||||
|
if (!t) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t.kind === 'purchase') {
|
||||||
|
const h = this.holdings.find((x) => x.id === t.id);
|
||||||
|
if (h) {
|
||||||
|
this.settleDeal(h, this.settleConfirmation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const s = this.sales.find((x) => x.id === t.id);
|
||||||
|
if (s) {
|
||||||
|
this.settleSale(s, this.settleConfirmation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downloads the deal slip (PDF) of a purchase deal number or a 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))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */
|
/** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */
|
||||||
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
||||||
settlingDealId: number | null = null;
|
settlingDealId: number | null = null;
|
||||||
|
|
||||||
settleDeal(h: InvestmentHolding): void {
|
settleDeal(h: InvestmentHolding, confirmation: SettleConfirmation = {}): void {
|
||||||
if (!h.id || this.settlingDealId !== null) {
|
if (!h.id || this.settlingDealId !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.settlingDealId = h.id;
|
this.settlingDealId = h.id;
|
||||||
this.investmentService.settlePurchase(h.id).subscribe({
|
this.investmentService.settlePurchase(h.id, confirmation).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.settlingDealId = null;
|
this.settlingDealId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
this.toast(
|
this.toast(
|
||||||
awaitingApproval(res)
|
awaitingApproval(res)
|
||||||
? sentForApproval(`Settlement of deal ${h.dealNo}`)
|
? sentForApproval(`Settlement of deal ${h.dealNo}`)
|
||||||
@@ -1368,7 +1428,9 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
brokerage: null as number | null,
|
brokerage: null as number | null,
|
||||||
bankGlCode: '',
|
bankGlCode: '',
|
||||||
sanctionAuthority: '',
|
sanctionAuthority: '',
|
||||||
remarks: ''
|
remarks: '',
|
||||||
|
brokerCode: '',
|
||||||
|
confirmationMode: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1460,6 +1522,8 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
bankGlAccountId,
|
bankGlAccountId,
|
||||||
...(f.sanctionAuthority.trim() ? { sanctionAuthority: f.sanctionAuthority.trim() } : {}),
|
...(f.sanctionAuthority.trim() ? { sanctionAuthority: f.sanctionAuthority.trim() } : {}),
|
||||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||||
|
...(f.brokerCode ? { brokerCode: f.brokerCode } : {}),
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
dateFormat: 'dd-MM-yyyy',
|
dateFormat: 'dd-MM-yyyy',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
};
|
};
|
||||||
@@ -1486,14 +1550,15 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
/** The pending sale being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
/** The pending sale being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
||||||
busySaleId: number | null = null;
|
busySaleId: number | null = null;
|
||||||
|
|
||||||
settleSale(s: InvestmentSaleDto): void {
|
settleSale(s: InvestmentSaleDto, confirmation: SettleConfirmation = {}): void {
|
||||||
if (!s.id || this.busySaleId !== null) {
|
if (!s.id || this.busySaleId !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.busySaleId = s.id;
|
this.busySaleId = s.id;
|
||||||
this.investmentService.settleSale(s.id).subscribe({
|
this.investmentService.settleSale(s.id, confirmation).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.busySaleId = null;
|
this.busySaleId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
if (awaitingApproval(res)) {
|
if (awaitingApproval(res)) {
|
||||||
this.toast(sentForApproval(`Settlement of the sale of deal ${s.dealNo}`));
|
this.toast(sentForApproval(`Settlement of the sale of deal ${s.dealNo}`));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
}
|
||||||
@@ -264,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">
|
||||||
@@ -276,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">
|
||||||
@@ -300,9 +297,7 @@
|
|||||||
? bondTxns
|
? bondTxns
|
||||||
: activeReport === 'shareLedger'
|
: activeReport === 'shareLedger'
|
||||||
? shareTxns
|
? shareTxns
|
||||||
: activeReport === 'amortizeLedger'
|
: amortizeTxns;
|
||||||
? amortizeTxns
|
|
||||||
: dealSlips;
|
|
||||||
track $index
|
track $index
|
||||||
) {
|
) {
|
||||||
<tr>
|
<tr>
|
||||||
@@ -324,6 +319,122 @@
|
|||||||
</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">
|
<div class="pos-section-label">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { Component, OnInit, inject } 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 { RouterLink } from '@angular/router';
|
||||||
import { forkJoin, of } from 'rxjs';
|
import { forkJoin, of } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
@@ -21,8 +22,11 @@ import {
|
|||||||
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 {
|
import {
|
||||||
InvestmentDailyRateDto,
|
InvestmentDailyRateDto,
|
||||||
|
InvestmentDealSlipDto,
|
||||||
InvestmentFdDto,
|
InvestmentFdDto,
|
||||||
InvestmentFdScheduleDto,
|
InvestmentFdScheduleDto,
|
||||||
InvestmentInterestReportRowDto,
|
InvestmentInterestReportRowDto,
|
||||||
@@ -30,6 +34,7 @@ import {
|
|||||||
InvestmentService,
|
InvestmentService,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
bookValueSoldByPurchase,
|
bookValueSoldByPurchase,
|
||||||
|
confirmationModeLabel,
|
||||||
toFdHolding,
|
toFdHolding,
|
||||||
toFdTxns,
|
toFdTxns,
|
||||||
toHolding,
|
toHolding,
|
||||||
@@ -57,7 +62,9 @@ type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterLink
|
FormsModule,
|
||||||
|
RouterLink,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InvestmentReportsComponent implements OnInit {
|
export class InvestmentReportsComponent implements OnInit {
|
||||||
@@ -80,8 +87,17 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
/** Interest received in the financial year to the as-on date, per deal / FD (Interest Received & Receivable report). */
|
/** Interest received in the financial year to the as-on date, per deal / FD (Interest Received & Receivable report). */
|
||||||
receivedRows: InvestmentInterestReportRowDto[] = [];
|
receivedRows: InvestmentInterestReportRowDto[] = [];
|
||||||
receivedFrom = '';
|
receivedFrom = '';
|
||||||
/** Every purchase deal, pending ones included — the Deal Slip register (the ledgers hold only what reached the GL). */
|
/**
|
||||||
dealSlipTxns: InvestmentTxn[] = [];
|
* 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). */
|
/** GOI category transfers (security shifted to another script / category). */
|
||||||
transfers: InvestmentSecurityTransferDto[] = [];
|
transfers: InvestmentSecurityTransferDto[] = [];
|
||||||
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
/** FD interest lines need one call per FD, so they load when a report needs them. */
|
||||||
@@ -128,7 +144,6 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
...r.transfers.map(toTransferTxn),
|
...r.transfers.map(toTransferTxn),
|
||||||
...r.fds.flatMap((f) => toFdTxns(f, []))
|
...r.fds.flatMap((f) => toFdTxns(f, []))
|
||||||
]);
|
]);
|
||||||
this.dealSlipTxns = this.byDateDesc(r.purchases.map(toPurchaseTxn));
|
|
||||||
// Received = coupons, FD payouts and FD interest paid at closure, as the interest report counts them.
|
// 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.receivedRows = r.interest.rows.filter((x) => x.received > 0);
|
||||||
this.receivedFrom = r.interest.fromDate;
|
this.receivedFrom = r.interest.fromDate;
|
||||||
@@ -211,6 +226,80 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
if (report === 'fdLedger') {
|
if (report === 'fdLedger') {
|
||||||
this.loadFdInterest();
|
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 {
|
||||||
@@ -298,9 +387,6 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
get amortizeTxns(): InvestmentTxn[] {
|
get amortizeTxns(): InvestmentTxn[] {
|
||||||
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
||||||
}
|
}
|
||||||
get dealSlips(): InvestmentTxn[] {
|
|
||||||
return this.dealSlipTxns;
|
|
||||||
}
|
|
||||||
get slrBondRegister(): InvestmentHolding[] {
|
get slrBondRegister(): InvestmentHolding[] {
|
||||||
return this.active('GOVT_SECURITY');
|
return this.active('GOVT_SECURITY');
|
||||||
}
|
}
|
||||||
@@ -542,13 +628,90 @@ export class InvestmentReportsComponent implements OnInit {
|
|||||||
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',
|
||||||
|
|||||||
@@ -434,6 +434,11 @@ export interface InvestmentPurchaseDto {
|
|||||||
crGlCode: string;
|
crGlCode: string;
|
||||||
crGlName: string;
|
crGlName: string;
|
||||||
otherDetails?: string;
|
otherDetails?: string;
|
||||||
|
/** How the deal was confirmed to the counterparty (deal slip). */
|
||||||
|
confirmationMode?: ConfirmationMode;
|
||||||
|
/** When the counterparty's confirmation reached the back office, recorded on Settle. */
|
||||||
|
confirmationReceivedOn?: string;
|
||||||
|
confirmationRef?: string;
|
||||||
status: string;
|
status: string;
|
||||||
glTransactionId?: string;
|
glTransactionId?: string;
|
||||||
settledOnDate?: string;
|
settledOnDate?: string;
|
||||||
@@ -517,9 +522,15 @@ export class InvestmentService {
|
|||||||
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/sales', body);
|
return this.http.post<{ resourceId?: number; resourceIdentifier?: string }>('/investment/sales', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Posts a pending sale on or after its settlement date; the voucher number comes back as resourceIdentifier. */
|
/**
|
||||||
settleSale(saleId: number): Observable<{ resourceIdentifier?: string }> {
|
* Posts a pending sale on or after its settlement date; the voucher number comes back as resourceIdentifier. The
|
||||||
return this.http.post<{ resourceIdentifier?: string }>(`/investment/sales/${saleId}/settle`, {});
|
* back office may record the counterparty's confirmation with it.
|
||||||
|
*/
|
||||||
|
settleSale(saleId: number, confirmation: SettleConfirmation = {}): Observable<{ resourceIdentifier?: string }> {
|
||||||
|
return this.http.post<{ resourceIdentifier?: string }>(
|
||||||
|
`/investment/sales/${saleId}/settle`,
|
||||||
|
settleConfirmationBody(confirmation)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSale(saleId: number): Observable<unknown> {
|
deleteSale(saleId: number): Observable<unknown> {
|
||||||
@@ -636,14 +647,30 @@ export class InvestmentService {
|
|||||||
return this.http.delete(`/investment/daily-rates/${rateId}`);
|
return this.http.delete(`/investment/daily-rates/${rateId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settles a deal: posts it to GL dated its settlement date. Under maker-checker the response only says it awaits approval. */
|
/**
|
||||||
settlePurchase(purchaseId: number): Observable<{ transactionId?: string; rollbackTransaction?: boolean }> {
|
* Settles a deal: posts it to GL dated its settlement date. Under maker-checker the response only says it awaits
|
||||||
|
* approval. The back office may record the counterparty's confirmation with it.
|
||||||
|
*/
|
||||||
|
settlePurchase(
|
||||||
|
purchaseId: number,
|
||||||
|
confirmation: SettleConfirmation = {}
|
||||||
|
): Observable<{ transactionId?: string; rollbackTransaction?: boolean }> {
|
||||||
return this.http.post<{ transactionId?: string; rollbackTransaction?: boolean }>(
|
return this.http.post<{ transactionId?: string; rollbackTransaction?: boolean }>(
|
||||||
`/investment/purchases/${purchaseId}/settle`,
|
`/investment/purchases/${purchaseId}/settle`,
|
||||||
{}
|
settleConfirmationBody(confirmation)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deal slip register: deals dated fromDate to toDate (DD-MM-YYYY), cancelled ones included, in slip number order. */
|
||||||
|
getDealSlips(fromDate: string, toDate: string): Observable<InvestmentDealSlipDto[]> {
|
||||||
|
return this.http.get<InvestmentDealSlipDto[]>('/investment/deal-slips', { params: { fromDate, toDate } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One deal slip by its number: a purchase deal number or a sale number. */
|
||||||
|
getDealSlip(slipNo: string): Observable<InvestmentDealSlipDto[]> {
|
||||||
|
return this.http.get<InvestmentDealSlipDto[]>('/investment/deal-slips', { params: { slipNo } });
|
||||||
|
}
|
||||||
|
|
||||||
/** RBI non-SLR limits for a purchase of `amount` (book value) in a script on a deal date (DD-MM-YYYY). */
|
/** RBI non-SLR limits for a purchase of `amount` (book value) in a script on a deal date (DD-MM-YYYY). */
|
||||||
getNonSlrLimit(scriptId: number | null, dealDate: string, amount: number): Observable<InvestmentNonSlrLimitDto> {
|
getNonSlrLimit(scriptId: number | null, dealDate: string, amount: number): Observable<InvestmentNonSlrLimitDto> {
|
||||||
const params: Record<string, string> = { dealDate, amount: String(amount || 0) };
|
const params: Record<string, string> = { dealDate, amount: String(amount || 0) };
|
||||||
@@ -1182,6 +1209,8 @@ export interface InvestmentSaleDto {
|
|||||||
id?: number;
|
id?: number;
|
||||||
purchaseId: number;
|
purchaseId: number;
|
||||||
dealNo: string;
|
dealNo: string;
|
||||||
|
/** Deal slip number of the sale itself, e.g. 202627S00001 (none for a write-off or amortisation). */
|
||||||
|
saleNo?: string;
|
||||||
certNo: string;
|
certNo: string;
|
||||||
bondType: string;
|
bondType: string;
|
||||||
scriptCode: string;
|
scriptCode: string;
|
||||||
@@ -1215,12 +1244,104 @@ export interface InvestmentSaleDto {
|
|||||||
investmentGlCode: string;
|
investmentGlCode: string;
|
||||||
sanctionAuthority?: string;
|
sanctionAuthority?: string;
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
|
brokerCode?: string;
|
||||||
|
confirmationMode?: ConfirmationMode;
|
||||||
|
confirmationReceivedOn?: string;
|
||||||
|
confirmationRef?: string;
|
||||||
status: 'PENDING_SETTLEMENT' | 'SETTLED';
|
status: 'PENDING_SETTLEMENT' | 'SETTLED';
|
||||||
glTransactionId?: string;
|
glTransactionId?: string;
|
||||||
voucherNo?: string;
|
voucherNo?: string;
|
||||||
settledOnDate?: string;
|
settledOnDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How a deal was confirmed to the counterparty. */
|
||||||
|
export type ConfirmationMode = 'EMAIL' | 'NDS_OM' | 'FAX' | 'LETTER' | 'PHONE';
|
||||||
|
|
||||||
|
export const CONFIRMATION_MODES: Array<{ value: ConfirmationMode; label: string }> = [
|
||||||
|
{ value: 'NDS_OM', label: 'NDS-OM / CCIL' },
|
||||||
|
{ value: 'EMAIL', label: 'Email' },
|
||||||
|
{ value: 'FAX', label: 'Fax' },
|
||||||
|
{ value: 'LETTER', label: 'Letter' },
|
||||||
|
{ value: 'PHONE', label: 'Phone (recorded line)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function confirmationModeLabel(mode?: string | null): string {
|
||||||
|
return CONFIRMATION_MODES.find((m) => m.value === mode)?.label ?? mode ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The counterparty's confirmation the back office records when it settles a deal (both optional). */
|
||||||
|
export interface SettleConfirmation {
|
||||||
|
/** DD-MM-YYYY. */
|
||||||
|
receivedOn?: string;
|
||||||
|
ref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function settleConfirmationBody(c: SettleConfirmation): Record<string, string> {
|
||||||
|
const body: Record<string, string> = {};
|
||||||
|
if (c.receivedOn) {
|
||||||
|
body['confirmationReceivedOn'] = c.receivedOn;
|
||||||
|
body['dateFormat'] = 'dd-MM-yyyy';
|
||||||
|
body['locale'] = 'en';
|
||||||
|
}
|
||||||
|
if (c.ref?.trim()) {
|
||||||
|
body['confirmationRef'] = c.ref.trim();
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One deal slip: a purchase (BUY) or a sale / refund / redemption / switch, with who handled it. */
|
||||||
|
export interface InvestmentDealSlipDto {
|
||||||
|
slipNo: string;
|
||||||
|
side: 'BUY' | 'SELL' | 'REFUND' | 'REDEEM' | 'SWITCH';
|
||||||
|
status: 'PENDING_SETTLEMENT' | 'SETTLED' | 'CANCELLED';
|
||||||
|
recordStatus: string;
|
||||||
|
purchaseId: number;
|
||||||
|
saleId?: number;
|
||||||
|
dealNo: string;
|
||||||
|
certNo?: string;
|
||||||
|
dealDate: string;
|
||||||
|
dealTime?: string;
|
||||||
|
settlementType?: string;
|
||||||
|
settlementDate: string;
|
||||||
|
partyCode?: string;
|
||||||
|
counterparty?: string;
|
||||||
|
bondType: string;
|
||||||
|
scriptCode: string;
|
||||||
|
scriptDesc: string;
|
||||||
|
isin?: string;
|
||||||
|
category?: string;
|
||||||
|
units: number;
|
||||||
|
faceValue: number;
|
||||||
|
price?: number;
|
||||||
|
consideration: number;
|
||||||
|
accruedInterest?: number;
|
||||||
|
brokerage?: number;
|
||||||
|
otherCharges?: number;
|
||||||
|
netAmount: number;
|
||||||
|
bankGlCode?: string;
|
||||||
|
bankGlName?: string;
|
||||||
|
dealMode: 'DIRECT' | 'BROKER';
|
||||||
|
brokerCode?: string;
|
||||||
|
tradeNo?: string;
|
||||||
|
orderNo?: string;
|
||||||
|
confirmationMode?: ConfirmationMode;
|
||||||
|
confirmationReceivedOn?: string;
|
||||||
|
confirmationRef?: string;
|
||||||
|
enteredBy?: string;
|
||||||
|
enteredOn?: string;
|
||||||
|
approvedBy?: string;
|
||||||
|
approvedOn?: string;
|
||||||
|
settledBy?: string;
|
||||||
|
settledOn?: string;
|
||||||
|
settleApprovedBy?: string;
|
||||||
|
settleApprovedOn?: string;
|
||||||
|
cancelledBy?: string;
|
||||||
|
cancelledOn?: string;
|
||||||
|
voucherNo?: string;
|
||||||
|
glTransactionId?: string;
|
||||||
|
remarks?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** A market valuation (MTM) of AFS or HFT holdings: a preview (no id) or a posted run. */
|
/** A market valuation (MTM) of AFS or HFT holdings: a preview (no id) or a posted run. */
|
||||||
export interface InvestmentValuationDto {
|
export interface InvestmentValuationDto {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|||||||
@@ -72,6 +72,36 @@
|
|||||||
@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>
|
||||||
@@ -113,8 +143,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pg-btn pg-btn--primary"
|
class="pg-btn pg-btn--primary"
|
||||||
[disabled]="settlingDealId !== null"
|
[disabled]="settlingDealId !== null || settleTarget !== null"
|
||||||
(click)="settleDeal(d)"
|
(click)="openPurchaseSettle(d)"
|
||||||
>
|
>
|
||||||
Settle
|
Settle
|
||||||
</button>
|
</button>
|
||||||
@@ -127,6 +157,7 @@
|
|||||||
Drop
|
Drop
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(d.dealNo)">Deal Slip</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
@@ -219,6 +250,15 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Category</span><input type="text" value="AFS (Available For Sale)" readonly
|
><span>Category</span><input type="text" value="AFS (Available For Sale)" readonly
|
||||||
/></label>
|
/></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) {
|
@if (shareLimit; as l) {
|
||||||
@@ -415,6 +455,15 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="refund.remarks"
|
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="refund.remarks"
|
||||||
/></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) {
|
@if (refundQuote; as q) {
|
||||||
<table class="pg-table">
|
<table class="pg-table">
|
||||||
@@ -448,10 +497,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pos-section-label">Pending Refunds (posted on Settle)</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">
|
<table class="pg-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Deal No</th>
|
<th>Sale No / Deal No</th>
|
||||||
<th>Institution</th>
|
<th>Institution</th>
|
||||||
<th class="num">Shares</th>
|
<th class="num">Shares</th>
|
||||||
<th class="num">Amount</th>
|
<th class="num">Amount</th>
|
||||||
@@ -464,7 +543,10 @@
|
|||||||
@for (s of pendingRefunds; track s.id) {
|
@for (s of pendingRefunds; track s.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="pg-chip">{{ s.dealNo }}</span>
|
<span class="pg-chip">{{ s.saleNo || s.dealNo }}</span>
|
||||||
|
@if (s.saleNo) {
|
||||||
|
<br /><small>Deal {{ s.dealNo }}</small>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ s.partyName }}</td>
|
<td>{{ s.partyName }}</td>
|
||||||
<td class="num">{{ s.units | number: '1.0-0' }}</td>
|
<td class="num">{{ s.units | number: '1.0-0' }}</td>
|
||||||
@@ -479,8 +561,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pg-btn pg-btn--primary"
|
class="pg-btn pg-btn--primary"
|
||||||
[disabled]="busySaleId !== null"
|
[disabled]="busySaleId !== null || settleTarget !== null"
|
||||||
(click)="settleRefund(s)"
|
(click)="openRefundSettle(s)"
|
||||||
>
|
>
|
||||||
Settle
|
Settle
|
||||||
</button>
|
</button>
|
||||||
@@ -492,6 +574,7 @@
|
|||||||
>
|
>
|
||||||
Drop
|
Drop
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(s.saleNo)">Deal Slip</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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 { InvestmentTxn, SETTLEMENT_BANK_GL_OPTIONS, 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 {
|
import {
|
||||||
InvestmentDividendDto,
|
InvestmentDividendDto,
|
||||||
InvestmentPartyDto,
|
InvestmentPartyDto,
|
||||||
@@ -22,6 +23,8 @@ import {
|
|||||||
InvestmentService,
|
InvestmentService,
|
||||||
InvestmentShareLimitDto,
|
InvestmentShareLimitDto,
|
||||||
SHARE_INSTITUTION_TYPES,
|
SHARE_INSTITUTION_TYPES,
|
||||||
|
CONFIRMATION_MODES,
|
||||||
|
SettleConfirmation,
|
||||||
apiErrorMessage,
|
apiErrorMessage,
|
||||||
awaitingApproval,
|
awaitingApproval,
|
||||||
sentForApproval,
|
sentForApproval,
|
||||||
@@ -42,6 +45,8 @@ interface SharePurchaseForm {
|
|||||||
shares: number | null;
|
shares: number | null;
|
||||||
pricePerShare: number | null;
|
pricePerShare: number | null;
|
||||||
bankGlCode: string;
|
bankGlCode: string;
|
||||||
|
/** How the deal was confirmed to the institution (deal slip). */
|
||||||
|
confirmationMode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareRefundForm {
|
interface ShareRefundForm {
|
||||||
@@ -51,6 +56,7 @@ interface ShareRefundForm {
|
|||||||
pricePerShare: number | null;
|
pricePerShare: number | null;
|
||||||
bankGlCode: string;
|
bankGlCode: string;
|
||||||
remarks: string;
|
remarks: string;
|
||||||
|
confirmationMode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareDividendForm {
|
interface ShareDividendForm {
|
||||||
@@ -218,18 +224,68 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
return `inv-badge inv-badge--${status.toLowerCase()}`;
|
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. */
|
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
||||||
settlingDealId: number | null = null;
|
settlingDealId: number | null = null;
|
||||||
|
|
||||||
/** Posts a pending deal to GL dated its settlement date: Dr 100706 Investment in Shares / Cr bank. */
|
/** Posts a pending deal to GL dated its settlement date: Dr 100706 Investment in Shares / Cr bank. */
|
||||||
settleDeal(d: InvestmentPurchaseDto): void {
|
settleDeal(d: InvestmentPurchaseDto, confirmation: SettleConfirmation = {}): void {
|
||||||
if (this.settlingDealId !== null) {
|
if (this.settlingDealId !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.settlingDealId = d.id;
|
this.settlingDealId = d.id;
|
||||||
this.investmentService.settlePurchase(d.id).subscribe({
|
this.investmentService.settlePurchase(d.id, confirmation).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.settlingDealId = null;
|
this.settlingDealId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
this.toast(
|
this.toast(
|
||||||
awaitingApproval(res)
|
awaitingApproval(res)
|
||||||
? sentForApproval(`Settlement of deal ${d.dealNo}`)
|
? sentForApproval(`Settlement of deal ${d.dealNo}`)
|
||||||
@@ -271,7 +327,8 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
settlementType: 'T+0',
|
settlementType: 'T+0',
|
||||||
shares: null,
|
shares: null,
|
||||||
pricePerShare: null,
|
pricePerShare: null,
|
||||||
bankGlCode: SETTLEMENT_BANK_GL_OPTIONS[0].glCode
|
bankGlCode: SETTLEMENT_BANK_GL_OPTIONS[0].glCode,
|
||||||
|
confirmationMode: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +443,7 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
category: SHARE_CATEGORY,
|
category: SHARE_CATEGORY,
|
||||||
crGlAccountId,
|
crGlAccountId,
|
||||||
subType: '',
|
subType: '',
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
dateFormat: 'dd-MM-yyyy',
|
dateFormat: 'dd-MM-yyyy',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
};
|
};
|
||||||
@@ -474,7 +532,8 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
units: null,
|
units: null,
|
||||||
pricePerShare: null,
|
pricePerShare: null,
|
||||||
bankGlCode: '',
|
bankGlCode: '',
|
||||||
remarks: ''
|
remarks: '',
|
||||||
|
confirmationMode: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +605,7 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
saleRate: f.pricePerShare,
|
saleRate: f.pricePerShare,
|
||||||
bankGlAccountId,
|
bankGlAccountId,
|
||||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||||
|
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||||
dateFormat: 'dd-MM-yyyy',
|
dateFormat: 'dd-MM-yyyy',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
};
|
};
|
||||||
@@ -570,14 +630,15 @@ export class ShareInvestmentComponent implements OnInit {
|
|||||||
/** The pending refund being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
/** The pending refund being settled or dropped; its buttons stay disabled so it cannot be sent twice. */
|
||||||
busySaleId: number | null = null;
|
busySaleId: number | null = null;
|
||||||
|
|
||||||
settleRefund(s: InvestmentSaleDto): void {
|
settleRefund(s: InvestmentSaleDto, confirmation: SettleConfirmation = {}): void {
|
||||||
if (!s.id || this.busySaleId !== null) {
|
if (!s.id || this.busySaleId !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.busySaleId = s.id;
|
this.busySaleId = s.id;
|
||||||
this.investmentService.settleSale(s.id).subscribe({
|
this.investmentService.settleSale(s.id, confirmation).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.busySaleId = null;
|
this.busySaleId = null;
|
||||||
|
this.settleTarget = null;
|
||||||
this.toast(
|
this.toast(
|
||||||
awaitingApproval(res)
|
awaitingApproval(res)
|
||||||
? sentForApproval(`Settlement of the refund on deal ${s.dealNo}`)
|
? sentForApproval(`Settlement of the refund on deal ${s.dealNo}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user