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') {
|
||||
<div class="pg-card">
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -173,14 +201,19 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="settlingDealId !== null"
|
||||
(click)="settleDeal(h)"
|
||||
[disabled]="settlingDealId !== null || settleTarget !== null"
|
||||
(click)="openPurchaseSettle(h)"
|
||||
>
|
||||
{{ settlingDealId === h.id ? 'Settling…' : 'Settle (' + h.settlementDate + ')' }}
|
||||
</button>
|
||||
} @else if (h.glTransactionId) {
|
||||
<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>
|
||||
</tr>
|
||||
} @empty {
|
||||
@@ -238,6 +271,15 @@
|
||||
><span>* Deal Time (HH:mm:ss)</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.dealTime" placeholder="HH:mm:ss"
|
||||
/></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">
|
||||
<span>* Settlement Type</span>
|
||||
<select [(ngModel)]="purchaseForm.settlementType" (ngModelChange)="onSettlementTypeChange()">
|
||||
@@ -1092,6 +1134,24 @@
|
||||
><span>Brokerage</span
|
||||
><input type="number" min="0" [(ngModel)]="sale.brokerage" (change)="refreshSaleQuote()"
|
||||
/></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">
|
||||
<span>* Amount received into (Bank)</span>
|
||||
<select [(ngModel)]="sale.bankGlCode" (ngModelChange)="refreshSaleQuote()">
|
||||
@@ -1185,10 +1245,38 @@
|
||||
</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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Deal No</th>
|
||||
<th>Sale No / Deal No</th>
|
||||
<th>Script</th>
|
||||
<th>Buyer</th>
|
||||
<th class="num">Units</th>
|
||||
@@ -1203,7 +1291,10 @@
|
||||
@for (s of pendingSalesForType; track s.id) {
|
||||
<tr>
|
||||
<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>{{ s.scriptCode }} — {{ s.scriptDesc }}</td>
|
||||
<td>{{ s.partyName }}</td>
|
||||
@@ -1216,8 +1307,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="busySaleId !== null"
|
||||
(click)="settleSale(s)"
|
||||
[disabled]="busySaleId !== null || settleTarget !== null"
|
||||
(click)="openSaleSettle(s)"
|
||||
>
|
||||
{{ busySaleId === s.id ? 'Settling…' : 'Settle' }}
|
||||
</button>
|
||||
@@ -1229,6 +1320,7 @@
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(s.saleNo)">Deal Slip</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
todayDDMMYYYY
|
||||
} from '../investment-data';
|
||||
import { exportWorkbook } from '../investment-export';
|
||||
import { saveDealSlipPdf } from '../deal-slip-pdf';
|
||||
import {
|
||||
InvestmentDailyRateDto,
|
||||
InvestmentInterestDueDto,
|
||||
@@ -69,6 +70,8 @@ import {
|
||||
InvestmentAmortisationDto,
|
||||
InvestmentAmortisationDueDto,
|
||||
InvestmentSaleDto,
|
||||
CONFIRMATION_MODES,
|
||||
SettleConfirmation,
|
||||
InvestmentSecurityTransferDto,
|
||||
InvestmentNonSlrLimitDto,
|
||||
InvestmentService,
|
||||
@@ -117,6 +120,8 @@ interface BondPurchaseForm {
|
||||
premiumAmt: number | null;
|
||||
brokerage: number | null;
|
||||
brokerName: string;
|
||||
/** How the deal was confirmed to the counterparty (deal slip). */
|
||||
confirmationMode: string;
|
||||
tradeNo: string;
|
||||
orderNo: string;
|
||||
recommAuthority: string;
|
||||
@@ -553,6 +558,7 @@ export class BondInvestmentComponent implements OnInit {
|
||||
premiumAmt: null,
|
||||
brokerage: null,
|
||||
brokerName: '',
|
||||
confirmationMode: '',
|
||||
tradeNo: '',
|
||||
orderNo: '',
|
||||
recommAuthority: '',
|
||||
@@ -999,6 +1005,7 @@ export class BondInvestmentComponent implements OnInit {
|
||||
crGlAccountId,
|
||||
subType: f.subType || '',
|
||||
otherDetails: this.purchaseOtherDetails(),
|
||||
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
};
|
||||
@@ -1107,18 +1114,71 @@ export class BondInvestmentComponent implements OnInit {
|
||||
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. */
|
||||
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
||||
settlingDealId: number | null = null;
|
||||
|
||||
settleDeal(h: InvestmentHolding): void {
|
||||
settleDeal(h: InvestmentHolding, confirmation: SettleConfirmation = {}): void {
|
||||
if (!h.id || this.settlingDealId !== null) {
|
||||
return;
|
||||
}
|
||||
this.settlingDealId = h.id;
|
||||
this.investmentService.settlePurchase(h.id).subscribe({
|
||||
this.investmentService.settlePurchase(h.id, confirmation).subscribe({
|
||||
next: (res) => {
|
||||
this.settlingDealId = null;
|
||||
this.settleTarget = null;
|
||||
this.toast(
|
||||
awaitingApproval(res)
|
||||
? sentForApproval(`Settlement of deal ${h.dealNo}`)
|
||||
@@ -1368,7 +1428,9 @@ export class BondInvestmentComponent implements OnInit {
|
||||
brokerage: null as number | null,
|
||||
bankGlCode: '',
|
||||
sanctionAuthority: '',
|
||||
remarks: ''
|
||||
remarks: '',
|
||||
brokerCode: '',
|
||||
confirmationMode: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1460,6 +1522,8 @@ export class BondInvestmentComponent implements OnInit {
|
||||
bankGlAccountId,
|
||||
...(f.sanctionAuthority.trim() ? { sanctionAuthority: f.sanctionAuthority.trim() } : {}),
|
||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||
...(f.brokerCode ? { brokerCode: f.brokerCode } : {}),
|
||||
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
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. */
|
||||
busySaleId: number | null = null;
|
||||
|
||||
settleSale(s: InvestmentSaleDto): void {
|
||||
settleSale(s: InvestmentSaleDto, confirmation: SettleConfirmation = {}): void {
|
||||
if (!s.id || this.busySaleId !== null) {
|
||||
return;
|
||||
}
|
||||
this.busySaleId = s.id;
|
||||
this.investmentService.settleSale(s.id).subscribe({
|
||||
this.investmentService.settleSale(s.id, confirmation).subscribe({
|
||||
next: (res) => {
|
||||
this.busySaleId = null;
|
||||
this.settleTarget = null;
|
||||
if (awaitingApproval(res)) {
|
||||
this.toast(sentForApproval(`Settlement of the sale of deal ${s.dealNo}`));
|
||||
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 === 'bondLedger' ||
|
||||
activeReport === 'shareLedger' ||
|
||||
activeReport === 'amortizeLedger' ||
|
||||
activeReport === 'dealSlip'
|
||||
activeReport === 'amortizeLedger'
|
||||
) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
@@ -276,9 +275,7 @@
|
||||
? 'Govt/Bond Ledger'
|
||||
: activeReport === 'shareLedger'
|
||||
? 'Share Ledger'
|
||||
: activeReport === 'amortizeLedger'
|
||||
? 'Amortize Ledger'
|
||||
: 'Deal Slip Register'
|
||||
: 'Amortize Ledger'
|
||||
}}
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
@@ -300,9 +297,7 @@
|
||||
? bondTxns
|
||||
: activeReport === 'shareLedger'
|
||||
? shareTxns
|
||||
: activeReport === 'amortizeLedger'
|
||||
? amortizeTxns
|
||||
: dealSlips;
|
||||
: amortizeTxns;
|
||||
track $index
|
||||
) {
|
||||
<tr>
|
||||
@@ -324,6 +319,122 @@
|
||||
</table>
|
||||
</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') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
@@ -21,8 +22,11 @@ import {
|
||||
todayDDMMYYYY
|
||||
} from '../investment-data';
|
||||
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
||||
import { brokerNameOf, saveDealSlipPdf } from '../deal-slip-pdf';
|
||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||
import {
|
||||
InvestmentDailyRateDto,
|
||||
InvestmentDealSlipDto,
|
||||
InvestmentFdDto,
|
||||
InvestmentFdScheduleDto,
|
||||
InvestmentInterestReportRowDto,
|
||||
@@ -30,6 +34,7 @@ import {
|
||||
InvestmentService,
|
||||
apiErrorMessage,
|
||||
bookValueSoldByPurchase,
|
||||
confirmationModeLabel,
|
||||
toFdHolding,
|
||||
toFdTxns,
|
||||
toHolding,
|
||||
@@ -57,7 +62,9 @@ type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
DateDdmmyyyyPickerComponent
|
||||
]
|
||||
})
|
||||
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). */
|
||||
receivedRows: InvestmentInterestReportRowDto[] = [];
|
||||
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). */
|
||||
transfers: InvestmentSecurityTransferDto[] = [];
|
||||
/** 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.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.
|
||||
this.receivedRows = r.interest.rows.filter((x) => x.received > 0);
|
||||
this.receivedFrom = r.interest.fromDate;
|
||||
@@ -211,6 +226,80 @@ export class InvestmentReportsComponent implements OnInit {
|
||||
if (report === 'fdLedger') {
|
||||
this.loadFdInterest();
|
||||
}
|
||||
if (report === 'dealSlip' && !this.slips.length && !this.slipLoading) {
|
||||
this.loadDealSlips();
|
||||
}
|
||||
}
|
||||
|
||||
/** Deal Slip Register for the period (default: the financial year to date). */
|
||||
loadDealSlips(): void {
|
||||
if (!this.slipFrom) {
|
||||
const d = parseDDMMYYYY(this.slipTo);
|
||||
this.slipFrom = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
|
||||
}
|
||||
this.slipLoading = true;
|
||||
this.slipError = '';
|
||||
this.investmentService.getDealSlips(this.slipFrom, this.slipTo).subscribe({
|
||||
next: (rows) => {
|
||||
this.slips = rows;
|
||||
this.slipGaps = this.findSlipGaps(rows);
|
||||
this.slipLoading = false;
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
this.slips = [];
|
||||
this.slipGaps = [];
|
||||
this.slipLoading = false;
|
||||
this.slipError = apiErrorMessage(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing numbers in each series (purchases 202627000001…, sales 202627S00001…) between the first and the last
|
||||
* slip shown: every slip must be accounted for, cancelled ones included.
|
||||
*/
|
||||
private findSlipGaps(rows: InvestmentDealSlipDto[]): string[] {
|
||||
const series = new Map<string, { width: number; nos: number[] }>();
|
||||
for (const r of rows) {
|
||||
const m = /^(\d{6}S?)(\d+)$/.exec(r.slipNo);
|
||||
if (!m) {
|
||||
continue;
|
||||
}
|
||||
const s = series.get(m[1]) ?? { width: m[2].length, nos: [] };
|
||||
s.nos.push(Number(m[2]));
|
||||
series.set(m[1], s);
|
||||
}
|
||||
const gaps: string[] = [];
|
||||
series.forEach((s, prefix) => {
|
||||
const nos = new Set(s.nos);
|
||||
for (let n = Math.min(...s.nos) + 1; n < Math.max(...s.nos); n++) {
|
||||
if (!nos.has(n)) {
|
||||
gaps.push(prefix + String(n).padStart(s.width, '0'));
|
||||
}
|
||||
}
|
||||
});
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/** A deal settled without the counterparty's confirmation recorded (cancelled deals need none). */
|
||||
confirmationAwaited(s: InvestmentDealSlipDto): boolean {
|
||||
return s.status !== 'CANCELLED' && !s.confirmationReceivedOn && s.side !== 'REDEEM';
|
||||
}
|
||||
|
||||
get slipsAwaited(): number {
|
||||
return this.slips.filter((s) => this.confirmationAwaited(s)).length;
|
||||
}
|
||||
|
||||
printSlip(s: InvestmentDealSlipDto): void {
|
||||
saveDealSlipPdf(s);
|
||||
}
|
||||
|
||||
confirmationLabel(mode?: string): string {
|
||||
return confirmationModeLabel(mode);
|
||||
}
|
||||
|
||||
brokerName(code?: string): string {
|
||||
return brokerNameOf(code) ?? '';
|
||||
}
|
||||
|
||||
instrumentLabel(type: InstrumentType): string {
|
||||
@@ -298,9 +387,6 @@ export class InvestmentReportsComponent implements OnInit {
|
||||
get amortizeTxns(): InvestmentTxn[] {
|
||||
return this.txns.filter((t) => t.txnType === 'AMORTIZE');
|
||||
}
|
||||
get dealSlips(): InvestmentTxn[] {
|
||||
return this.dealSlipTxns;
|
||||
}
|
||||
get slrBondRegister(): InvestmentHolding[] {
|
||||
return this.active('GOVT_SECURITY');
|
||||
}
|
||||
@@ -542,13 +628,90 @@ export class InvestmentReportsComponent implements OnInit {
|
||||
this.amortizeTxns.map((t) => this.txnExportRow(t))
|
||||
);
|
||||
case 'dealSlip':
|
||||
return one(
|
||||
'Deal Slip',
|
||||
'Deal Slip Register',
|
||||
this.txnExportHeaders,
|
||||
this.txnExportWidths,
|
||||
this.dealSlips.map((t) => this.txnExportRow(t))
|
||||
);
|
||||
return [
|
||||
{
|
||||
sheetName: 'Deal Slip Register',
|
||||
title: 'Deal Slip Register',
|
||||
subtitle:
|
||||
`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':
|
||||
return one(
|
||||
'Interest Due Register',
|
||||
|
||||
@@ -434,6 +434,11 @@ export interface InvestmentPurchaseDto {
|
||||
crGlCode: string;
|
||||
crGlName: 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;
|
||||
glTransactionId?: string;
|
||||
settledOnDate?: string;
|
||||
@@ -517,9 +522,15 @@ export class InvestmentService {
|
||||
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 }> {
|
||||
return this.http.post<{ resourceIdentifier?: string }>(`/investment/sales/${saleId}/settle`, {});
|
||||
/**
|
||||
* Posts a pending sale on or after its settlement date; the voucher number comes back as resourceIdentifier. The
|
||||
* 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> {
|
||||
@@ -636,14 +647,30 @@ export class InvestmentService {
|
||||
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 }>(
|
||||
`/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). */
|
||||
getNonSlrLimit(scriptId: number | null, dealDate: string, amount: number): Observable<InvestmentNonSlrLimitDto> {
|
||||
const params: Record<string, string> = { dealDate, amount: String(amount || 0) };
|
||||
@@ -1182,6 +1209,8 @@ export interface InvestmentSaleDto {
|
||||
id?: number;
|
||||
purchaseId: number;
|
||||
dealNo: string;
|
||||
/** Deal slip number of the sale itself, e.g. 202627S00001 (none for a write-off or amortisation). */
|
||||
saleNo?: string;
|
||||
certNo: string;
|
||||
bondType: string;
|
||||
scriptCode: string;
|
||||
@@ -1215,12 +1244,104 @@ export interface InvestmentSaleDto {
|
||||
investmentGlCode: string;
|
||||
sanctionAuthority?: string;
|
||||
remarks?: string;
|
||||
brokerCode?: string;
|
||||
confirmationMode?: ConfirmationMode;
|
||||
confirmationReceivedOn?: string;
|
||||
confirmationRef?: string;
|
||||
status: 'PENDING_SETTLEMENT' | 'SETTLED';
|
||||
glTransactionId?: string;
|
||||
voucherNo?: 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. */
|
||||
export interface InvestmentValuationDto {
|
||||
id?: number;
|
||||
|
||||
@@ -72,6 +72,36 @@
|
||||
@if (activeTab === 'holdings') {
|
||||
<div class="pg-card">
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -113,8 +143,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="settlingDealId !== null"
|
||||
(click)="settleDeal(d)"
|
||||
[disabled]="settlingDealId !== null || settleTarget !== null"
|
||||
(click)="openPurchaseSettle(d)"
|
||||
>
|
||||
Settle
|
||||
</button>
|
||||
@@ -127,6 +157,7 @@
|
||||
Drop
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(d.dealNo)">Deal Slip</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
@@ -219,6 +250,15 @@
|
||||
<label class="inv-form-field"
|
||||
><span>Category</span><input type="text" value="AFS (Available For Sale)" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Confirmation to Institution</span>
|
||||
<select [(ngModel)]="purchaseForm.confirmationMode">
|
||||
<option value="">-- Select --</option>
|
||||
@for (m of confirmationModes; track m.value) {
|
||||
<option [value]="m.value">{{ m.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (shareLimit; as l) {
|
||||
@@ -415,6 +455,15 @@
|
||||
<label class="inv-form-field"
|
||||
><span>Remarks</span><input type="text" maxlength="500" [(ngModel)]="refund.remarks"
|
||||
/></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>
|
||||
@if (refundQuote; as q) {
|
||||
<table class="pg-table">
|
||||
@@ -448,10 +497,40 @@
|
||||
</div>
|
||||
|
||||
<div class="pos-section-label">Pending Refunds (posted on Settle)</div>
|
||||
@if (settleTarget?.kind === 'sale') {
|
||||
<div class="pos-section-label">
|
||||
Settle {{ settleTarget?.label }} — confirmation from the institution (back office)
|
||||
</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Confirmation received on</span
|
||||
><mifosx-date-ddmmyyyy [(ngModel)]="settleConfirmation.receivedOn"></mifosx-date-ddmmyyyy
|
||||
></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Confirmation / letter ref.</span
|
||||
><input type="text" maxlength="100" [(ngModel)]="settleConfirmation.ref"
|
||||
/></label>
|
||||
</div>
|
||||
<p class="inv-hint">
|
||||
Record the institution's confirmation (share certificate / letter) if it has come; leave it blank otherwise
|
||||
and the Deal Slip Register shows it as awaited.
|
||||
</p>
|
||||
<div class="inv-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="busySaleId !== null"
|
||||
(click)="confirmSettle()"
|
||||
>
|
||||
{{ busySaleId !== null ? 'Settling…' : 'Settle and post to GL' }}
|
||||
</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="settleTarget = null">Cancel</button>
|
||||
</div>
|
||||
}
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Deal No</th>
|
||||
<th>Sale No / Deal No</th>
|
||||
<th>Institution</th>
|
||||
<th class="num">Shares</th>
|
||||
<th class="num">Amount</th>
|
||||
@@ -464,7 +543,10 @@
|
||||
@for (s of pendingRefunds; track s.id) {
|
||||
<tr>
|
||||
<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>{{ s.partyName }}</td>
|
||||
<td class="num">{{ s.units | number: '1.0-0' }}</td>
|
||||
@@ -479,8 +561,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="busySaleId !== null"
|
||||
(click)="settleRefund(s)"
|
||||
[disabled]="busySaleId !== null || settleTarget !== null"
|
||||
(click)="openRefundSettle(s)"
|
||||
>
|
||||
Settle
|
||||
</button>
|
||||
@@ -492,6 +574,7 @@
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="printDealSlip(s.saleNo)">Deal Slip</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { forkJoin } from 'rxjs';
|
||||
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||
import { InvestmentTxn, SETTLEMENT_BANK_GL_OPTIONS, todayDDMMYYYY } from '../investment-data';
|
||||
import { exportWorkbook } from '../investment-export';
|
||||
import { saveDealSlipPdf } from '../deal-slip-pdf';
|
||||
import {
|
||||
InvestmentDividendDto,
|
||||
InvestmentPartyDto,
|
||||
@@ -22,6 +23,8 @@ import {
|
||||
InvestmentService,
|
||||
InvestmentShareLimitDto,
|
||||
SHARE_INSTITUTION_TYPES,
|
||||
CONFIRMATION_MODES,
|
||||
SettleConfirmation,
|
||||
apiErrorMessage,
|
||||
awaitingApproval,
|
||||
sentForApproval,
|
||||
@@ -42,6 +45,8 @@ interface SharePurchaseForm {
|
||||
shares: number | null;
|
||||
pricePerShare: number | null;
|
||||
bankGlCode: string;
|
||||
/** How the deal was confirmed to the institution (deal slip). */
|
||||
confirmationMode: string;
|
||||
}
|
||||
|
||||
interface ShareRefundForm {
|
||||
@@ -51,6 +56,7 @@ interface ShareRefundForm {
|
||||
pricePerShare: number | null;
|
||||
bankGlCode: string;
|
||||
remarks: string;
|
||||
confirmationMode: string;
|
||||
}
|
||||
|
||||
interface ShareDividendForm {
|
||||
@@ -218,18 +224,68 @@ export class ShareInvestmentComponent implements OnInit {
|
||||
return `inv-badge inv-badge--${status.toLowerCase()}`;
|
||||
}
|
||||
|
||||
// -------------------- Settle with the institution's confirmation (back office) --------------------
|
||||
/** The deal or refund the back office is settling, with the confirmation it records (both optional). */
|
||||
settleTarget: { kind: 'purchase' | 'sale'; id: number; label: string } | null = null;
|
||||
settleConfirmation: SettleConfirmation = {};
|
||||
readonly confirmationModes = CONFIRMATION_MODES;
|
||||
|
||||
openPurchaseSettle(d: InvestmentPurchaseDto): void {
|
||||
this.settleTarget = { kind: 'purchase', id: d.id, label: `deal ${d.dealNo}` };
|
||||
this.settleConfirmation = {};
|
||||
}
|
||||
|
||||
openRefundSettle(s: InvestmentSaleDto): void {
|
||||
if (!s.id) {
|
||||
return;
|
||||
}
|
||||
this.settleTarget = { kind: 'sale', id: s.id, label: `refund ${s.saleNo ?? ''} on deal ${s.dealNo}` };
|
||||
this.settleConfirmation = {};
|
||||
}
|
||||
|
||||
confirmSettle(): void {
|
||||
const t = this.settleTarget;
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
if (t.kind === 'purchase') {
|
||||
const d = this.deals.find((x) => x.id === t.id);
|
||||
if (d) {
|
||||
this.settleDeal(d, this.settleConfirmation);
|
||||
}
|
||||
} else {
|
||||
const s = this.sales.find((x) => x.id === t.id);
|
||||
if (s) {
|
||||
this.settleRefund(s, this.settleConfirmation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Downloads the deal slip (PDF) of a purchase deal number or a refund's sale number. */
|
||||
printDealSlip(slipNo?: string): void {
|
||||
if (!slipNo) {
|
||||
this.toast('This deal has no deal slip number.');
|
||||
return;
|
||||
}
|
||||
this.investmentService.getDealSlip(slipNo).subscribe({
|
||||
next: (rows) => (rows.length ? saveDealSlipPdf(rows[0]) : this.toast(`Deal slip ${slipNo} not found.`)),
|
||||
error: (err) => this.toast(apiErrorMessage(err))
|
||||
});
|
||||
}
|
||||
|
||||
/** The purchase being settled; its button stays disabled so it cannot be sent twice. */
|
||||
settlingDealId: number | null = null;
|
||||
|
||||
/** Posts a pending deal to GL dated its settlement date: Dr 100706 Investment in Shares / Cr bank. */
|
||||
settleDeal(d: InvestmentPurchaseDto): void {
|
||||
settleDeal(d: InvestmentPurchaseDto, confirmation: SettleConfirmation = {}): void {
|
||||
if (this.settlingDealId !== null) {
|
||||
return;
|
||||
}
|
||||
this.settlingDealId = d.id;
|
||||
this.investmentService.settlePurchase(d.id).subscribe({
|
||||
this.investmentService.settlePurchase(d.id, confirmation).subscribe({
|
||||
next: (res) => {
|
||||
this.settlingDealId = null;
|
||||
this.settleTarget = null;
|
||||
this.toast(
|
||||
awaitingApproval(res)
|
||||
? sentForApproval(`Settlement of deal ${d.dealNo}`)
|
||||
@@ -271,7 +327,8 @@ export class ShareInvestmentComponent implements OnInit {
|
||||
settlementType: 'T+0',
|
||||
shares: 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,
|
||||
crGlAccountId,
|
||||
subType: '',
|
||||
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
locale: 'en'
|
||||
};
|
||||
@@ -474,7 +532,8 @@ export class ShareInvestmentComponent implements OnInit {
|
||||
units: null,
|
||||
pricePerShare: null,
|
||||
bankGlCode: '',
|
||||
remarks: ''
|
||||
remarks: '',
|
||||
confirmationMode: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -546,6 +605,7 @@ export class ShareInvestmentComponent implements OnInit {
|
||||
saleRate: f.pricePerShare,
|
||||
bankGlAccountId,
|
||||
...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
|
||||
...(f.confirmationMode ? { confirmationMode: f.confirmationMode } : {}),
|
||||
dateFormat: 'dd-MM-yyyy',
|
||||
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. */
|
||||
busySaleId: number | null = null;
|
||||
|
||||
settleRefund(s: InvestmentSaleDto): void {
|
||||
settleRefund(s: InvestmentSaleDto, confirmation: SettleConfirmation = {}): void {
|
||||
if (!s.id || this.busySaleId !== null) {
|
||||
return;
|
||||
}
|
||||
this.busySaleId = s.id;
|
||||
this.investmentService.settleSale(s.id).subscribe({
|
||||
this.investmentService.settleSale(s.id, confirmation).subscribe({
|
||||
next: (res) => {
|
||||
this.busySaleId = null;
|
||||
this.settleTarget = null;
|
||||
this.toast(
|
||||
awaitingApproval(res)
|
||||
? sentForApproval(`Settlement of the refund on deal ${s.dealNo}`)
|
||||
|
||||
Reference in New Issue
Block a user