284 lines
9.0 KiB
TypeScript
284 lines
9.0 KiB
TypeScript
/**
|
|
* 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 } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import * as ExcelJS from 'exceljs';
|
|
import {
|
|
addBanner,
|
|
addLineRows,
|
|
addSectionHeader,
|
|
boxAllCells,
|
|
buildGeneralInfoSheet,
|
|
buildSignatorySheet,
|
|
downloadWorkbook,
|
|
newWorkbook,
|
|
solidFill,
|
|
XL
|
|
} from '../oss-workbook';
|
|
import { RAL_LIABILITIES } from '../oss1-ral/oss1-ral-data';
|
|
import {
|
|
ROE_EARNINGS,
|
|
ROE_EARNINGS_NOTES,
|
|
ROE_GENERAL_INFO,
|
|
ROE_INTEREST_NOT_RECOGNIZED,
|
|
ROE_OTHER_OPERATING_EXPENSES,
|
|
ROE_SIGNATORIES
|
|
} from './oss2-roe-data';
|
|
|
|
type Oss2Tab = 'general' | 'earnings' | 'interest' | 'other-expenses' | 'signatory';
|
|
|
|
/** One of the two OSS-1 tie-ups printed at the foot of the Earnings sheet. */
|
|
interface TieUp {
|
|
label: string;
|
|
oss2Value: number | null;
|
|
oss1Label: string;
|
|
oss1Value: number | null;
|
|
tallies: boolean;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'mifosx-oss2-roe',
|
|
templateUrl: './oss2-roe.component.html',
|
|
styleUrls: ['../oss-return.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
FaIconComponent,
|
|
FormsModule
|
|
]
|
|
})
|
|
export class Oss2RoeComponent {
|
|
activeTab: Oss2Tab = 'general';
|
|
|
|
generalInfo = ROE_GENERAL_INFO;
|
|
earnings = ROE_EARNINGS;
|
|
earningsNotes = ROE_EARNINGS_NOTES;
|
|
interestNotRecognized = ROE_INTEREST_NOT_RECOGNIZED;
|
|
otherExpenses = ROE_OTHER_OPERATING_EXPENSES;
|
|
signatories = ROE_SIGNATORIES;
|
|
|
|
generating = false;
|
|
toastMsg = '';
|
|
toastVisible = false;
|
|
private toastTimer: any;
|
|
|
|
showTab(tab: Oss2Tab): void {
|
|
this.activeTab = tab;
|
|
}
|
|
|
|
private info(label: string): string {
|
|
return this.generalInfo.find((g) => g.label === label)?.value ?? '';
|
|
}
|
|
|
|
get returnName(): string {
|
|
return this.info('Return Name');
|
|
}
|
|
get returnCode(): string {
|
|
return this.info('Return Code');
|
|
}
|
|
get bankName(): string {
|
|
return this.info('Name Of Bank');
|
|
}
|
|
get bankCode(): string {
|
|
return this.info('Bank Code');
|
|
}
|
|
get quarterEndDate(): string {
|
|
return this.info('Quarter End Date');
|
|
}
|
|
get auditFlag(): string {
|
|
return this.info('Audit Flag');
|
|
}
|
|
|
|
private earningsValue(prefix: string): number | null {
|
|
return this.earnings.find((l) => l.label.startsWith(prefix))?.value ?? null;
|
|
}
|
|
|
|
private liabilityValue(prefix: string): number | null {
|
|
return RAL_LIABILITIES.find((l) => l.label.startsWith(prefix))?.value ?? null;
|
|
}
|
|
|
|
get netProfitAfterTax(): number {
|
|
return this.earningsValue('23.') ?? 0;
|
|
}
|
|
get operatingProfit(): number {
|
|
return this.earningsValue('14.') ?? 0;
|
|
}
|
|
|
|
/** Sum of the detail sheet — must equal Earnings line 12 ix) Others. */
|
|
get otherExpensesTotal(): number {
|
|
return this.otherExpenses.reduce((sum, e) => sum + (e.amount ?? 0), 0);
|
|
}
|
|
get otherExpensesLineValue(): number | null {
|
|
return this.earningsValue('ix) Others');
|
|
}
|
|
get otherExpensesTally(): boolean {
|
|
return this.otherExpensesTotal === (this.otherExpensesLineValue ?? 0);
|
|
}
|
|
|
|
/**
|
|
* The Earnings sheet carries two footnotes requiring 17(i) and 17(ii) to tally
|
|
* with OSS-1 liabilities 12(ii) and 12(i). Checked live rather than left to a
|
|
* reader spotting it in the fine print.
|
|
*/
|
|
get oss1TieUps(): TieUp[] {
|
|
const rows: TieUp[] = [
|
|
{
|
|
label: '17 (i) Provision for loan losses (BDDR, Spl. BDDR, etc.)',
|
|
oss2Value: this.earningsValue('i) Provision for loan losses'),
|
|
oss1Label: 'OSS-1 liabilities 12 (ii) BDDR, Spl BDDR',
|
|
oss1Value: this.liabilityValue('ii. BDDR, Spl BDDR'),
|
|
tallies: false
|
|
},
|
|
{
|
|
label: '17 (ii) Provision for depreciation in investments including AFS',
|
|
oss2Value: this.earningsValue('ii) Provision for depreciation in investments'),
|
|
oss1Label: 'OSS-1 liabilities 12 (i) Investment Depreciation Reserve',
|
|
oss1Value: this.liabilityValue('i. For investments (Investment Depreciation Reserve)'),
|
|
tallies: false
|
|
}
|
|
];
|
|
return rows.map((r) => ({ ...r, tallies: (r.oss2Value ?? 0) === (r.oss1Value ?? 0) }));
|
|
}
|
|
|
|
get allTieUpsMatch(): boolean {
|
|
return this.oss1TieUps.every((t) => t.tallies);
|
|
}
|
|
|
|
// ══════════════ Excel generation ══════════════
|
|
|
|
/** Rebuilds the RBI workbook: one sheet per section, original layout and styling. */
|
|
async generateExcel(): Promise<void> {
|
|
this.generating = true;
|
|
try {
|
|
const wb = newWorkbook();
|
|
|
|
buildGeneralInfoSheet(wb, this.generalInfo);
|
|
this.buildEarningsSheet(wb);
|
|
this.buildInterestSheet(wb);
|
|
this.buildOtherExpensesSheet(wb);
|
|
buildSignatorySheet(wb, this.signatories, 'Signatory Information');
|
|
|
|
await downloadWorkbook(wb, `Form OSS-2_CIMS ${this.returnCode} - ${this.quarterEndDate}.xlsx`);
|
|
this.toast(`${this.returnCode} workbook generated for quarter ending ${this.quarterEndDate}`);
|
|
} catch {
|
|
this.toast('Could not generate the workbook — please retry');
|
|
} finally {
|
|
this.generating = false;
|
|
}
|
|
}
|
|
|
|
private buildEarningsSheet(wb: ExcelJS.Workbook): void {
|
|
const sheet = wb.addWorksheet('Earnings');
|
|
sheet.columns = [
|
|
{ width: 90 },
|
|
{ width: 20 }
|
|
];
|
|
addBanner(sheet, 'Earnings', 'B');
|
|
|
|
const first = sheet.rowCount + 1;
|
|
addSectionHeader(sheet, '', 'Rs in Thousand');
|
|
addLineRows(sheet, this.earnings);
|
|
boxAllCells(sheet, first, sheet.rowCount, 2);
|
|
|
|
sheet.addRow([]);
|
|
const notesStart = sheet.rowCount + 1;
|
|
for (const note of this.earningsNotes) {
|
|
const row = sheet.addRow([note]);
|
|
sheet.mergeCells(`A${row.number}:B${row.number}`);
|
|
row.font = { bold: true, italic: true, size: 10 };
|
|
row.alignment = { wrapText: true };
|
|
row.getCell(1).fill = solidFill(XL.headerGrey);
|
|
}
|
|
boxAllCells(sheet, notesStart, sheet.rowCount, 2);
|
|
|
|
sheet.views = [{ state: 'frozen', ySplit: 3 }];
|
|
}
|
|
|
|
private buildInterestSheet(wb: ExcelJS.Workbook): void {
|
|
const sheet = wb.addWorksheet('Interest Receivable Not Recog');
|
|
sheet.columns = [
|
|
{ width: 72 },
|
|
{ width: 22 }
|
|
];
|
|
addBanner(sheet, 'Interest Receivable Not Recognized', 'B');
|
|
|
|
const start = sheet.rowCount + 1;
|
|
const unit = sheet.addRow(['', 'Rs in Thousand']);
|
|
unit.font = { bold: true };
|
|
unit.alignment = { horizontal: 'center' };
|
|
unit.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
|
|
|
const head = sheet.addRow(['Interest receivable on NPAs not recognized as income', this.quarterEndDate]);
|
|
head.font = { bold: true };
|
|
head.getCell(2).alignment = { horizontal: 'center' };
|
|
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
|
|
|
for (const r of this.interestNotRecognized) {
|
|
const row = sheet.addRow([r.label, r.value]);
|
|
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
|
|
row.getCell(1).fill = solidFill(XL.headerGrey);
|
|
row.getCell(2).numFmt = '#,##0.00';
|
|
if (r.total) {
|
|
row.getCell(2).fill = solidFill(XL.derivedBlue);
|
|
row.getCell(2).font = { bold: true };
|
|
}
|
|
}
|
|
boxAllCells(sheet, start, sheet.rowCount, 2);
|
|
}
|
|
|
|
private buildOtherExpensesSheet(wb: ExcelJS.Workbook): void {
|
|
const sheet = wb.addWorksheet('Other Operating Expenses');
|
|
sheet.columns = [
|
|
{ width: 10 },
|
|
{ width: 56 },
|
|
{ width: 20 }
|
|
];
|
|
addBanner(sheet, 'Other Operating Expenses', 'C');
|
|
|
|
const start = sheet.rowCount + 1;
|
|
const title = sheet.addRow(['Details of Other Operating Expenses', '', 'Rs in Thousand']);
|
|
sheet.mergeCells(`A${title.number}:B${title.number}`);
|
|
title.font = { bold: true };
|
|
title.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
|
title.getCell(3).alignment = { horizontal: 'center' };
|
|
|
|
const head = sheet.addRow(['Sr No', 'Particulars', 'Amount']);
|
|
head.font = { bold: true };
|
|
head.alignment = { horizontal: 'center' };
|
|
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
|
|
|
for (const e of this.otherExpenses) {
|
|
const row = sheet.addRow([e.srNo, e.particulars, e.amount]);
|
|
row.getCell(3).numFmt = '#,##0.00';
|
|
}
|
|
|
|
const total = sheet.addRow(['Total', '', this.otherExpensesTotal]);
|
|
sheet.mergeCells(`A${total.number}:B${total.number}`);
|
|
total.font = { bold: true };
|
|
total.getCell(1).fill = solidFill(XL.headerGrey);
|
|
total.getCell(3).numFmt = '#,##0.00';
|
|
total.getCell(3).fill = solidFill(XL.derivedBlue);
|
|
|
|
boxAllCells(sheet, start, sheet.rowCount, 3);
|
|
|
|
const ref = sheet.addRow(['', '', 'Refer Earnings Sheet — line 12 ix) Others']);
|
|
ref.getCell(3).font = { italic: true, size: 10 };
|
|
}
|
|
|
|
private toast(msg: string): void {
|
|
this.toastMsg = msg;
|
|
this.toastVisible = true;
|
|
if (this.toastTimer) {
|
|
clearTimeout(this.toastTimer);
|
|
}
|
|
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
|
}
|
|
}
|