Files
cbsnexora/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts
T

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