feat(investment): interest received and receivable report page with Excel export

This commit is contained in:
2026-10-23 11:26:11 +05:30
parent 86e3a5da27
commit 7bd880387e
6 changed files with 342 additions and 2 deletions
+2 -2
View File
@@ -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 &amp; Receivable Report <fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received &amp; 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 &amp; Receivable Report <fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received &amp; 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;
}
@@ -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 &amp; 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>
@@ -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,
+31
View File
@@ -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 } });