feat(investment): interest received and receivable report page with Excel export
This commit is contained in:
@@ -1525,7 +1525,7 @@
|
|||||||
<button mat-menu-item routerLink="/investment/diary" tabindex="0">
|
<button mat-menu-item routerLink="/investment/diary" tabindex="0">
|
||||||
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
<button mat-menu-item routerLink="/investment/interest-report" tabindex="0">
|
||||||
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||||
@@ -2500,7 +2500,7 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
||||||
routerLink="/investment/reports"
|
routerLink="/investment/interest-report"
|
||||||
(click)="investmentMenuOpen = false"
|
(click)="investmentMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
||||||
|
|||||||
@@ -82,3 +82,8 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-total td {
|
||||||
|
font-weight: 700;
|
||||||
|
border-top: 2px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pg">
|
||||||
|
<header class="pg-head">
|
||||||
|
<div>
|
||||||
|
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||||
|
<h1 class="pg-title">Total Interest Received & Receivable</h1>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Interest received from Govt Securities, bonds and FDs in the period, and interest earned up to the To date that
|
||||||
|
has not come in yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="!report || generating" (click)="exportExcel()">
|
||||||
|
{{ generating ? 'Generating…' : 'Export to Excel' }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From</span><mifosx-date-ddmmyyyy [(ngModel)]="fromDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To (receivable as on)</span><mifosx-date-ddmmyyyy [(ngModel)]="toDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Investment</span>
|
||||||
|
<select [(ngModel)]="instrument">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="SECURITY">Govt Security / Bond</option>
|
||||||
|
<option value="FD">FD with banks</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="diary-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (error) {
|
||||||
|
<p class="diary-error">{{ error }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (report; as r) {
|
||||||
|
<div class="diary-summary">
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Received</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.received | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Receivable</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.receivable | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Of which provisioned (100603)</span
|
||||||
|
><span class="diary-tile-v">₹{{ totals.provisioned | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Total</span><span class="diary-tile-v">₹{{ totals.total | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">
|
||||||
|
{{ r.fromDate }} to {{ r.toDate }} · received in the period, receivable as on {{ r.toDate }}
|
||||||
|
</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Investment</th>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="num">Principal / Face Value</th>
|
||||||
|
<th class="num">Rate</th>
|
||||||
|
<th class="num col-in">Received</th>
|
||||||
|
<th class="num">Receivable</th>
|
||||||
|
<th class="num">Of which provisioned</th>
|
||||||
|
<th class="num">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (x of rows; track x.instrument + x.refId) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ x.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ x.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ x.particulars }}</td>
|
||||||
|
<td>{{ x.status }}</td>
|
||||||
|
<td class="num">₹{{ x.principal | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">{{ x.rate !== null && x.rate !== undefined ? x.rate + '%' : '—' }}</td>
|
||||||
|
<td class="num col-in">₹{{ x.received | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.receivable | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.provisioned | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ x.total | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="pg-empty">No interest received or receivable in this period.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
@if (rows.length) {
|
||||||
|
<tfoot>
|
||||||
|
<tr class="report-total">
|
||||||
|
<td colspan="6">Total</td>
|
||||||
|
<td class="num col-in">₹{{ totals.received | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.receivable | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.provisioned | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ totals.total | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import { parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
|
||||||
|
import { exportWorkbook } from '../investment-export';
|
||||||
|
import {
|
||||||
|
InvestmentInterestReportDto,
|
||||||
|
InvestmentInterestReportRowDto,
|
||||||
|
InvestmentService,
|
||||||
|
apiErrorMessage
|
||||||
|
} from '../investment.service';
|
||||||
|
|
||||||
|
type InstrumentFilter = '' | 'SECURITY' | 'FD';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total Interest Received & Receivable: per Govt Security / bond deal and FD, the interest received in a period and the
|
||||||
|
* interest earned up to its last day that has not come in yet, with the part already provisioned in the books.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-investment-interest-report',
|
||||||
|
templateUrl: './investment-interest-report.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../investment-shared.scss',
|
||||||
|
'../investment-diary/investment-diary.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InvestmentInterestReportComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
/** Defaults to the current financial year, 1 April to today. */
|
||||||
|
fromDate = InvestmentInterestReportComponent.financialYearStart();
|
||||||
|
toDate = todayDDMMYYYY();
|
||||||
|
instrument: InstrumentFilter = '';
|
||||||
|
report: InvestmentInterestReportDto | null = null;
|
||||||
|
loading = false;
|
||||||
|
generating = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static financialYearStart(): string {
|
||||||
|
const d = parseDDMMYYYY(todayDDMMYYYY());
|
||||||
|
const year = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
|
||||||
|
return `01-04-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (!this.fromDate || !this.toDate) {
|
||||||
|
this.error = 'Enter the From and To dates.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.investmentService.getInterestReport(this.fromDate, this.toDate).subscribe({
|
||||||
|
next: (r) => {
|
||||||
|
this.report = r;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.report = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows(): InvestmentInterestReportRowDto[] {
|
||||||
|
return (this.report?.rows ?? []).filter((r) => !this.instrument || r.instrument === this.instrument);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Totals of the rows shown, so they follow the Investment filter. */
|
||||||
|
get totals(): { received: number; receivable: number; provisioned: number; total: number } {
|
||||||
|
return this.rows.reduce(
|
||||||
|
(t, r) => ({
|
||||||
|
received: t.received + r.received,
|
||||||
|
receivable: t.receivable + r.receivable,
|
||||||
|
provisioned: t.provisioned + r.provisioned,
|
||||||
|
total: t.total + r.total
|
||||||
|
}),
|
||||||
|
{ received: 0, receivable: 0, provisioned: 0, total: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportExcel(): Promise<void> {
|
||||||
|
if (!this.report) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.generating = true;
|
||||||
|
try {
|
||||||
|
const t = this.totals;
|
||||||
|
await exportWorkbook(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
sheetName: 'Interest Received',
|
||||||
|
title: 'Total Interest Received & Receivable',
|
||||||
|
subtitle: `${this.report.fromDate} to ${this.report.toDate}`,
|
||||||
|
headers: [
|
||||||
|
'Investment',
|
||||||
|
'Deal No',
|
||||||
|
'Particulars',
|
||||||
|
'Status',
|
||||||
|
'Principal / Face Value',
|
||||||
|
'Rate %',
|
||||||
|
'Received',
|
||||||
|
'Receivable',
|
||||||
|
'Of which provisioned',
|
||||||
|
'Total'
|
||||||
|
],
|
||||||
|
widths: [
|
||||||
|
14,
|
||||||
|
16,
|
||||||
|
40,
|
||||||
|
12,
|
||||||
|
18,
|
||||||
|
8,
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
18,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...this.rows.map((r) => [
|
||||||
|
r.instrument === 'FD' ? 'FD' : 'Govt Security / Bond',
|
||||||
|
r.dealNo,
|
||||||
|
r.particulars,
|
||||||
|
r.status,
|
||||||
|
r.principal,
|
||||||
|
r.rate ?? null,
|
||||||
|
r.received,
|
||||||
|
r.receivable,
|
||||||
|
r.provisioned,
|
||||||
|
r.total
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Total',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
t.received,
|
||||||
|
t.receivable,
|
||||||
|
t.provisioned,
|
||||||
|
t.total
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
`Interest-Received-Receivable-${this.report.toDate}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.generating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { InvestmentMastersComponent } from './investment-masters/investment-mast
|
|||||||
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
||||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||||
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
||||||
|
import { InvestmentInterestReportComponent } from './investment-interest-report/investment-interest-report.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -60,6 +61,11 @@ const routes: Routes = [
|
|||||||
component: InvestmentDiaryComponent,
|
component: InvestmentDiaryComponent,
|
||||||
data: { title: 'Due Date Diary', breadcrumb: 'Due Date Diary' }
|
data: { title: 'Due Date Diary', breadcrumb: 'Due Date Diary' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'interest-report',
|
||||||
|
component: InvestmentInterestReportComponent,
|
||||||
|
data: { title: 'Interest Received & Receivable', breadcrumb: 'Interest Received & Receivable' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'reports',
|
path: 'reports',
|
||||||
component: InvestmentReportsComponent,
|
component: InvestmentReportsComponent,
|
||||||
|
|||||||
@@ -143,6 +143,32 @@ export interface InvestmentFdScheduleDto {
|
|||||||
rows: InvestmentFdInterestRowDto[];
|
rows: InvestmentFdInterestRowDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One Govt Security / bond deal or FD in the Interest Received & Receivable report. */
|
||||||
|
export interface InvestmentInterestReportRowDto {
|
||||||
|
instrument: 'SECURITY' | 'FD';
|
||||||
|
refId: number;
|
||||||
|
dealNo: string;
|
||||||
|
particulars: string;
|
||||||
|
status: string;
|
||||||
|
principal: number;
|
||||||
|
rate?: number;
|
||||||
|
received: number;
|
||||||
|
receivable: number;
|
||||||
|
provisioned: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interest received in a period and receivable at its end, with totals. */
|
||||||
|
export interface InvestmentInterestReportDto {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
totalReceived: number;
|
||||||
|
totalReceivable: number;
|
||||||
|
totalProvisioned: number;
|
||||||
|
total: number;
|
||||||
|
rows: InvestmentInterestReportRowDto[];
|
||||||
|
}
|
||||||
|
|
||||||
/** One dated event in the Due Date Diary. */
|
/** One dated event in the Due Date Diary. */
|
||||||
export interface InvestmentDiaryRowDto {
|
export interface InvestmentDiaryRowDto {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -501,6 +527,11 @@ export class InvestmentService {
|
|||||||
return this.http.get<InvestmentFdDueDto[]>('/investment/fds/due');
|
return this.http.get<InvestmentFdDueDto[]>('/investment/fds/due');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Total Interest Received & Receivable for a period (DD-MM-YYYY). */
|
||||||
|
getInterestReport(fromDate: string, toDate: string): Observable<InvestmentInterestReportDto> {
|
||||||
|
return this.http.get<InvestmentInterestReportDto>('/investment/interest-report', { params: { fromDate, toDate } });
|
||||||
|
}
|
||||||
|
|
||||||
/** Due Date Diary: dated events of all active investments between two dates (DD-MM-YYYY). */
|
/** Due Date Diary: dated events of all active investments between two dates (DD-MM-YYYY). */
|
||||||
getDiary(fromDate: string, toDate: string): Observable<InvestmentDiaryDto> {
|
getDiary(fromDate: string, toDate: string): Observable<InvestmentDiaryDto> {
|
||||||
return this.http.get<InvestmentDiaryDto>('/investment/diary', { params: { fromDate, toDate } });
|
return this.http.get<InvestmentDiaryDto>('/investment/diary', { params: { fromDate, toDate } });
|
||||||
|
|||||||
Reference in New Issue
Block a user