/** * 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 { 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); } }