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
@@ -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;
}
}
}