Files
cbsnexora/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts
T

424 lines
14 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_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<void> {
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);
}
}