424 lines
14 KiB
TypeScript
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);
|
|
}
|
|
}
|