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">
|
||||
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
||||
</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
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||
@@ -2500,7 +2500,7 @@
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
||||
routerLink="/investment/reports"
|
||||
routerLink="/investment/interest-report"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
||||
|
||||
@@ -82,3 +82,8 @@
|
||||
white-space: nowrap;
|
||||
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 { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
||||
import { InvestmentInterestReportComponent } from './investment-interest-report/investment-interest-report.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
@@ -60,6 +61,11 @@ const routes: Routes = [
|
||||
component: InvestmentDiaryComponent,
|
||||
data: { title: 'Due Date Diary', breadcrumb: 'Due Date Diary' }
|
||||
},
|
||||
{
|
||||
path: 'interest-report',
|
||||
component: InvestmentInterestReportComponent,
|
||||
data: { title: 'Interest Received & Receivable', breadcrumb: 'Interest Received & Receivable' }
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
component: InvestmentReportsComponent,
|
||||
|
||||
@@ -143,6 +143,32 @@ export interface InvestmentFdScheduleDto {
|
||||
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. */
|
||||
export interface InvestmentDiaryRowDto {
|
||||
date: string;
|
||||
@@ -501,6 +527,11 @@ export class InvestmentService {
|
||||
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). */
|
||||
getDiary(fromDate: string, toDate: string): Observable<InvestmentDiaryDto> {
|
||||
return this.http.get<InvestmentDiaryDto>('/investment/diary', { params: { fromDate, toDate } });
|
||||
|
||||
Reference in New Issue
Block a user