/** * 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_AGEING_HEADERS, RAL_ASSETS, RAL_BALANCE_SHEET_NOTES, RAL_COMPLAINTS, RAL_DEPOSITORS_BORROWERS, RAL_GENERAL_INFO, RAL_INTER_BANK_AGEING, RAL_INTER_BRANCH_AGEING, RAL_LIABILITIES, RAL_MEMO, RAL_OTHER_FUNDS, RAL_SIGNATORIES, RAL_SUNDRY_DEBTORS, RAL_SUNDRY_DEBTORS_NOTE, RAL_TOP_20_DEPOSITS, RAL_UNSECURED_GUARANTEES, RAL_UNSECURED_GUARANTEES_TOTAL } from './oss1-ral-data'; type Oss1Tab = 'general' | 'balance-sheet' | 'annexure1' | 'annexure2' | 'signatory'; @Component({ selector: 'mifosx-oss1-ral', templateUrl: './oss1-ral.component.html', styleUrls: ['../oss-return.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, FormsModule ] }) export class Oss1RalComponent { activeTab: Oss1Tab = 'general'; generalInfo = RAL_GENERAL_INFO; liabilities = RAL_LIABILITIES; assets = RAL_ASSETS; memo = RAL_MEMO; balanceSheetNotes = RAL_BALANCE_SHEET_NOTES; otherFunds = RAL_OTHER_FUNDS; topDeposits = RAL_TOP_20_DEPOSITS; interBranchAgeing = RAL_INTER_BRANCH_AGEING; interBankAgeing = RAL_INTER_BANK_AGEING; ageingHeaders = RAL_AGEING_HEADERS; unsecuredGuarantees = RAL_UNSECURED_GUARANTEES; unsecuredGuaranteesTotal = RAL_UNSECURED_GUARANTEES_TOTAL; sundryDebtors = RAL_SUNDRY_DEBTORS; sundryDebtorsNote = RAL_SUNDRY_DEBTORS_NOTE; depositorsBorrowers = RAL_DEPOSITORS_BORROWERS; complaints = RAL_COMPLAINTS; signatories = RAL_SIGNATORIES; generating = false; toastMsg = ''; toastVisible = false; private toastTimer: any; showTab(tab: Oss1Tab): 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('Bank Name'); } 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'); } /** Total Liabilities must equal Total Assets — surfaced so a mismatch can't be filed unnoticed. */ get totalLiabilities(): number { return this.liabilities.find((l) => l.label.startsWith('15.'))?.value ?? 0; } get totalAssets(): number { return this.assets.find((a) => a.label.startsWith('12.'))?.value ?? 0; } get isBalanced(): boolean { return this.totalLiabilities === this.totalAssets; } // ══════════════ 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.buildBalanceSheet(wb); this.buildAnnexure1(wb); this.buildAnnexure2(wb); buildSignatorySheet(wb, this.signatories); await downloadWorkbook(wb, `Form RAL_CIMS ${this.returnCode} OSS 1 - ${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 buildBalanceSheet(wb: ExcelJS.Workbook): void { const sheet = wb.addWorksheet('Assets & Liabilities'); sheet.columns = [ { width: 78 }, { width: 20 } ]; addBanner(sheet, 'Assets & Liabilities', 'B'); const first = sheet.rowCount + 1; addSectionHeader(sheet, 'Liabilities', 'Rs in Thousands'); addLineRows(sheet, this.liabilities); addSectionHeader(sheet, 'Assets', 'Rs in Thousands'); addLineRows(sheet, this.assets); boxAllCells(sheet, first, sheet.rowCount, 2); sheet.addRow([]); const notesStart = sheet.rowCount + 1; for (const note of this.balanceSheetNotes) { const row = sheet.addRow([note]); sheet.mergeCells(`A${row.number}:B${row.number}`); row.font = { bold: true, size: 10 }; row.getCell(1).fill = solidFill(XL.headerGrey); } boxAllCells(sheet, notesStart, sheet.rowCount, 2); sheet.addRow([]); const memoStart = sheet.rowCount + 1; addSectionHeader(sheet, 'Memo', 'Rs in Thousands'); addLineRows(sheet, this.memo); boxAllCells(sheet, memoStart, sheet.rowCount, 2); sheet.views = [{ state: 'frozen', ySplit: 3 }]; } private buildAnnexure1(wb: ExcelJS.Workbook): void { const sheet = wb.addWorksheet('Annexure 1'); sheet.columns = [ { width: 10 }, { width: 46 }, { width: 20 }, { width: 18 }, { width: 16 }, { width: 12 }, { width: 12 }, { width: 12 } ]; addBanner(sheet, 'Annexure 1', 'H'); // 1. Break-up of Other Funds and Reserves let start = sheet.rowCount + 1; const fundsTitle = sheet.addRow(['1. Break-up of Other Funds and Reserves', '', 'Rs in Thousands']); sheet.mergeCells(`A${fundsTitle.number}:B${fundsTitle.number}`); fundsTitle.font = { bold: true }; fundsTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const line of this.otherFunds) { const row = sheet.addRow([line.label, '', line.value]); sheet.mergeCells(`A${row.number}:B${row.number}`); row.getCell(1).alignment = { wrapText: true, vertical: 'middle' }; row.getCell(3).numFmt = '#,##0.00'; if (line.total) { row.font = { bold: true }; row.getCell(3).fill = solidFill(XL.derivedBlue); row.getCell(1).alignment = { horizontal: 'center' }; } } boxAllCells(sheet, start, sheet.rowCount, 3); sheet.addRow([]); // 2. Top 20 deposits from other banks / institutions start = sheet.rowCount + 1; const depTitle = sheet.addRow([ `2. Particulars of top 20 deposits accepted from other banks / and institutions as on the date of review` ]); sheet.mergeCells(`A${depTitle.number}:H${depTitle.number}`); depTitle.font = { bold: true }; depTitle.getCell(1).fill = solidFill(XL.headerGrey); const depHead = sheet.addRow([ 'Sr. No', 'Name of Bank / Institution', 'Amount of deposit', 'Rate of interest (%)', 'Date of receipt', 'Period in Days', 'Period in Months', 'Period in Years' ]); depHead.font = { bold: true }; depHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; depHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const d of this.topDeposits) { const row = sheet.addRow([ d.srNo, d.institution, d.amount, d.rateOfInterest, d.dateOfReceipt, d.days, d.months, d.years ]); row.getCell(3).numFmt = '#,##0.00'; } boxAllCells(sheet, start, sheet.rowCount, 8); sheet.addRow([]); // 3 & 4 — ageing analyses start = sheet.rowCount + 1; this.addAgeingBlock( sheet, `3. Age-wise analysis of Inter Branch Adjustment Account - Position as on:${this.quarterEndDate}`, this.interBranchAgeing ); sheet.addRow([]); this.addAgeingBlock( sheet, `4. Age-wise analysis of Inter Bank Reconciliation Account - Position as on:${this.quarterEndDate}`, this.interBankAgeing ); sheet.addRow([]); // 5. Unsecured guarantees start = sheet.rowCount + 1; const gTitle = sheet.addRow(['5. Unsecured guarantees issued by the bank']); sheet.mergeCells(`A${gTitle.number}:D${gTitle.number}`); gTitle.font = { bold: true }; gTitle.getCell(1).fill = solidFill(XL.headerGrey); const gHead = sheet.addRow([ 'Sr. No', 'Name of Borrower', 'Name of Beneficiary', `Amount Outstanding as on:${this.quarterEndDate}` ]); gHead.font = { bold: true }; gHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; gHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const g of this.unsecuredGuarantees) { const row = sheet.addRow([g.srNo, g.borrower, g.beneficiary, g.amountOutstanding]); row.getCell(4).numFmt = '#,##0.00'; } const gTotal = sheet.addRow(['Total', '', '', this.unsecuredGuaranteesTotal]); gTotal.font = { bold: true }; gTotal.getCell(4).numFmt = '#,##0.00'; gTotal.getCell(4).fill = solidFill(XL.derivedBlue); const gNote = sheet.addRow([ 'Note : * Give name if borrower/beneficiary is director/top management/related entity. Other guarantees may be consolidated and reported against others.' ]); sheet.mergeCells(`A${gNote.number}:D${gNote.number}`); gNote.font = { bold: true, size: 10 }; gNote.alignment = { wrapText: true }; boxAllCells(sheet, start, sheet.rowCount, 4); } private addAgeingBlock(sheet: ExcelJS.Worksheet, title: string, rows: typeof RAL_INTER_BRANCH_AGEING): void { const start = sheet.rowCount + 1; const titleRow = sheet.addRow([title]); sheet.mergeCells(`A${titleRow.number}:E${titleRow.number}`); titleRow.font = { bold: true }; titleRow.getCell(1).fill = solidFill(XL.headerGrey); const head = sheet.addRow([...this.ageingHeaders]); head.font = { bold: true }; head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const r of rows) { const row = sheet.addRow([ r.period, r.noOfDebitEntries, r.amtOfDebitEntries, r.noOfCreditEntries, r.amtOfCreditEntries ]); row.getCell(3).numFmt = '#,##0.00'; row.getCell(5).numFmt = '#,##0.00'; if (r.period === 'Total') { row.font = { bold: true }; [ 2, 3, 4, 5 ].forEach( (c) => (row.getCell(c).fill = solidFill(XL.derivedBlue)) ); } } boxAllCells(sheet, start, sheet.rowCount, 5); } private buildAnnexure2(wb: ExcelJS.Workbook): void { const sheet = wb.addWorksheet('Annexure 2'); sheet.columns = [ { width: 10 }, { width: 62 }, { width: 20 }, { width: 20 } ]; addBanner(sheet, 'Annexure 2', 'D'); for (const block of this.sundryDebtors) { const start = sheet.rowCount + 1; const t = sheet.addRow([block.title]); sheet.mergeCells(`A${t.number}:D${t.number}`); t.font = { bold: true }; t.getCell(1).fill = solidFill(XL.headerGrey); const s = sheet.addRow([block.subtitle]); sheet.mergeCells(`A${s.number}:D${s.number}`); s.font = { bold: true, size: 10 }; s.getCell(1).fill = solidFill(XL.headerGrey); const head = sheet.addRow(['Sr. No', 'Due from Name', 'Amount due', 'Provision made']); head.font = { bold: true }; head.alignment = { horizontal: 'center' }; head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const r of block.rows) { const row = sheet.addRow([r.srNo, r.dueFromName, r.amountDue, r.provisionMade]); row.getCell(3).numFmt = '#,##0.00'; row.getCell(4).numFmt = '#,##0.00'; } const note = sheet.addRow([this.sundryDebtorsNote]); sheet.mergeCells(`A${note.number}:D${note.number}`); note.font = { bold: true, size: 10 }; note.alignment = { wrapText: true }; boxAllCells(sheet, start, sheet.rowCount, 4); sheet.addRow([]); } // Details of Depositors & Borrowers let start = sheet.rowCount + 1; const dbTitle = sheet.addRow(['Details of Depositors & Borrowers', '', 'Rs. In Thousands']); sheet.mergeCells(`A${dbTitle.number}:B${dbTitle.number}`); dbTitle.font = { bold: true }; dbTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); dbTitle.getCell(3).alignment = { horizontal: 'right' }; for (const line of this.depositorsBorrowers) { const row = sheet.addRow([line.label, '', line.value]); sheet.mergeCells(`A${row.number}:B${row.number}`); row.getCell(1).alignment = { wrapText: true, vertical: 'middle', indent: line.level }; row.getCell(3).numFmt = line.count ? '#,##0' : '#,##0.00'; if (line.total) { row.font = { bold: true }; row.getCell(3).fill = solidFill(XL.derivedBlue); } } boxAllCells(sheet, start, sheet.rowCount, 3); sheet.addRow([]); // Complaints start = sheet.rowCount + 1; const cTitle = sheet.addRow(['Complaints Received from public']); sheet.mergeCells(`A${cTitle.number}:B${cTitle.number}`); cTitle.font = { bold: true }; cTitle.getCell(1).fill = solidFill(XL.headerGrey); for (const line of this.complaints) { const row = sheet.addRow([line.label, '', line.value]); sheet.mergeCells(`A${row.number}:B${row.number}`); row.getCell(1).alignment = { wrapText: true, vertical: 'middle' }; row.getCell(3).numFmt = '#,##0'; } boxAllCells(sheet, start, sheet.rowCount, 3); } private toast(msg: string): void { this.toastMsg = msg; this.toastVisible = true; if (this.toastTimer) { clearTimeout(this.toastTimer); } this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); } }