From 3030e2f34eada99924213c2f1aa9ca473d576b87 Mon Sep 17 00:00:00 2001 From: keshav Date: Sun, 2 Aug 2026 11:46:40 +0530 Subject: [PATCH] form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates --- src/app/core/branch-scope.service.ts | 98 + src/app/core/shell/shell.component.html | 62 +- src/app/fiu/fiu-data.ts | 26 +- src/app/fiu/fiu.component.html | 321 ++- src/app/fiu/fiu.component.scss | 199 ++ src/app/fiu/fiu.component.ts | 255 +- .../form-declaration-data.ts | 638 +++++ .../form-declaration-report.component.html | 370 +++ .../form-declaration-report.component.scss | 608 +++++ .../form-declaration-report.component.ts | 474 ++++ .../form-declaration.compute.ts | 296 +++ src/app/gl-reports/gl-reports-data.ts | 263 +- src/app/gl-reports/gl-reports.component.html | 572 +++- src/app/gl-reports/gl-reports.component.scss | 54 + src/app/gl-reports/gl-reports.component.ts | 776 +++++- src/app/gl-reports/gl-reports.compute.ts | 490 +++- .../aml-rules/aml-rules.component.html | 51 +- .../aml-rules/aml-rules.component.scss | 138 +- .../aml-rules/aml-rules.component.ts | 200 +- .../gl-mapping-dialog.component.html | 126 + .../gl-mapping-dialog.component.scss | 358 +++ .../gl-mapping-dialog.component.ts | 121 + src/app/rbi-circulars/gl-mapping.host.ts | 140 + src/app/rbi-circulars/gl-mapping.ts | 170 ++ src/app/rbi-circulars/oss-return.scss | 152 ++ src/app/rbi-circulars/oss-workbook.ts | 61 +- .../rbi-circulars/oss1-ral/oss1-ral-data.ts | 120 +- .../oss1-ral/oss1-ral.component.html | 114 +- .../oss1-ral/oss1-ral.component.ts | 81 +- .../oss2-roe/oss2-roe.component.html | 88 +- .../oss2-roe/oss2-roe.component.ts | 64 +- .../rbi-circulars/oss3-raq/oss3-raq-data.ts | 2297 ++++++++++++++--- .../oss3-raq/oss3-raq.component.html | 99 +- .../oss3-raq/oss3-raq.component.ts | 16 +- src/app/reports/reports-routing.module.ts | 10 + src/app/shared/icons.module.ts | 2 + src/environments/.env.ts | 2 +- 37 files changed, 9292 insertions(+), 620 deletions(-) create mode 100644 src/app/core/branch-scope.service.ts create mode 100644 src/app/form-declarations/form-declaration-data.ts create mode 100644 src/app/form-declarations/form-declaration-report.component.html create mode 100644 src/app/form-declarations/form-declaration-report.component.scss create mode 100644 src/app/form-declarations/form-declaration-report.component.ts create mode 100644 src/app/form-declarations/form-declaration.compute.ts create mode 100644 src/app/rbi-circulars/gl-mapping-dialog.component.html create mode 100644 src/app/rbi-circulars/gl-mapping-dialog.component.scss create mode 100644 src/app/rbi-circulars/gl-mapping-dialog.component.ts create mode 100644 src/app/rbi-circulars/gl-mapping.host.ts create mode 100644 src/app/rbi-circulars/gl-mapping.ts diff --git a/src/app/core/branch-scope.service.ts b/src/app/core/branch-scope.service.ts new file mode 100644 index 000000000..66da9d3d4 --- /dev/null +++ b/src/app/core/branch-scope.service.ts @@ -0,0 +1,98 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { AuthenticationService } from './authentication/authentication.service'; +import { GL_BRANCHES, GlBranch } from 'app/gl-reports/gl-reports-data'; + +/** + * Resolves what branch data the logged-in user is allowed to see. + * + * Head Office sees every branch and can drill into any one of them; a branch login is + * locked to its own book. Previously this logic was copy-pasted into the Cash/Clearing + * Summary and the NPA report with slightly different branch code lists; this service is + * the single place it lives so new reports cannot drift. + * + * The logged-in office is matched to a branch by name, since session credentials carry + * only officeId and officeName. Office names are seeded to match the GL branch names + * exactly (see Liquibase 0442). + */ +@Injectable({ providedIn: 'root' }) +export class BranchScopeService { + private auth = inject(AuthenticationService); + + /** Sentinel used when a branch login cannot be matched to a known branch. */ + static readonly UNRESOLVED = '__unresolved__'; + + get isHeadOffice(): boolean { + return this.auth.isHeadOfficeUser(); + } + + /** The office name on the session, for labelling the scope in report headers. */ + get officeName(): string { + return this.auth.getCredentials()?.officeName || 'Branch Office'; + } + + /** + * Branch code the user is confined to, or null for Head Office. + * + * Deliberately returns the UNRESOLVED sentinel rather than falling back to the first + * branch when the office cannot be matched: showing an unrecognised branch someone + * else's figures would be worse than showing them nothing. + */ + get ownBranchCode(): string | null { + if (this.isHeadOffice) { + return null; + } + const name = this.officeName.toLowerCase().trim(); + const match = GL_BRANCHES.find((b) => b.code !== 'HO' && name === b.name.toLowerCase()); + // Fall back to a substring match for offices named e.g. "Andheri (E) Branch". + const loose = match ?? GL_BRANCHES.find((b) => b.code !== 'HO' && name.includes(b.name.toLowerCase())); + return loose?.code ?? BranchScopeService.UNRESOLVED; + } + + /** True when the user may choose which branch to view. */ + get canSelectBranch(): boolean { + return this.isHeadOffice; + } + + /** Branches offered in a selector — everything for Head Office, nothing for a branch login. */ + get selectableBranches(): GlBranch[] { + return this.isHeadOffice ? GL_BRANCHES : []; + } + + /** Branch code a report should start on. '' means all branches consolidated. */ + get defaultBranchCode(): string { + return this.isHeadOffice ? '' : (this.ownBranchCode ?? ''); + } + + /** + * Forces a requested branch code into what the user is actually allowed to see. + * Call this wherever a branch code reaches a data query, so a stale or hand-edited + * selection cannot widen the user's scope. + */ + enforce(requested: string): string { + return this.isHeadOffice ? requested : (this.ownBranchCode ?? BranchScopeService.UNRESOLVED); + } + + /** True when a branch login's office matched no known branch, so it sees no data. */ + get isUnresolved(): boolean { + return this.ownBranchCode === BranchScopeService.UNRESOLVED; + } + + /** Human label for the current scope, for report headers. */ + scopeLabel(selectedCode: string): string { + if (!this.isHeadOffice) { + return this.officeName; + } + if (!selectedCode) { + return 'All Branches (Consolidated)'; + } + return GL_BRANCHES.find((b) => b.code === selectedCode)?.name ?? selectedCode; + } +} diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 9b169d73e..b3948a6a9 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -579,6 +579,10 @@ + + @@ -347,32 +347,75 @@ CTR - @if (t.complianceStatus === 'SUSPICIOUS') { - + STR + @if (t.complianceStatus === 'REFERRED') { + + STR } - @if (!t.complianceStatus || t.complianceStatus === 'PENDING') { -
- - + @if (t.complianceStatus === 'REFERRED') { + REFERRED TO STR + + } @else if (t.complianceStatus === 'EVIDENCED') { + EVIDENCE ON FILE +
+ {{ t.evidenceDocType }} + @if (t.evidenceReference) { + · {{ t.evidenceReference }} + }
} @else { - - {{ t.complianceStatus }} - - @if (t.complianceStatus === 'WHITELISTED') { -
Stays in CTR only
- } @else { - - } +
+ + +
} + + + @if (evidencingTxn?.txnId === t.txnId) { + + +
+
+ + Record source-of-funds evidence for {{ t.customerName }} — ₹{{ + t.txnAmount | number: '1.0-0' + }} +
+
+
+ + +
+
+ + +
+
+ + +
+
+

+ Recording evidence is a documentation step, not a suspicion verdict. The transaction is reported + in the CTR either way — this simply confirms no STR referral is needed. +

+
+ + + } } @empty { No transactions match the filters. @@ -382,10 +425,11 @@

- CTR every cash txn ≥ threshold stays in the monthly CTR / TC2 - file · - + STR also flagged as suspicious and escalated to a separate STR - case (see the STR tab) + CTR every cash txn ≥ threshold is reported here regardless of + outcome. Record evidence when the customer produces valid documents — CTR only, no STR · + + STR Send to STR when the source of funds cannot + be evidenced; the Principal Officer investigates and decides whether to file with FIU-IND. A referred + transaction stays in the CTR file as well.

} @@ -397,9 +441,7 @@

Live AML Alerts (Awaiting Triage)

-

- Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate -

+

Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate

@@ -468,7 +510,7 @@ → STR escalated to Principal Officer as an STR case

-
+
@@ -627,23 +669,26 @@ } @else if (derivedConclusion === 'NOT_SUSPICIOUS') { - Not suspicious. All {{ strChecklist.length }} points found genuine — the case will - be closed and nothing filed with FIU-IND. + Not suspicious. All {{ strChecklist.length }} points found genuine — the case will be + closed and nothing filed with FIU-IND. } @else { Suspicious — STR must be filed. {{ failedFindings.length }} of {{ strChecklist.length }} points not satisfied: - {{ failedFindings[0].label }}@if (failedFindings.length > 1) { + {{ failedFindings[0].label }} + @if (failedFindings.length > 1) { and {{ failedFindings.length - 1 }} more - }. + } + . }
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') { @@ -675,8 +720,9 @@ + - +
@@ -752,17 +798,198 @@

{{ poFindingLabel(s.poFinding) }}

- } @else { + } + + + @if (s.status === 'awaiting-filing') { +
+ +

+ Generate the FINnet STR XML, upload it on the FIU-IND portal under + {{ bank.principalOfficer }}'s digital signature, then record the acknowledgement number here. +

+
+ + + +
+
+ } + + @if (s.status === 'filed' && s.fiuReferenceNo) { +
+ + + Filed with FIU-IND on {{ s.filedOn }} · acknowledgement + {{ s.fiuReferenceNo }} + +
+ } + + @if (!s.poFinding && investigatingStr?.reportRefNo !== s.reportRefNo) {
-
- - +
+ } + + + @if (investigatingStr?.reportRefNo === s.reportRefNo) { +
+
+ Principal Officer investigation — STR-{{ s.reportRefNo }} +
+ + +
+
1 Documentary evidence called for
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
2 Customer’s explanation of the source
+ +
+ + +
+
+ 3 Verification checklist + + {{ answeredCount }} / {{ strChecklist.length }} answered + +
+ + + + + + + + + + @for (item of strChecklist; track item.key) { + + + + + + } + +
Verification pointGenuineNot satisfied
+
{{ item.label }}
+
{{ item.hint }}
+
+ + + +
+
+ + +
+ @if (!canConcludeLocal) { + + + Complete all three steps to determine the outcome. + @if (!evidenceRecorded) { + Evidence not yet recorded. + } + @if (!allAnswered) { + {{ strChecklist.length - answeredCount }} verification point(s) pending. + } + + } @else if (derivedConclusion === 'NOT_SUSPICIOUS') { + + + Source of funds established. Case closes as clean — the transaction stays in + the CTR report and no STR is filed with FIU-IND. + + } @else { + + + Source of funds not established. {{ failedFindings.length }} point(s) + unsatisfied — an STR will be filed with FIU-IND in addition to the CTR. + + } +
+ +
}
@@ -871,8 +1098,8 @@

FINnet 2.0 Submission Files

- 6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and - STR tabs, so every file in the bundle covers the same transactions. + 6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and STR + tabs, so every file in the bundle covers the same transactions.

+ + + +
+
+
+ @for (m of periodModes; track m.key) { + + } +
+ + @switch (periodMode) { + @case ('day') { + + } + @case ('week') { + + Week: {{ rangeLabel }} + } + @case ('month') { + + } + @default { + + + } + } +
+ +
+ + + + + + + + + + + +
+ + @if (!canSelectBranch) { +
+ + Showing {{ branchLabel }} only. Head Office logins can select any branch. +
+ } +
+ + +
+
+
{{ summary.persons }}
+
Declarants
+
{{ summary.form60Persons }} on Form 97 · {{ summary.form61Persons }} on Form 98
+
+
+
{{ summary.txnCount }}
+
Transactions — all types
+
{{ summary.declarationRequired }} in a Rule 114B category
+
+
+
{{ summary.cashTotal | number: '1.0-0' }}
+
Cash turnover (₹)
+
{{ summary.cashCount }} cash transactions
+
+
+
{{ summary.ctrReportable }}
+
CTR reportable
+
cash at or above ₹10,00,000
+
+
+ + + @if (summary.byGroup.length) { +
+ Transaction mix + @for (g of summary.byGroup; track g.group) { + + } +
+ } + + +
+ + + + + {{ view === 'person' ? personRows.length + ' declarants' : txnRows.length + ' transactions' }} + +
+ + + @if (view === 'person') { +
+ + + + + + + + + + + + + + + + + + + + @for (p of personRows; track p.declarant.ackNo) { + + + + + + + + + + + + + + + + + @if (expanded === p.declarant.ackNo) { + + + + } + } @empty { + + + + } + +
AcknowledgementFormName of declarantAccount NoBranchTxnsRule 114BCash turnoverCreditDebitDeclared incomeCash ÷ income
+ + {{ p.declarant.ackNo }} + + {{ formLabel[p.declarant.formType] }} + + + {{ p.declarant.name }} + @if (p.ctrCount > 0) { + {{ p.ctrCount }} CTR + } + {{ p.declarant.accountNo }}{{ p.declarant.branchCode }}{{ p.txnCount }}{{ p.declaredCount }}{{ p.cashTotal | number: '1.0-0' }}{{ p.totalCredit | number: '1.0-0' }}{{ p.totalDebit | number: '1.0-0' }}{{ p.declarant.estimatedIncome | number: '1.0-0' }} + {{ p.incomeRatio | number: '1.1-1' }}× +
+
+
+ {{ p.declarant.fatherName }} +
+
+ {{ p.declarant.dateOfBirth }} +
+
+ XXXX XXXX {{ p.declarant.aadhaarLast4 }} +
+
+ {{ p.declarant.declaredOn }} +
+
+ {{ p.declarant.address }} +
+
+ {{ p.declarant.reasonForNoPan }} +
+
+ + + + + + + + + + + + + + + + @for (t of p.txns; track t.txnId) { + + + + + + + + + + + } + +
DateTxn IDModeDr/CrAmountNature of transaction (Rule 114B)VoucherFlag
{{ t.date }}{{ t.txnId }} + {{ t.mode }} + {{ t.drCr }}{{ t.amount | number: '1.0-0' }}{{ t.nature }}{{ t.voucherNo }} + @if (t.ctrReportable) { + CTR + } @else if (t.declarationRequired) { + Rule 114B + } +
+
+ No Form 97 or Form 98 transactions for {{ rangeLabel }} at {{ branchLabel }}. +
+
+ } + + + @if (view === 'transaction') { +
+ + + + + + + + + + + + + + + + + + + + + @for (t of txnRows; track t.txnId) { + + + + + + + + + + + + + + + + + } @empty { + + + + } + +
DateTxn IDAcknowledgementFormName of declarantAccount NoBranchCategoryModeDr/CrAmountNature of transactionVoucherFlag
{{ t.date }}{{ t.txnId }}{{ t.ackNo }} + {{ formLabel[t.formType] }} + {{ t.declarantName }}{{ t.accountNo }}{{ t.branchCode }} + {{ t.group }} + + {{ t.mode }} + {{ t.drCr }}{{ t.amount | number: '1.0-0' }}{{ t.nature }}{{ t.voucherNo }} + @if (t.ctrReportable) { + CTR + } @else if (t.declarationRequired) { + Rule 114B + } +
+ No Form 97 or Form 98 transactions for {{ rangeLabel }} at {{ branchLabel }}. +
+
+ } + +
{{ toastMsg }}
+ diff --git a/src/app/form-declarations/form-declaration-report.component.scss b/src/app/form-declarations/form-declaration-report.component.scss new file mode 100644 index 000000000..8df1981a1 --- /dev/null +++ b/src/app/form-declarations/form-declaration-report.component.scss @@ -0,0 +1,608 @@ +/** + * 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/. + */ + +:host { + --f-navy: #1a237e; + --f-blue: #1d4ed8; + --f-blue-l: #eff6ff; + --f-green: #15803d; + --f-green-l: #dcfce7; + --f-amber: #b45309; + --f-amber-l: #fef3c7; + --f-red: #c0392b; + --f-red-l: #fdecea; + --f-g50: #f8fafc; + --f-g100: #f1f5f9; + --f-g200: #e2e8f0; + --f-g300: #cbd5e1; + --f-g500: #64748b; + --f-g700: #334155; + --f-g900: #0f172a; + + display: block; + font-family: 'DM Sans', system-ui, sans-serif; + background: var(--f-g50); + color: var(--f-g900); + font-size: 14px; + line-height: 1.5; +} + +.fd-root { + padding: 18px 22px; + max-width: 1700px; + margin: 0 auto; +} + +/* HEADER */ +.fd-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.fd-head-left { + display: flex; + align-items: flex-start; + gap: 14px; +} + +.fd-head-ic { + font-size: 26px; + color: var(--f-navy); + margin-top: 3px; +} + +.fd-head-t { + margin: 0; + font-size: 18px; + font-weight: 700; +} + +.fd-head-s { + font-size: 12px; + color: var(--f-g500); + max-width: 820px; +} + +/* BUTTONS */ +.fd-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 15px; + border: 1px solid var(--f-g300); + background: #fff; + border-radius: 8px; + font-size: 12.5px; + font-weight: 600; + color: var(--f-g700); + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + + &:hover:not(:disabled) { + background: var(--f-g50); + border-color: var(--f-g500); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + } + + &--primary { + background: var(--f-green); + border-color: var(--f-green); + color: #fff; + + &:hover:not(:disabled) { + background: #12692f; + border-color: #12692f; + color: #fff; + } + } +} + +/* FILTERS */ +.fd-filters { + background: #fff; + border: 1px solid var(--f-g200); + border-radius: 11px; + padding: 12px 16px; + margin-bottom: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.fd-filter-row { + display: flex; + align-items: flex-end; + gap: 12px; + flex-wrap: wrap; +} + +.fd-modes { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.fd-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border: 1px solid var(--f-g300); + background: #fff; + border-radius: 999px; + font-size: 12.5px; + font-weight: 600; + color: var(--f-g700); + cursor: pointer; + font-family: inherit; + + &:hover { + border-color: var(--f-navy); + } + + &.active { + background: var(--f-navy); + border-color: var(--f-navy); + color: #fff; + } +} + +.fd-field { + display: flex; + flex-direction: column; + gap: 3px; + + span { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--f-g500); + } + + input, + select { + padding: 7px 10px; + border: 1px solid var(--f-g300); + border-radius: 7px; + font-size: 13px; + font-family: inherit; + background: #fff; + color: var(--f-g900); + + &:focus { + outline: none; + border-color: var(--f-navy); + } + + &:disabled { + background: var(--f-g100); + color: var(--f-g500); + } + } + + &--grow { + flex: 1; + min-width: 240px; + + input { + width: 100%; + } + } +} + +.fd-resolved { + font-size: 12px; + color: var(--f-g500); + padding-bottom: 8px; +} + +.fd-check { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12.5px; + font-weight: 600; + color: var(--f-g700); + padding-bottom: 8px; + cursor: pointer; +} + +.fd-scope-note { + display: flex; + align-items: center; + gap: 7px; + font-size: 12px; + color: var(--f-amber); + background: var(--f-amber-l); + border: 1px solid #fcd34d; + border-radius: 7px; + padding: 6px 11px; +} + +/* KPIs */ +.fd-kpis { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 14px; +} + +.fd-kpi { + background: #fff; + border: 1px solid var(--f-g200); + border-left: 4px solid var(--f-navy); + border-radius: 11px; + padding: 12px 16px; + + &--blue { + border-left-color: var(--f-blue); + } + + &--amber { + border-left-color: var(--f-amber); + } + + &--red { + border-left-color: var(--f-red); + } +} + +.fd-kpi-n { + font-size: 22px; + font-weight: 700; + font-family: 'DM Mono', monospace; +} + +.fd-kpi-l { + font-size: 12px; + font-weight: 600; + color: var(--f-g700); +} + +.fd-kpi-sub { + font-size: 11px; + color: var(--f-g500); +} + +/* VIEW SWITCH */ +.fd-views { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.fd-spacer { + flex: 1; +} + +.fd-count { + font-size: 12px; + color: var(--f-g500); + font-weight: 600; +} + +/* TABLE */ +.fd-table-wrap { + background: #fff; + border: 1px solid var(--f-g200); + border-radius: 11px; + overflow-x: auto; +} + +.fd-table { + width: 100%; + border-collapse: collapse; + font-size: 12.5px; + + th { + text-align: left; + padding: 9px 10px; + background: var(--f-g100); + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--f-g700); + white-space: nowrap; + border-bottom: 1px solid var(--f-g200); + } + + td { + padding: 8px 10px; + border-bottom: 1px solid var(--f-g100); + vertical-align: top; + } + + .num { + text-align: right; + font-family: 'DM Mono', monospace; + } + + .strong { + font-weight: 700; + } + + tbody tr:hover:not(.fd-detail) { + background: var(--f-g50); + } +} + +.fd-person { + cursor: pointer; + + &.open { + background: var(--f-blue-l); + } +} + +.fd-row--ctr { + background: var(--f-red-l); +} + +.fd-detail { + background: #fcfcfe; + + td { + padding: 12px 16px 16px; + } +} + +.fd-decl { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px 18px; + margin-bottom: 12px; + padding: 10px 14px; + background: #fff; + border: 1px solid var(--f-g200); + border-radius: 8px; + + > div { + display: flex; + flex-direction: column; + } + + .wide { + grid-column: span 2; + } + + label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--f-g500); + } + + span { + font-size: 12.5px; + } +} + +.fd-subtable { + width: 100%; + border-collapse: collapse; + font-size: 12px; + background: #fff; + border: 1px solid var(--f-g200); + border-radius: 8px; + overflow: hidden; + + th { + text-align: left; + padding: 7px 10px; + background: var(--f-g50); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--f-g500); + white-space: nowrap; + } + + td { + padding: 6px 10px; + border-top: 1px solid var(--f-g100); + } + + .num { + text-align: right; + font-family: 'DM Mono', monospace; + } + + .strong { + font-weight: 700; + } +} + +/* Badges */ +.fd-form { + display: inline-block; + padding: 1px 8px; + border-radius: 999px; + font-size: 10.5px; + font-weight: 700; + background: var(--f-blue-l); + color: var(--f-blue); + white-space: nowrap; + + &--61 { + background: var(--f-green-l); + color: var(--f-green); + } +} + +.fd-mode { + display: inline-block; + padding: 1px 7px; + border-radius: 5px; + font-size: 10.5px; + font-weight: 700; + background: var(--f-g100); + color: var(--f-g700); +} + +.fd-group { + display: inline-block; + padding: 1px 8px; + border-radius: 999px; + font-size: 10.5px; + font-weight: 600; + background: #fff; + border: 1px solid var(--f-g300); + color: var(--f-g700); + white-space: nowrap; +} + +/* Category mix — doubles as a one-click filter. */ +.fd-mix { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.fd-mix-t { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--f-g500); + margin-right: 2px; +} + +.fd-mix-item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 11px; + border: 1px solid var(--f-g200); + background: #fff; + border-radius: 7px; + font-size: 11.5px; + font-weight: 600; + color: var(--f-g700); + cursor: pointer; + font-family: inherit; + + strong { + font-family: 'DM Mono', monospace; + color: var(--f-navy); + } + + &:hover { + border-color: var(--f-navy); + } + + &.active { + background: var(--f-navy); + border-color: var(--f-navy); + color: #fff; + + strong { + color: #fff; + } + } +} + +.fd-tag { + display: inline-block; + margin-left: 6px; + padding: 1px 7px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: var(--f-amber-l); + color: var(--f-amber); + white-space: nowrap; + + &--ctr { + background: var(--f-red-l); + color: var(--f-red); + } +} + +.fd-ratio { + font-weight: 600; + + &--mid { + color: var(--f-amber); + } + + &--high { + color: var(--f-red); + font-weight: 700; + } +} + +.cr { + color: var(--f-green); + font-weight: 700; +} + +.dr { + color: var(--f-red); + font-weight: 700; +} + +.mono { + font-family: 'DM Mono', monospace; + font-size: 11.5px; +} + +.fd-empty { + padding: 44px; + text-align: center; + color: var(--f-g500); + font-size: 13px; +} + +/* TOAST */ +.fd-toast { + position: fixed; + bottom: 32px; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: var(--f-navy); + color: #fff; + padding: 12px 22px; + border-radius: 10px; + font-size: 13px; + box-shadow: 0 6px 20px rgb(0 0 0 / 25%); + opacity: 0; + pointer-events: none; + transition: all 0.25s; + z-index: 1500; + + &.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@media (width <= 1200px) { + .fd-kpis { + grid-template-columns: 1fr 1fr; + } + + .fd-decl { + grid-template-columns: 1fr 1fr; + } +} diff --git a/src/app/form-declarations/form-declaration-report.component.ts b/src/app/form-declarations/form-declaration-report.component.ts new file mode 100644 index 000000000..4dc6d517b --- /dev/null +++ b/src/app/form-declarations/form-declaration-report.component.ts @@ -0,0 +1,474 @@ +/** + * 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, OnInit, inject } 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 { BranchScopeService } from '../core/branch-scope.service'; +import { GL_BRANCHES, GL_PERIOD, glBranchName } from '../gl-reports/gl-reports-data'; +import { FORM_LABEL, FORM_THRESHOLD, FormType, TXN_GROUPS } from './form-declaration-data'; +import { + DateRange, + FormPersonRow, + FormReportFilter, + FormReportSummary, + FormTxnRow, + formPersonRows, + formReportSummary, + formTxnRows, + monthRange, + weekRange +} from './form-declaration.compute'; + +type PeriodMode = 'day' | 'week' | 'month' | 'range'; +type ViewMode = 'person' | 'transaction'; + +@Component({ + selector: 'mifosx-form-declaration-report', + templateUrl: './form-declaration-report.component.html', + styleUrls: ['./form-declaration-report.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + FormsModule + ] +}) +export class FormDeclarationReportComponent implements OnInit { + private scope = inject(BranchScopeService); + + readonly period = GL_PERIOD; + readonly branches = GL_BRANCHES; + readonly formLabel = FORM_LABEL; + readonly threshold = FORM_THRESHOLD; + readonly txnGroups = TXN_GROUPS; + + readonly periodModes: { key: PeriodMode; label: string }[] = [ + { key: 'day', label: 'Day wise' }, + { key: 'week', label: 'Week wise' }, + { key: 'month', label: 'Month wise' }, + { key: 'range', label: 'Between dates' } + ]; + + // ── Filter state ─────────────────────────────────────────────────────────── + periodMode: PeriodMode = 'month'; + filterDay = '2026-07-31'; + /** Any date inside the wanted week; resolved to Mon–Sat. */ + filterWeek = '2026-07-27'; + filterMonth = '2026-07'; + filterFrom = GL_PERIOD.fyStart; + filterTo = GL_PERIOD.fyEnd; + branchCode = ''; + formType: FormType | '' = ''; + group = ''; + search = ''; + flaggedOnly = false; + + view: ViewMode = 'person'; + /** Acknowledgement number of the person row currently expanded. */ + expanded: string | null = null; + + generating = false; + toastMsg = ''; + toastVisible = false; + private toastTimer: any; + + ngOnInit(): void { + // A branch login opens on its own book and cannot move off it. + this.branchCode = this.scope.defaultBranchCode; + } + + // ── Filter resolution ────────────────────────────────────────────────────── + + get range(): DateRange { + if (this.periodMode === 'day') { + return { from: this.filterDay, to: this.filterDay }; + } + if (this.periodMode === 'week') { + return weekRange(this.filterWeek); + } + if (this.periodMode === 'month') { + return monthRange(this.filterMonth); + } + return { from: this.filterFrom, to: this.filterTo }; + } + + get rangeLabel(): string { + const r = this.range; + return r.from === r.to ? r.from : `${r.from} to ${r.to}`; + } + + get canSelectBranch(): boolean { + return this.scope.canSelectBranch; + } + + get branchLabel(): string { + if (!this.canSelectBranch) { + return this.scope.officeName; + } + return this.branchCode ? glBranchName(this.branchCode) : 'All Branches (Consolidated)'; + } + + /** + * Applied at the data boundary rather than only in the UI, so a branch login + * cannot widen its own scope by tampering with component state. + */ + private get effectiveBranch(): string { + return this.scope.enforce(this.branchCode); + } + + private get filter(): FormReportFilter { + return { + range: this.range, + branchCode: this.effectiveBranch, + formType: this.formType, + group: this.group, + search: this.search, + flaggedOnly: this.flaggedOnly + }; + } + + resetFilters(): void { + this.periodMode = 'month'; + this.filterDay = '2026-07-31'; + this.filterWeek = '2026-07-27'; + this.filterMonth = '2026-07'; + this.filterFrom = GL_PERIOD.fyStart; + this.filterTo = GL_PERIOD.fyEnd; + this.formType = ''; + this.group = ''; + this.search = ''; + this.flaggedOnly = false; + this.expanded = null; + // Reset returns a branch login to its own branch, never to "all". + this.branchCode = this.scope.defaultBranchCode; + } + + // ── Data ─────────────────────────────────────────────────────────────────── + + get summary(): FormReportSummary { + return formReportSummary(this.filter); + } + + get personRows(): FormPersonRow[] { + return formPersonRows(this.filter); + } + + get txnRows(): FormTxnRow[] { + return formTxnRows(this.filter); + } + + toggle(ackNo: string): void { + this.expanded = this.expanded === ackNo ? null : ackNo; + } + + /** Cash turnover far above declared income is the point of the report. */ + ratioClass(ratio: number): string { + if (ratio >= 5) { + return 'fd-ratio fd-ratio--high'; + } + if (ratio >= 2) { + return 'fd-ratio fd-ratio--mid'; + } + return 'fd-ratio'; + } + + // ── Excel ────────────────────────────────────────────────────────────────── + + async exportExcel(): Promise { + this.generating = true; + try { + const wb = new ExcelJS.Workbook(); + wb.creator = 'CBSNexora'; + wb.created = new Date(); + + this.writeSummarySheet(wb); + this.writePersonSheet(wb); + this.writeTransactionSheet(wb); + + const buffer = await wb.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `Form-97-98-Transaction-Report-${this.range.from}-to-${this.range.to}.xlsx`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + this.toast(`Exported ${this.summary.txnCount} transactions for ${this.rangeLabel}`); + } catch { + this.toast('Could not generate the workbook — please retry'); + } finally { + this.generating = false; + } + } + + /** Title band + filter caption, repeated on every sheet so pages stand alone. */ + private banner(sheet: ExcelJS.Worksheet, title: string, cols: number): void { + const t = sheet.addRow([title]); + sheet.mergeCells(t.number, 1, t.number, cols); + t.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } }; + t.alignment = { horizontal: 'center', vertical: 'middle' }; + t.height = 24; + t.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A237E' } }; + + const formCaption = this.formType ? FORM_LABEL[this.formType] : 'Form 97 and Form 98'; + const meta = sheet.addRow([ + `Period: ${this.rangeLabel} · Branch: ${this.branchLabel} · Declaration: ${formCaption}` + + ` · Category: ${this.group || 'All transaction types'}` + ]); + sheet.mergeCells(meta.number, 1, meta.number, cols); + meta.font = { italic: true, size: 10 }; + sheet.addRow([]); + } + + private headerRow(sheet: ExcelJS.Worksheet, headers: string[]): void { + const head = sheet.addRow(headers); + head.font = { bold: true }; + head.alignment = { wrapText: true, horizontal: 'center' }; + head.eachCell((c) => { + c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } }; + c.border = { bottom: { style: 'thin' } }; + }); + } + + private writeSummarySheet(wb: ExcelJS.Workbook): void { + const s = this.summary; + const sheet = wb.addWorksheet('Summary'); + sheet.columns = [ + { width: 52 }, + { width: 24 }]; + this.banner(sheet, 'Form 97 / 98 Transaction Report — Summary', 2); + this.headerRow(sheet, [ + 'Particulars', + 'Value' + ]); + + const money = new Set([ + 'Total cash turnover', + 'Total credits', + 'Total debits' + ]); + + const lines: [string, string | number][] = [ + [ + 'Declarants transacting in the period', + s.persons + ], + [ + ' of which Form 97 (no PAN)', + s.form60Persons + ], + [ + ' of which Form 98 (agricultural income only)', + s.form61Persons + ], + [ + 'Total transactions — all types', + s.txnCount + ], + [ + ' of which cash (counter and ATM)', + s.cashCount + ], + [ + ' of which in a Rule 114B category', + s.declarationRequired + ], + [ + 'Total cash turnover', + s.cashTotal + ], + [ + 'Total credits', + s.totalCredit + ], + [ + 'Total debits', + s.totalDebit + ], + [ + `Cash transactions at or above ₹${FORM_THRESHOLD.CTR.toLocaleString('en-IN')} (CTR reportable)`, + s.ctrReportable + ] + ]; + + for (const [ + label, + value + ] of lines) { + const row = sheet.addRow([ + label, + value + ]); + if (money.has(label)) { + row.getCell(2).numFmt = '#,##0.00'; + } + } + + // Category mix — what the customer group actually does, beyond the + // declared transactions. + sheet.addRow([]); + this.headerRow(sheet, [ + 'Transaction category', + 'Count' + ]); + for (const g of s.byGroup) { + sheet.addRow([ + g.group, + g.count + ]); + } + sheet.views = [{ state: 'frozen', ySplit: 4 }]; + } + + private writePersonSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Person wise'); + const widths = [ + 18, + 12, + 26, + 24, + 18, + 14, + 10, + 10, + 10, + 16, + 16, + 16, + 14, + 10, + 34 + ]; + sheet.columns = widths.map((w) => ({ width: w })); + this.banner(sheet, 'Form 97 / 98 Transaction Report — Person wise', widths.length); + this.headerRow(sheet, [ + 'Acknowledgement', + 'Form', + 'Name of declarant', + "Father's name", + 'Account No', + 'Branch', + 'Total txns', + 'Cash txns', + 'Rule 114B txns', + 'Cash turnover', + 'Total credit', + 'Total debit', + 'Declared income', + 'CTR rows', + 'Categories transacted' + ]); + + for (const p of this.personRows) { + const row = sheet.addRow([ + p.declarant.ackNo, + FORM_LABEL[p.declarant.formType], + p.declarant.name, + p.declarant.fatherName, + p.declarant.accountNo, + glBranchName(p.declarant.branchCode), + p.txnCount, + p.cashCount, + p.declaredCount, + p.cashTotal, + p.totalCredit, + p.totalDebit, + p.declarant.estimatedIncome, + p.ctrCount, + p.groups.join(', ') + ]); + for (const c of [ + 10, + 11, + 12, + 13 + ]) { + row.getCell(c).numFmt = '#,##0.00'; + } + } + sheet.views = [{ state: 'frozen', ySplit: 4 }]; + } + + private writeTransactionSheet(wb: ExcelJS.Workbook): void { + const sheet = wb.addWorksheet('Transactions'); + const widths = [ + 12, + 12, + 18, + 12, + 26, + 18, + 14, + 12, + 10, + 6, + 16, + 46, + 16, + 12 + ]; + sheet.columns = widths.map((w) => ({ width: w })); + this.banner(sheet, 'Form 97 / 98 Transaction Report — Transaction wise', widths.length); + this.headerRow(sheet, [ + 'Date', + 'Txn ID', + 'Acknowledgement', + 'Form', + 'Name of declarant', + 'Account No', + 'Branch', + 'Category', + 'Mode', + 'Dr/Cr', + 'Amount', + 'Nature of transaction', + 'Voucher No', + 'Flag' + ]); + + for (const t of this.txnRows) { + const row = sheet.addRow([ + t.date, + t.txnId, + t.ackNo, + FORM_LABEL[t.formType], + t.declarantName, + t.accountNo, + glBranchName(t.branchCode), + t.group, + t.mode, + t.drCr, + t.amount, + t.nature, + t.voucherNo, + t.ctrReportable ? 'CTR' : t.declarationRequired ? 'Rule 114B' : '' + ]); + row.getCell(11).numFmt = '#,##0.00'; + if (t.ctrReportable) { + row.getCell(14).font = { bold: true, color: { argb: 'FFC0392B' } }; + } + } + sheet.views = [{ state: 'frozen', ySplit: 4 }]; + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/form-declarations/form-declaration.compute.ts b/src/app/form-declarations/form-declaration.compute.ts new file mode 100644 index 000000000..f5aecf016 --- /dev/null +++ b/src/app/form-declarations/form-declaration.compute.ts @@ -0,0 +1,296 @@ +/** + * 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 { GL_PERIOD } from '../gl-reports/gl-reports-data'; +import { + FORM_THRESHOLD, + FormDeclarant, + FormDeclarantTxn, + FormType, + formDeclarants, + formDeclarantTxns +} from './form-declaration-data'; + +export interface DateRange { + from: string; + to: string; +} + +export interface FormReportFilter { + range: DateRange; + /** '' means every branch. */ + branchCode: string; + /** '' means both declaration types. */ + formType: FormType | ''; + /** '' means every transaction category (Cash, Clearing, Loan, …). */ + group: string; + /** Free text over name, account number and acknowledgement number. */ + search: string; + /** Show only rows a compliance officer would need to look at. */ + flaggedOnly: boolean; +} + +/** One transaction, joined to the declarant who filed for it. */ +export interface FormTxnRow extends FormDeclarantTxn { + declarantName: string; + formType: FormType; + fatherName: string; + declaredOn: string; + reasonForNoPan: string; + estimatedIncome: number; + /** Counter or ATM cash, as opposed to a book or electronic entry. */ + isCash: boolean; + /** + * The transaction is one of the categories in Rule 114B, so PAN — and in its + * absence the Form 97/98 declaration — was mandatory for it. + */ + declarationRequired: boolean; + /** Cash at or above the CTR limit — also lands in the monthly CTR. */ + ctrReportable: boolean; +} + +/** A declarant with their totals for the period. */ +export interface FormPersonRow { + declarant: FormDeclarant; + txns: FormTxnRow[]; + txnCount: number; + cashCount: number; + totalCredit: number; + totalDebit: number; + cashTotal: number; + /** Largest single transaction, used to sort the riskiest to the top. */ + maxAmount: number; + /** How many of the period's transactions needed the declaration. */ + declaredCount: number; + ctrCount: number; + /** Distinct categories the customer transacted in during the period. */ + groups: string[]; + /** + * Aggregate cash for the period against the income the customer declared. + * A ratio well above 1 is the classic "turnover inconsistent with declared + * income" flag, which is why the declaration is collected in the first place. + */ + incomeRatio: number; +} + +export interface FormReportSummary { + persons: number; + form60Persons: number; + form61Persons: number; + txnCount: number; + cashTotal: number; + cashCount: number; + totalCredit: number; + totalDebit: number; + /** Transactions falling in a Rule 114B category. */ + declarationRequired: number; + ctrReportable: number; + /** Transaction count by category, largest first — drives the mix panel. */ + byGroup: { group: string; count: number; amount: number }[]; +} + +function clamp(range: DateRange): DateRange { + const from = range.from < GL_PERIOD.fyStart ? GL_PERIOD.fyStart : range.from; + const to = range.to > GL_PERIOD.fyEnd ? GL_PERIOD.fyEnd : range.to; + // A backwards range would silently return nothing; collapse it to a single day. + return from > to ? { from: to, to: to } : { from, to }; +} + +/** Monday-to-Saturday banking week containing the given date. */ +export function weekRange(anchor: string): DateRange { + const d = new Date(`${anchor}T00:00:00Z`); + const dow = d.getUTCDay(); // 0 = Sunday + const monday = new Date(d.getTime()); + monday.setUTCDate(d.getUTCDate() - (dow === 0 ? 6 : dow - 1)); + const saturday = new Date(monday.getTime()); + saturday.setUTCDate(monday.getUTCDate() + 5); + const iso = (x: Date) => x.toISOString().slice(0, 10); + return { from: iso(monday), to: iso(saturday) }; +} + +export function monthRange(month: string): DateRange { + const [ + y, + m + ] = month.split('-').map(Number); + const last = new Date(Date.UTC(y, m, 0)).getUTCDate(); + return { from: `${month}-01`, to: `${month}-${`${last}`.padStart(2, '0')}` }; +} + +/** + * Transactions matching the filter, each joined to its declarant. + * + * Memoised on the filter signature: the getters that feed the template are read + * many times per change-detection pass, and re-scanning several hundred rows on + * every read is what made the GL reports stall. + */ +let rowCacheKey = ''; +let rowCache: FormTxnRow[] = []; + +export function formTxnRows(filter: FormReportFilter): FormTxnRow[] { + const range = clamp(filter.range); + const key = [ + range.from, + range.to, + filter.branchCode, + filter.formType, + filter.group, + filter.search.trim().toLowerCase(), + filter.flaggedOnly ? '1' : '0' + ].join('|'); + + if (key === rowCacheKey) { + return rowCache; + } + + const byAck = new Map( + formDeclarants().map((d) => [ + d.ackNo, + d + ]) + ); + const needle = filter.search.trim().toLowerCase(); + + const rows: FormTxnRow[] = []; + for (const t of formDeclarantTxns()) { + if (t.date < range.from || t.date > range.to) { + continue; + } + if (filter.branchCode && t.branchCode !== filter.branchCode) { + continue; + } + if (filter.group && t.group !== filter.group) { + continue; + } + const d = byAck.get(t.ackNo); + if (!d) { + continue; + } + if (filter.formType && d.formType !== filter.formType) { + continue; + } + if ( + needle && + !d.name.toLowerCase().includes(needle) && + !t.accountNo.toLowerCase().includes(needle) && + !d.ackNo.toLowerCase().includes(needle) + ) { + continue; + } + + // ATM dispensing is cash in the customer's hands, so it counts towards the + // cash view even though it never crossed the counter. + const isCash = t.mode === 'CASH' || t.mode === 'ATM'; + const ctrReportable = isCash && t.amount >= FORM_THRESHOLD.CTR; + + if (filter.flaggedOnly && !ctrReportable) { + continue; + } + + rows.push({ + ...t, + declarantName: d.name, + formType: d.formType, + fatherName: d.fatherName, + declaredOn: d.declaredOn, + reasonForNoPan: d.reasonForNoPan, + estimatedIncome: d.estimatedIncome, + isCash, + declarationRequired: t.rule114B, + ctrReportable + }); + } + + rowCacheKey = key; + rowCache = rows; + return rows; +} + +/** The same rows grouped by declarant, heaviest customer first. */ +export function formPersonRows(filter: FormReportFilter): FormPersonRow[] { + const byAck = new Map( + formDeclarants().map((d) => [ + d.ackNo, + d + ]) + ); + const grouped = new Map(); + + for (const r of formTxnRows(filter)) { + const list = grouped.get(r.ackNo); + if (list) { + list.push(r); + } else { + grouped.set(r.ackNo, [r]); + } + } + + const out: FormPersonRow[] = []; + for (const [ + ackNo, + txns + ] of grouped) { + const declarant = byAck.get(ackNo); + if (!declarant) { + continue; + } + const cash = txns.filter((t) => t.isCash); + const cashTotal = cash.reduce((s, t) => s + t.amount, 0); + out.push({ + declarant, + txns, + txnCount: txns.length, + cashCount: cash.length, + totalCredit: txns.filter((t) => t.drCr === 'CR').reduce((s, t) => s + t.amount, 0), + totalDebit: txns.filter((t) => t.drCr === 'DR').reduce((s, t) => s + t.amount, 0), + cashTotal, + maxAmount: txns.reduce((m, t) => Math.max(m, t.amount), 0), + declaredCount: txns.filter((t) => t.declarationRequired).length, + ctrCount: txns.filter((t) => t.ctrReportable).length, + groups: [...new Set(txns.map((t) => t.group))].sort(), + incomeRatio: declarant.estimatedIncome > 0 ? cashTotal / declarant.estimatedIncome : 0 + }); + } + + out.sort((a, b) => b.cashTotal - a.cashTotal); + return out; +} + +export function formReportSummary(filter: FormReportFilter): FormReportSummary { + const rows = formTxnRows(filter); + const acks = new Set(rows.map((r) => r.ackNo)); + const form61 = new Set(rows.filter((r) => r.formType === 'FORM_61').map((r) => r.ackNo)); + const form60 = new Set(rows.filter((r) => r.formType === 'FORM_60').map((r) => r.ackNo)); + + const groupMap = new Map(); + for (const r of rows) { + const g = groupMap.get(r.group); + if (g) { + g.count++; + g.amount += r.amount; + } else { + groupMap.set(r.group, { group: r.group, count: 1, amount: r.amount }); + } + } + + const cash = rows.filter((r) => r.isCash); + + return { + persons: acks.size, + form60Persons: form60.size, + form61Persons: form61.size, + txnCount: rows.length, + cashTotal: cash.reduce((s, r) => s + r.amount, 0), + cashCount: cash.length, + totalCredit: rows.filter((r) => r.drCr === 'CR').reduce((s, r) => s + r.amount, 0), + totalDebit: rows.filter((r) => r.drCr === 'DR').reduce((s, r) => s + r.amount, 0), + declarationRequired: rows.filter((r) => r.declarationRequired).length, + ctrReportable: rows.filter((r) => r.ctrReportable).length, + byGroup: [...groupMap.values()].sort((a, b) => b.count - a.count) + }; +} diff --git a/src/app/gl-reports/gl-reports-data.ts b/src/app/gl-reports/gl-reports-data.ts index f61efa8c2..24ed45741 100644 --- a/src/app/gl-reports/gl-reports-data.ts +++ b/src/app/gl-reports/gl-reports-data.ts @@ -114,18 +114,84 @@ export const GL_ACCOUNTS: GlAccount[] = [ { code: '420103', name: "Auditors' Fees", type: 'EXPENSE', group: 'Operating Expenses' }, { code: '420104', name: 'Rent, Taxes & Lighting', type: 'EXPENSE', group: 'Operating Expenses' }, { code: '420105', name: 'Depreciation on Bank Property', type: 'EXPENSE', group: 'Operating Expenses' }, - { code: '430101', name: 'Printing & Stationery', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430102', name: 'Postage, Telegram & Telephone', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430103', name: 'Advertisement & Publicity', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430104', name: 'Repairs & Maintenance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430105', name: 'Travelling & Conveyance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430106', name: 'Computer & Software Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430107', name: 'Legal & Professional Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430108', name: 'Insurance (DICGC & Other)', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430109', name: 'Bank Charges & Commission Paid', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430110', name: 'AGM & Meeting Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { + code: '430101', + name: 'Printing & Stationery', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430102', + name: 'Postage, Telegram & Telephone', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430103', + name: 'Advertisement & Publicity', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430104', + name: 'Repairs & Maintenance', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430105', + name: 'Travelling & Conveyance', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430106', + name: 'Computer & Software Expenses', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430107', + name: 'Legal & Professional Charges', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430108', + name: 'Insurance (DICGC & Other)', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430109', + name: 'Bank Charges & Commission Paid', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, + { + code: '430110', + name: 'AGM & Meeting Expenses', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, { code: '430111', name: 'Electricity Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, - { code: '430112', name: 'Other Miscellaneous Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true }, + { + code: '430112', + name: 'Other Miscellaneous Expenses', + type: 'EXPENSE', + group: 'Miscellaneous Expenses', + miscExpense: true + }, // ── Contingent (memoranda, not part of the balance sheet totals) ────────── { code: '910101', name: 'Guarantees Issued — Financial', type: 'CONTINGENT', group: 'Guarantees' }, @@ -261,8 +327,20 @@ const VOUCHER_TEMPLATES: { { debit: '110101', credit: '320102', narration: 'Locker rent collected', scale: 9_000, frequency: 0.3 }, { debit: '110101', credit: '320103', narration: 'Loan processing fees', scale: 18_000, frequency: 0.5 }, { debit: '110101', credit: '320105', narration: 'Miscellaneous income', scale: 7_500, frequency: 0.4 }, - { debit: '410101', credit: '230101', narration: 'Interest provided on savings deposits', scale: 128_000, frequency: 0.8 }, - { debit: '410102', credit: '230101', narration: 'Interest provided on term deposits', scale: 265_000, frequency: 0.9 }, + { + debit: '410101', + credit: '230101', + narration: 'Interest provided on savings deposits', + scale: 128_000, + frequency: 0.8 + }, + { + debit: '410102', + credit: '230101', + narration: 'Interest provided on term deposits', + scale: 265_000, + frequency: 0.9 + }, { debit: '420101', credit: '110101', narration: 'Salaries and allowances paid', scale: 210_000, frequency: 0.35 }, { debit: '420104', credit: '110101', narration: 'Rent, taxes and lighting', scale: 46_000, frequency: 0.3 }, { debit: '430101', credit: '110101', narration: 'Printing and stationery purchased', scale: 9_400, frequency: 0.3 }, @@ -275,14 +353,32 @@ const VOUCHER_TEMPLATES: { { debit: '430109', credit: '110101', narration: 'Bank charges and commission paid', scale: 4_800, frequency: 0.3 }, { debit: '430111', credit: '110101', narration: 'Electricity charges', scale: 13_600, frequency: 0.25 }, { debit: '430112', credit: '110101', narration: 'Other miscellaneous expenses', scale: 8_900, frequency: 0.3 }, - { debit: '110102', credit: '110101', narration: 'Cash remitted to RBI currency chest', scale: 1_800_000, frequency: 0.2 }, - { debit: '110101', credit: '110103', narration: 'Funds drawn from correspondent bank', scale: 1_400_000, frequency: 0.2 } + { + debit: '110102', + credit: '110101', + narration: 'Cash remitted to RBI currency chest', + scale: 1_800_000, + frequency: 0.2 + }, + { + debit: '110101', + credit: '110103', + narration: 'Funds drawn from correspondent bank', + scale: 1_400_000, + frequency: 0.2 + } ]; /** Head-Office-only vouchers — investments, reserves and centralised charges. */ const HO_TEMPLATES: typeof VOUCHER_TEMPLATES = [ { debit: '120101', credit: '110102', narration: 'Purchase of Govt securities', scale: 2_400_000, frequency: 0.12 }, - { debit: '120102', credit: '110102', narration: 'Purchase of State Govt securities', scale: 1_900_000, frequency: 0.12 }, + { + debit: '120102', + credit: '110102', + narration: 'Purchase of State Govt securities', + scale: 1_900_000, + frequency: 0.12 + }, { debit: '110102', credit: '320104', narration: 'Profit on sale of securities', scale: 78_000, frequency: 0.1 }, { debit: '420102', credit: '110101', narration: "Directors' sitting fees", scale: 32_000, frequency: 0.08 }, { debit: '420103', credit: '110101', narration: "Statutory auditors' fees", scale: 45_000, frequency: 0.06 }, @@ -342,7 +438,10 @@ export function glPostings(): GlPosting[] { let openingDebit = 0; let openingCredit = 0; - for (const [code, amount] of Object.entries(OPENING_BALANCES)) { + for (const [ + code, + amount + ] of Object.entries(OPENING_BALANCES)) { if (!amount) { continue; } @@ -425,7 +524,10 @@ export function glPostings(): GlPosting[] { for (const branch of GL_BRANCHES) { // Head Office posts fewer counter transactions than the branches. const weight = branch.code === 'HO' ? 0.45 : 1; - const templates = branch.code === 'HO' ? [...VOUCHER_TEMPLATES, ...HO_TEMPLATES] : VOUCHER_TEMPLATES; + const templates = branch.code === 'HO' ? [ + ...VOUCHER_TEMPLATES, + ...HO_TEMPLATES + ] : VOUCHER_TEMPLATES; for (const tpl of templates) { if (rand() > tpl.frequency * weight) { @@ -487,6 +589,131 @@ export function glPostings(): GlPosting[] { return out; } +/** + * Customer sub-ledger. + * + * The GL is account-level, so a client-wise report needs a customer dimension on top. + * Each customer holds a share of one GL head at one branch; balances are not stored, + * they are derived at query time as `share x that head's GL balance`. That way the + * client-wise report always reconciles to the General Ledger exactly, whatever period + * or branch is selected, instead of drifting as a second source of truth. + */ +export interface GlCustomer { + cif: string; + name: string; + branchCode: string; + /** GL head this relationship sits under. */ + accountCode: string; + accountNo: string; + /** Fraction of the branch's balance in that head. Shares per (branch, head) sum to 1. */ + share: number; +} + +const FIRST_NAMES = [ + 'Ravi', + 'Priya', + 'Vikram', + 'Nirav', + 'Kavita', + 'Arjun', + 'Lakshmi', + 'Sameer', + 'Anjali', + 'Rohan', + 'Meera', + 'Karan', + 'Divya', + 'Sanjay', + 'Pooja', + 'Amit', + 'Sneha', + 'Manish', + 'Rekha', + 'Deepak' +]; +const LAST_NAMES = [ + 'Sharma', + 'Patel', + 'Reddy', + 'Shah', + 'Verma', + 'Deshmukh', + 'Iyer', + 'Kapoor', + 'Nair', + 'Joshi', + 'Mehta', + 'Gupta', + 'Rao', + 'Bhatt', + 'Chauhan' +]; +const FIRMS = [ + 'Traders', + 'Enterprises', + 'Agencies', + '& Sons', + 'Industries', + 'Stores', + 'Corporation' +]; + +/** Customer-facing heads a client relationship can sit under. */ +const CUSTOMER_HEADS = [ + '220101', + '220102', + '220103', + '220104', + '130101', + '130102', + '130103', + '130104', + '130105', + '130106' +]; + +let cachedCustomers: GlCustomer[] | null = null; + +export function glCustomers(): GlCustomer[] { + if (cachedCustomers) { + return cachedCustomers; + } + const rand = lcg(76543210); + const out: GlCustomer[] = []; + let seq = 1; + + for (const branch of GL_BRANCHES) { + for (const head of CUSTOMER_HEADS) { + // Deposits are held by many customers; advances by fewer, larger ones. + const isAdvance = head.startsWith('13'); + const count = isAdvance ? 2 + Math.floor(rand() * 3) : 3 + Math.floor(rand() * 4); + const weights: number[] = []; + for (let i = 0; i < count; i++) { + weights.push(0.35 + rand() * 0.65); + } + const weightTotal = weights.reduce((a, b) => a + b, 0); + + for (let i = 0; i < count; i++) { + const corporate = isAdvance && rand() > 0.55; + const name = corporate + ? `${LAST_NAMES[Math.floor(rand() * LAST_NAMES.length)]} ${FIRMS[Math.floor(rand() * FIRMS.length)]}` + : `${FIRST_NAMES[Math.floor(rand() * FIRST_NAMES.length)]} ${LAST_NAMES[Math.floor(rand() * LAST_NAMES.length)]}`; + out.push({ + cif: `CIF${`${100000 + seq}`}`, + name, + branchCode: branch.code, + accountCode: head, + accountNo: `${branch.code === 'HO' ? '0001' : branch.code}${head}${`${seq}`.padStart(4, '0')}`, + share: weights[i] / weightTotal + }); + seq++; + } + } + } + cachedCustomers = out; + return out; +} + export function glAccount(code: string): GlAccount | undefined { return GL_ACCOUNTS.find((a) => a.code === code); } diff --git a/src/app/gl-reports/gl-reports.component.html b/src/app/gl-reports/gl-reports.component.html index da982ebc5..1e4277b3c 100644 --- a/src/app/gl-reports/gl-reports.component.html +++ b/src/app/gl-reports/gl-reports.component.html @@ -79,17 +79,33 @@
- + @if (canSelectBranch) { + + } @else { + + {{ scopeName }} + + }
+ @if (scopeIsUnresolved) { +
+ + + Your office {{ scopeName }} does not match any branch in the ledger, so no figures can be + shown. Ask an administrator to align your office name with a branch. + +
+ } +
@for (r of reports; track r.code) { @@ -193,7 +209,9 @@ @for (r of pnlExpense; track r.code) { {{ r.code }} - {{ r.name }} · {{ r.group }} + + {{ r.name }} · {{ r.group }} + {{ r.amount | number: '1.2-2' }} } @empty { @@ -229,7 +247,9 @@ @for (r of pnlIncome; track r.code) { {{ r.code }} - {{ r.name }} · {{ r.group }} + + {{ r.name }} · {{ r.group }} + {{ r.amount | number: '1.2-2' }} } @empty { @@ -281,7 +301,9 @@ @for (r of bsLiabilities; track r.code) { {{ r.code }} - {{ r.name }} · {{ r.group }} + + {{ r.name }} · {{ r.group }} + {{ r.amount | number: '1.2-2' }} } @@ -312,7 +334,9 @@ @for (r of bsAssets; track r.code) { {{ r.code }} - {{ r.name }} · {{ r.group }} + + {{ r.name }} · {{ r.group }} + {{ r.amount | number: '1.2-2' }} } @@ -331,11 +355,17 @@ @if (activeReport === 'comparative-balance-sheet') {

- Compares the closing position as at {{ range.to }} against the position immediately before - this period began. Change the filter above to compare a different span. + Compares the closing position as at {{ range.to }} against the position immediately before this + period began. Change the filter above to compare a different span.

- @for (block of [{ title: 'Capital & Liabilities', rows: compLiabilities }, { title: 'Assets', rows: compAssets }]; track block.title) { + @for ( + block of [ + { title: 'Capital & Liabilities', rows: compLiabilities }, + { title: 'Assets', rows: compAssets } + ]; + track block.title + ) {
{{ block.title }}
@@ -353,7 +383,9 @@ @for (r of block.rows; track r.code) { - +
{{ r.code }}{{ r.name }} · {{ r.group }} + {{ r.name }} · {{ r.group }} + {{ r.current | number: '1.2-2' }} {{ r.previous | number: '1.2-2' }} @@ -379,8 +411,8 @@ @if (activeReport === 'contingent-liabilities') {

- Off-balance-sheet commitments outstanding as at {{ range.to }}. These are memoranda accounts - and are deliberately excluded from the Trial Balance and Balance Sheet totals. + Off-balance-sheet commitments outstanding as at {{ range.to }}. These are memoranda accounts and are deliberately excluded from the Trial Balance and Balance Sheet totals.

@@ -419,8 +451,8 @@ @if (activeReport === 'gl-wise-dr-cr') {

- Debit and credit turnover posted between {{ rangeLabel }} for {{ branchLabel }}. Every - voucher is double-entry, so the debit and credit totals below must match. + Debit and credit turnover posted between {{ rangeLabel }} for {{ branchLabel }}. Every voucher + is double-entry, so the debit and credit totals below must match.

@@ -474,8 +506,8 @@ @if (activeReport === 'misc-expense-ledger') {

- Every miscellaneous-expense voucher posted between {{ rangeLabel }} for {{ branchLabel }} - — {{ miscRows.length | number }} entries. + Every miscellaneous-expense voucher posted between {{ rangeLabel }} for + {{ branchLabel }} — {{ miscRows.length | number }} entries.

@@ -518,6 +550,506 @@ } + + @if (activeReport === 'gl-average-balance') { +
+

+ Average of each account's closing balance across every day in + {{ rangeLabel }} for {{ branchLabel }}. Banks work from average balances rather than a closing + snapshot for interest, CRR/SLR and profitability, because a single day's figure can be dressed up. +

+
+
+ + + + + + + + + + + + + + + @for (r of averageRows; track r.code) { + + + + + + + + + + + + } @empty { + + + + } + + + + + + + + +
GL CodeAccountGroupOpeningClosingAverage DailyPeakLowestActive Days
{{ r.code }}{{ r.name }}{{ r.group }}{{ r.opening | number: '1.2-2' }}{{ r.closing | number: '1.2-2' }} + {{ r.average | number: '1.2-2' }} + {{ r.peak | number: '1.2-2' }}{{ r.lowest | number: '1.2-2' }}{{ r.activeDays }}
No ledger activity for this period and branch.
TOTAL ({{ averageRows.length }} accounts){{ avgTotalAverage | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'gl-mapping') { +
+

+ Which General Ledger accounts feed each RBI return line. Mappings are created on the OSS 1, OSS 2 and OSS 3 + screens using Add / Remove GLs; this is the consolidated view of them, with the value each + mapping currently produces from the ledger. +

+ +
+
Mapped return lines ({{ mappingRows.length }})
+ + + + + + + + + + + @for (r of mappingRows; track r.returnCode + r.lineKey) { + + + + + + + } @empty { + + + + } + +
ReturnReturn lineMapped GL accountsLedger value (₹ thousands)
{{ r.returnLabel }}{{ r.lineKey }} + @for (c of r.codes; track c) { + {{ c }} + } + {{ r.value | number: '1.2-2' }}
+ No return lines mapped yet. Open OSS 1, 2 or 3 and use “Add / Remove GLs” to pin accounts to a line. +
+
+ +
+
Unmapped GL accounts ({{ unmappedAccounts.length }}) — not feeding any return line
+ + + + + + + + + + @for (a of unmappedAccounts; track a.code) { + + + + + + } @empty { + + + + } + +
GL CodeAccountGroup
{{ a.code }}{{ a.name }}{{ a.group }}
Every GL account feeds at least one return line.
+
+
+ } + + + @if (activeReport === 'account-wise-balance') { +
+

+ Closing balance of every account as at {{ range.to }}, broken out by branch — which branch is actually carrying each balance. +

+
+ + + + + + + @for (c of branchColumns; track c) { + + } + + + + + @for (r of accountWiseRows; track r.code) { + + + + + @for (c of branchColumns; track c) { + + } + + + } @empty { + + + + } + + + + + @for (c of branchColumns; track c) { + + } + + + +
GL CodeAccountGroup{{ branchName(c) }}Total
{{ r.code }}{{ r.name }}{{ r.group }}{{ r.byBranch[c] ? (r.byBranch[c] | number: '1.2-2') : '' }} + {{ r.total | number: '1.2-2' }} +
+ No balances for this period and branch. +
TOTAL ({{ accountWiseRows.length }} accounts){{ branchColumnTotal(c) | number: '1.2-2' }}{{ accountWiseGrandTotal | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'distribution-of-balance') { +
+

+ Composition of the book as at {{ range.to }} for {{ branchLabel }} — how concentrated each side + of the balance sheet is by account group. +

+ +
+ @for ( + block of [ + { title: 'Capital & Liabilities', rows: liabilityDistribution, total: liabilityDistTotal }, + { title: 'Assets', rows: assetDistribution, total: assetDistTotal } + ]; + track block.title + ) { +
+
{{ block.title }}
+ + + + + + + + + + + + @for (r of block.rows; track r.group) { + + + + + + + + } @empty { + + + + } + + + + + + + +
GroupA/csAmount (₹)ShareDistribution
{{ r.group }}{{ r.accounts }}{{ r.amount | number: '1.2-2' }} + {{ r.share | number: '1.2-2' }}% + +
+
+
+
No balances on this side.
Total{{ block.total | number: '1.2-2' }}100.00%
+
+ } +
+
+ } + + + @if (activeReport === 'client-wise-balance') { +
+
+
+
₹{{ clientDeposits | number: '1.2-2' }}
+
Customer Deposits
+
+
+
₹{{ clientAdvances | number: '1.2-2' }}
+
Customer Advances
+
+
+
{{ clientRows.length | number }}
+
Relationships
+
as at {{ range.to }} · {{ branchLabel }}
+
+
+ +

+ Customer balances are derived from the General Ledger — each relationship holds a share of its branch's balance + in that head — so this report always reconciles to the GL rather than drifting as a second source. +

+ +
+ + + + + + + + + + + + + + @for (r of clientRows; track r.cif) { + + + + + + + + + + } @empty { + + + + } + +
CIFCustomerBranchAccount NoProductSideBalance (₹)
{{ r.cif }}{{ r.name }}{{ branchName(r.branchCode) }}{{ r.accountNo }}{{ r.product }} + + {{ r.side }} + + {{ r.balance | number: '1.2-2' }}
No customer relationships for this period and branch.
+
+
+ } + + + @if (activeReport === 'banker-head') { +
+

+ Head-wise abstract for {{ rangeLabel }} — the trial balance rolled up to principal heads, with + opening, turnover and closing for each. Debit and credit turnover must agree. +

+
+ + + + + + + + + + + + + @for (r of bankerHeadRows; track r.head) { + + + + + + + + + } @empty { + + + + } + + + + + + + + +
Principal HeadAccountsOpeningDebitCreditClosing
+ {{ r.head }} + {{ r.accounts }}{{ r.opening | number: '1.2-2' }}{{ r.debit ? (r.debit | number: '1.2-2') : '' }}{{ r.credit ? (r.credit | number: '1.2-2') : '' }} + {{ r.closing | number: '1.2-2' }} +
No activity for this period and branch.
TOTAL ({{ bankerHeadRows.length }} heads){{ headDebitTotal | number: '1.2-2' }}{{ headCreditTotal | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'sundry-register') { +
+

+ Outstanding items in the sundry, suspense and bills-payable heads as at {{ range.to }}, aged from their posting date. These heads are where unreconciled entries collect, so ageing is the point of + the register. +

+ + @if (sundryBuckets.length) { +
+ @for (b of sundryBuckets; track b.bucket) { +
+
₹{{ b.amount | number: '1.2-2' }}
+
{{ b.bucket }}
+
{{ b.count }} item(s)
+
+ } +
+ } + +
+ + + + + + + + + + + + + + + @for (r of sundryRows; track r.voucherNo + r.code + r.date) { + + + + + + + + + + + } @empty { + + + + } + + + + + + + + +
DateVoucher NoBranchGL CodeAccountNarrationAmount (₹)Age
{{ r.date }}{{ r.voucherNo }}{{ branchName(r.branchCode) }}{{ r.code }}{{ r.name }}{{ r.narration }}{{ r.amount | number: '1.2-2' }} + + {{ r.ageDays }}d + +
No sundry or suspense items outstanding for this selection.
Total outstanding ({{ sundryRows.length }} items){{ sundryTotal | number: '1.2-2' }}
+
+
+ } + + + @if (activeReport === 'voucher-register') { +
+
+ + + {{ voucherRows.length | number }} voucher(s) raised in {{ rangeLabel }} for {{ branchLabel }} — + {{ + unbalancedVouchers + ? unbalancedVouchers + ' voucher(s) do NOT balance and must be investigated' + : 'every voucher is double-entry and balances' + }} + +
+ +
+ + + + + + + + + + + + + + + @for (r of voucherRows; track r.voucherNo) { + + + + + + + + + + + } @empty { + + + + } + + + + + + + + +
DateVoucher NoBranchNarrationDebit account(s)Credit account(s)Amount (₹)Legs
{{ r.date }}{{ r.voucherNo }}{{ branchName(r.branchCode) }}{{ r.narration }}{{ r.debitAccounts }}{{ r.creditAccounts }}{{ r.amount | number: '1.2-2' }}{{ r.legs }}
No vouchers raised in this period for this branch.
Total ({{ voucherRows.length }} vouchers){{ voucherTotal | number: '1.2-2' }}
+
+
+ } +
{{ toastMsg }}
diff --git a/src/app/gl-reports/gl-reports.component.scss b/src/app/gl-reports/gl-reports.component.scss index ac1f6bdc9..b5249a0c9 100644 --- a/src/app/gl-reports/gl-reports.component.scss +++ b/src/app/gl-reports/gl-reports.component.scss @@ -160,6 +160,21 @@ } } +/* Read-only branch shown to a branch-level login in place of the selector. */ +.glr-chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 14px; + min-width: 190px; + border: 1px solid var(--g-g200); + border-radius: 8px; + background: var(--g-g100); + font-size: 13px; + font-weight: 600; + color: var(--g-g700); +} + /* Day / Month / Range segmented control */ .glr-seg { display: inline-flex; @@ -434,6 +449,45 @@ font-style: italic; } +/* A voucher whose debits and credits disagree. */ +.glr-unbalanced td { + background: var(--g-red-l) !important; + color: #7f1d1d; + font-weight: 600; +} + +/* GL code chips on the mapping report */ +.glr-gl-chip { + display: inline-block; + padding: 1px 8px; + margin: 1px 3px 1px 0; + border-radius: 999px; + background: #e0f2fe; + color: #075985; + font-size: 11px; + font-weight: 600; +} + +/* Share bars on the distribution report */ +.glr-bar-col { + width: 150px; +} + +.glr-bar { + height: 9px; + border-radius: 999px; + background: var(--g-g100); + overflow: hidden; + min-width: 110px; +} + +.glr-bar-fill { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--g-navy), var(--g-blue)); + transition: width 0.25s; +} + /* Account-type pills on the GL Wise report */ .type-pill { display: inline-block; diff --git a/src/app/gl-reports/gl-reports.component.ts b/src/app/gl-reports/gl-reports.component.ts index c719c52d5..a2ca62596 100644 --- a/src/app/gl-reports/gl-reports.component.ts +++ b/src/app/gl-reports/gl-reports.component.ts @@ -6,24 +6,42 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; +import { BranchScopeService } from 'app/core/branch-scope.service'; 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 { GL_BRANCHES, GL_PERIOD, glBranchName } from './gl-reports-data'; +import { GL_ACCOUNTS, GL_BRANCHES, GL_PERIOD, glBranchName } from './gl-reports-data'; import { + AccountBranchRow, + AverageBalanceRow, BalanceSheetRow, ComparativeRow, + DistributionRow, GlDateRange, GlReportSet, MiscExpenseRow, PnlRow, + BankerHeadRow, + ClientBalanceRow, + SundryItemRow, + VoucherRow, + accountWiseByBranch, + averageBalances, + balanceDistribution, + bankerHeadSummary, + clientWiseBalances, + sundryRegister, + voucherRegister, buildReports, clampToLedger, + glValueForCodes, sum } from './gl-reports.compute'; +import { GlMappingEntry, allMappings, glLabel, returnName } from 'app/rbi-circulars/gl-mapping'; export type GlReportCode = | 'trial-balance' @@ -32,7 +50,15 @@ export type GlReportCode = | 'comparative-balance-sheet' | 'contingent-liabilities' | 'gl-wise-dr-cr' - | 'misc-expense-ledger'; + | 'misc-expense-ledger' + | 'gl-average-balance' + | 'gl-mapping' + | 'account-wise-balance' + | 'distribution-of-balance' + | 'client-wise-balance' + | 'banker-head' + | 'sundry-register' + | 'voucher-register'; type PeriodMode = 'day' | 'month' | 'range'; @@ -49,6 +75,19 @@ type PeriodMode = 'day' | 'month' | 'range'; export class GlReportsComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); + private scope = inject(BranchScopeService); + private destroyRef = inject(DestroyRef); + + /** Head Office may pick any branch; a branch login is locked to its own book. */ + get canSelectBranch(): boolean { + return this.scope.canSelectBranch; + } + get scopeIsUnresolved(): boolean { + return this.scope.isUnresolved; + } + get scopeName(): string { + return this.scope.scopeLabel(this.branchCode); + } readonly reports: { code: GlReportCode; label: string; icon: string }[] = [ { code: 'trial-balance', label: 'Trial Balance', icon: 'balance-scale' }, @@ -57,7 +96,15 @@ export class GlReportsComponent implements OnInit { { code: 'comparative-balance-sheet', label: 'Comparative Balance Sheet', icon: 'balance-scale' }, { code: 'contingent-liabilities', label: 'Contingent Liabilities', icon: 'exclamation-circle' }, { code: 'gl-wise-dr-cr', label: 'GL Wise DR / CR', icon: 'project-diagram' }, - { code: 'misc-expense-ledger', label: 'Misc Expense Ledger', icon: 'receipt' } + { code: 'misc-expense-ledger', label: 'Misc Expense Ledger', icon: 'receipt' }, + { code: 'gl-average-balance', label: 'GL Average Balance', icon: 'calculator' }, + { code: 'gl-mapping', label: 'GL Mapping for Report', icon: 'project-diagram' }, + { code: 'account-wise-balance', label: 'Account wise Balance List', icon: 'list' }, + { code: 'distribution-of-balance', label: 'Distribution of Balance', icon: 'chart-pie' }, + { code: 'client-wise-balance', label: 'Client wise Balance', icon: 'users' }, + { code: 'banker-head', label: 'Banker Head Report', icon: 'university' }, + { code: 'sundry-register', label: 'Sundry Register', icon: 'clipboard-list' }, + { code: 'voucher-register', label: 'Voucher Register', icon: 'receipt' } ]; activeReport: GlReportCode = 'trial-balance'; @@ -80,16 +127,29 @@ export class GlReportsComponent implements OnInit { private toastTimer: any; ngOnInit(): void { - const param = this.route.snapshot.paramMap.get('report') as GlReportCode | null; - if (param && this.reports.some((r) => r.code === param)) { - this.activeReport = param; - } + // Subscribe rather than read the snapshot: navigating between GL menu items only + // changes the route parameter, so Angular reuses this component and ngOnInit would + // never fire again — leaving the previously selected report on screen. + this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { + const param = params.get('report') as GlReportCode | null; + if (param && this.reports.some((r) => r.code === param)) { + this.activeReport = param; + } + }); + // A branch login opens on its own book and cannot move off it. + this.branchCode = this.scope.defaultBranchCode; } showReport(code: GlReportCode): void { this.activeReport = code; // Keep the URL deep-linkable so the GL menu items land on the right report. - this.router.navigate(['/gl/reports', code], { replaceUrl: true }); + this.router.navigate( + [ + '/gl/reports', + code + ], + { replaceUrl: true } + ); } get reportLabel(): string { @@ -120,21 +180,33 @@ export class GlReportsComponent implements OnInit { } get branchLabel(): string { + if (!this.canSelectBranch) { + return this.scopeName; + } return this.branchCode ? glBranchName(this.branchCode) : 'All Branches (Consolidated)'; } + /** + * The branch actually used for every query. Applied at the data boundary rather than + * only in the UI so a branch login cannot widen its scope by tampering with state. + */ + private get effectiveBranch(): string { + return this.scope.enforce(this.branchCode); + } + resetFilters(): void { this.periodMode = 'month'; this.filterMonth = '2026-07'; this.filterDay = '2026-07-31'; this.filterFrom = GL_PERIOD.fyStart; this.filterTo = GL_PERIOD.fyEnd; - this.branchCode = ''; + // Reset returns a branch login to its own branch, never to "all". + this.branchCode = this.scope.defaultBranchCode; } /** All seven reports for the current filter — memoised inside buildReports. */ get data(): GlReportSet { - return buildReports(this.range, this.branchCode); + return buildReports(this.range, this.effectiveBranch); } // ── Trial Balance ────────────────────────────────────────────────────────── @@ -234,6 +306,139 @@ export class GlReportsComponent implements OnInit { return glBranchName(code); } + // ── GL Average Balance ───────────────────────────────────────────────────── + get averageRows(): AverageBalanceRow[] { + return averageBalances(this.range, this.effectiveBranch); + } + get avgTotalAverage(): number { + return sum(this.averageRows, (r) => r.average); + } + + // ── GL Mapping for Report ────────────────────────────────────────────────── + /** Every RBI return line currently pinned to GL accounts, with its ledger value. */ + get mappingRows(): { + returnCode: string; + returnLabel: string; + lineKey: string; + codes: string[]; + codeLabels: string; + value: number; + }[] { + return allMappings() + .map((m: GlMappingEntry) => ({ + returnCode: m.returnCode, + returnLabel: returnName(m.returnCode), + lineKey: m.lineKey, + codes: m.codes, + codeLabels: m.codes.map((c) => glLabel(c)).join(', '), + // Returns are in thousands; the ledger is in rupees. + value: glValueForCodes(m.codes, this.range, this.effectiveBranch) / 1000 + })) + .sort((a, b) => a.returnCode.localeCompare(b.returnCode) || a.lineKey.localeCompare(b.lineKey)); + } + + /** GL accounts not pinned to any return line — the gap a mapping review looks for. */ + get unmappedAccounts(): { code: string; name: string; group: string }[] { + const used = new Set(allMappings().flatMap((m) => m.codes)); + return this.chartOfAccounts + .filter((a) => !used.has(a.code)) + .map((a) => ({ code: a.code, name: a.name, group: a.group })); + } + + readonly chartOfAccounts = GL_ACCOUNTS; + + // ── Account wise Balance List ────────────────────────────────────────────── + /** Branch columns shown — every branch for Head Office, just their own otherwise. */ + get branchColumns(): string[] { + return this.canSelectBranch ? GL_BRANCHES.map((b) => b.code) : [this.effectiveBranch]; + } + get accountWiseRows(): AccountBranchRow[] { + return accountWiseByBranch(this.range, this.branchColumns); + } + branchColumnTotal(code: string): number { + return sum(this.accountWiseRows, (r) => r.byBranch[code] ?? 0); + } + get accountWiseGrandTotal(): number { + return sum(this.accountWiseRows, (r) => r.total); + } + + // ── Client wise Balance ──────────────────────────────────────────────────── + get clientRows(): ClientBalanceRow[] { + return clientWiseBalances(this.range, this.effectiveBranch); + } + get clientDeposits(): number { + return sum( + this.clientRows.filter((r) => r.side === 'DEPOSIT'), + (r) => r.balance + ); + } + get clientAdvances(): number { + return sum( + this.clientRows.filter((r) => r.side === 'ADVANCE'), + (r) => r.balance + ); + } + + // ── Banker Head Report ───────────────────────────────────────────────────── + get bankerHeadRows(): BankerHeadRow[] { + return bankerHeadSummary(this.range, this.effectiveBranch); + } + get headDebitTotal(): number { + return sum(this.bankerHeadRows, (r) => r.debit); + } + get headCreditTotal(): number { + return sum(this.bankerHeadRows, (r) => r.credit); + } + + // ── Sundry Register ──────────────────────────────────────────────────────── + get sundryRows(): SundryItemRow[] { + return sundryRegister(this.range, this.effectiveBranch); + } + get sundryTotal(): number { + return sum(this.sundryRows, (r) => r.amount); + } + /** Ageing summary shown above the register. */ + get sundryBuckets(): { bucket: string; count: number; amount: number }[] { + const order = [ + 'Up to 30 days', + '31 to 90 days', + '91 to 180 days', + 'Over 180 days' + ]; + return order + .map((bucket) => { + const rows = this.sundryRows.filter((r) => r.bucket === bucket); + return { bucket, count: rows.length, amount: sum(rows, (r) => r.amount) }; + }) + .filter((b) => b.count > 0); + } + + // ── Voucher Register ─────────────────────────────────────────────────────── + get voucherRows(): VoucherRow[] { + return voucherRegister(this.range, this.effectiveBranch); + } + get voucherTotal(): number { + return sum(this.voucherRows, (r) => r.amount); + } + /** Vouchers whose debits and credits do not agree — should always be zero. */ + get unbalancedVouchers(): number { + return this.voucherRows.filter((r) => !r.balanced).length; + } + + // ── Distribution of Balance ──────────────────────────────────────────────── + get assetDistribution(): DistributionRow[] { + return balanceDistribution(this.range, this.effectiveBranch, 'ASSET'); + } + get liabilityDistribution(): DistributionRow[] { + return balanceDistribution(this.range, this.effectiveBranch, 'LIABILITY'); + } + get assetDistTotal(): number { + return sum(this.assetDistribution, (r) => r.amount); + } + get liabilityDistTotal(): number { + return sum(this.liabilityDistribution, (r) => r.amount); + } + // ── Excel export ─────────────────────────────────────────────────────────── /** Exports whichever report is on screen, carrying the active filter into the header. */ @@ -314,7 +519,17 @@ export class GlReportsComponent implements OnInit { 'Closing Dr', 'Closing Cr' ], - widths: [12, 42, 26, 16, 16, 16, 16, 16, 16], + widths: [ + 12, + 42, + 26, + 16, + 16, + 16, + 16, + 16, + 16 + ], rows: [ ...this.data.trialBalance.map((r) => [ r.code, @@ -349,13 +564,49 @@ export class GlReportsComponent implements OnInit { 'Group', 'Amount' ], - widths: [14, 12, 42, 26, 18], + widths: [ + 14, + 12, + 42, + 26, + 18 + ], rows: [ - ...this.pnlExpense.map((r) => ['Expenditure', r.code, r.name, r.group, r.amount]), - ['Total Expenditure', '', '', '', this.totalExpense], - ...this.pnlIncome.map((r) => ['Income', r.code, r.name, r.group, r.amount]), - ['Total Income', '', '', '', this.totalIncome], - ['Net Profit', '', '', '', this.netProfit] + ...this.pnlExpense.map((r) => [ + 'Expenditure', + r.code, + r.name, + r.group, + r.amount + ]), + [ + 'Total Expenditure', + '', + '', + '', + this.totalExpense + ], + ...this.pnlIncome.map((r) => [ + 'Income', + r.code, + r.name, + r.group, + r.amount + ]), + [ + 'Total Income', + '', + '', + '', + this.totalIncome + ], + [ + 'Net Profit', + '', + '', + '', + this.netProfit + ] ] }; case 'balance-sheet': @@ -367,13 +618,49 @@ export class GlReportsComponent implements OnInit { 'Group', 'Amount' ], - widths: [14, 12, 42, 26, 18], + widths: [ + 14, + 12, + 42, + 26, + 18 + ], rows: [ - ...this.bsLiabilities.map((r) => ['Liability', r.code, r.name, r.group, r.amount]), - ['Liability', '', 'Surplus for the period', 'Profit & Loss', this.bsSurplus], - ['Total Liabilities', '', '', '', this.bsTotalLiabilities], - ...this.bsAssets.map((r) => ['Asset', r.code, r.name, r.group, r.amount]), - ['Total Assets', '', '', '', this.bsTotalAssets] + ...this.bsLiabilities.map((r) => [ + 'Liability', + r.code, + r.name, + r.group, + r.amount + ]), + [ + 'Liability', + '', + 'Surplus for the period', + 'Profit & Loss', + this.bsSurplus + ], + [ + 'Total Liabilities', + '', + '', + '', + this.bsTotalLiabilities + ], + ...this.bsAssets.map((r) => [ + 'Asset', + r.code, + r.name, + r.group, + r.amount + ]), + [ + 'Total Assets', + '', + '', + '', + this.bsTotalAssets + ] ] }; case 'comparative-balance-sheet': @@ -387,7 +674,15 @@ export class GlReportsComponent implements OnInit { 'Variance', 'Variance %' ], - widths: [12, 12, 42, 18, 18, 18, 14], + widths: [ + 12, + 12, + 42, + 18, + 18, + 18, + 14 + ], rows: this.data.comparative.map((r) => [ r.side, r.code, @@ -406,10 +701,25 @@ export class GlReportsComponent implements OnInit { 'Group', 'Amount' ], - widths: [12, 48, 26, 18], + widths: [ + 12, + 48, + 26, + 18 + ], rows: [ - ...this.data.contingent.map((r) => [r.code, r.name, r.group, r.amount]), - ['TOTAL', '', '', this.contingentTotal] + ...this.data.contingent.map((r) => [ + r.code, + r.name, + r.group, + r.amount + ]), + [ + 'TOTAL', + '', + '', + this.contingentTotal + ] ] }; case 'gl-wise-dr-cr': @@ -424,10 +734,394 @@ export class GlReportsComponent implements OnInit { 'Net (Dr-Cr)', 'Vouchers' ], - widths: [12, 42, 14, 26, 18, 18, 18, 12], + widths: [ + 12, + 42, + 14, + 26, + 18, + 18, + 18, + 12 + ], rows: [ - ...this.data.glWise.map((r) => [r.code, r.name, r.type, r.group, r.debit, r.credit, r.net, r.vouchers]), - ['TOTAL', '', '', '', this.glWiseDebit, this.glWiseCredit, this.glWiseDebit - this.glWiseCredit, null] + ...this.data.glWise.map((r) => [ + r.code, + r.name, + r.type, + r.group, + r.debit, + r.credit, + r.net, + r.vouchers + ]), + [ + 'TOTAL', + '', + '', + '', + this.glWiseDebit, + this.glWiseCredit, + this.glWiseDebit - this.glWiseCredit, + null + ] + ] + }; + case 'gl-average-balance': + return { + headers: [ + 'GL Code', + 'Account', + 'Group', + 'Opening', + 'Closing', + 'Average Daily Balance', + 'Peak', + 'Lowest', + 'Active Days' + ], + widths: [ + 12, + 42, + 26, + 18, + 18, + 22, + 18, + 18, + 12 + ], + rows: [ + ...this.averageRows.map((r) => [ + r.code, + r.name, + r.group, + r.opening, + r.closing, + r.average, + r.peak, + r.lowest, + r.activeDays + ]), + [ + 'TOTAL', + '', + '', + null, + null, + this.avgTotalAverage, + null, + null, + null + ] + ] + }; + case 'gl-mapping': + return { + headers: [ + 'Return', + 'Return Line', + 'Mapped GL Accounts', + 'Ledger Value (Rs Thousands)' + ], + widths: [ + 42, + 46, + 60, + 24 + ], + rows: [ + ...this.mappingRows.map((r) => [ + r.returnLabel, + r.lineKey, + r.codeLabels, + r.value + ]), + ...(this.mappingRows.length ? [] : [[ + '— no return lines mapped —', + '', + '', + null + ]]), + [ + '', + '', + '', + null + ], + [ + 'UNMAPPED GL ACCOUNTS', + '', + '', + null + ], + ...this.unmappedAccounts.map((a) => [ + '', + a.code + ' — ' + a.name, + a.group, + null + ]) + ] + }; + case 'account-wise-balance': + return { + headers: [ + 'GL Code', + 'Account', + 'Group', + ...this.branchColumns.map((c) => this.branchName(c)), + 'Total' + ], + widths: [ + 12, + 40, + 24, + ...this.branchColumns.map(() => 18), + 20 + ], + rows: [ + ...this.accountWiseRows.map((r) => [ + r.code, + r.name, + r.group, + ...this.branchColumns.map((c) => r.byBranch[c] ?? 0), + r.total + ]), + [ + 'TOTAL', + '', + '', + ...this.branchColumns.map((c) => this.branchColumnTotal(c)), + this.accountWiseGrandTotal + ] + ] + }; + case 'distribution-of-balance': + return { + headers: [ + 'Side', + 'Group', + 'Accounts', + 'Amount', + 'Share %' + ], + widths: [ + 16, + 40, + 14, + 22, + 14 + ], + rows: [ + ...this.liabilityDistribution.map((r) => [ + 'Liability', + r.group, + r.accounts, + r.amount, + Number(r.share.toFixed(2)) + ]), + [ + 'Total Liabilities', + '', + null, + this.liabilityDistTotal, + 100 + ], + ...this.assetDistribution.map((r) => [ + 'Asset', + r.group, + r.accounts, + r.amount, + Number(r.share.toFixed(2)) + ]), + [ + 'Total Assets', + '', + null, + this.assetDistTotal, + 100 + ] + ] + }; + case 'client-wise-balance': + return { + headers: [ + 'CIF', + 'Customer', + 'Branch', + 'Account No', + 'Product', + 'Side', + 'Balance' + ], + widths: [ + 14, + 34, + 18, + 24, + 34, + 12, + 20 + ], + rows: [ + ...this.clientRows.map((r) => [ + r.cif, + r.name, + this.branchName(r.branchCode), + r.accountNo, + r.product, + r.side, + r.balance + ]), + [ + 'TOTAL DEPOSITS', + '', + '', + '', + '', + '', + this.clientDeposits + ], + [ + 'TOTAL ADVANCES', + '', + '', + '', + '', + '', + this.clientAdvances + ] + ] + }; + case 'banker-head': + return { + headers: [ + 'Principal Head', + 'Accounts', + 'Opening', + 'Debit', + 'Credit', + 'Closing' + ], + widths: [ + 34, + 12, + 20, + 20, + 20, + 20 + ], + rows: [ + ...this.bankerHeadRows.map((r) => [ + r.head, + r.accounts, + r.opening, + r.debit, + r.credit, + r.closing + ]), + [ + 'TOTAL', + null, + null, + this.headDebitTotal, + this.headCreditTotal, + null + ] + ] + }; + case 'sundry-register': + return { + headers: [ + 'Date', + 'Voucher No', + 'Branch', + 'GL Code', + 'Account', + 'Narration', + 'Amount', + 'Age (days)', + 'Ageing Bucket' + ], + widths: [ + 14, + 20, + 18, + 12, + 32, + 42, + 18, + 12, + 18 + ], + rows: [ + ...this.sundryRows.map((r) => [ + r.date, + r.voucherNo, + this.branchName(r.branchCode), + r.code, + r.name, + r.narration, + r.amount, + r.ageDays, + r.bucket + ]), + [ + 'TOTAL', + '', + '', + '', + '', + '', + this.sundryTotal, + null, + '' + ] + ] + }; + case 'voucher-register': + return { + headers: [ + 'Date', + 'Voucher No', + 'Branch', + 'Narration', + 'Debit Account(s)', + 'Credit Account(s)', + 'Amount', + 'Legs' + ], + widths: [ + 14, + 20, + 18, + 42, + 38, + 38, + 20, + 10 + ], + rows: [ + ...this.voucherRows.map((r) => [ + r.date, + r.voucherNo, + this.branchName(r.branchCode), + r.narration, + r.debitAccounts, + r.creditAccounts, + r.amount, + r.legs + ]), + [ + 'TOTAL', + '', + '', + '', + '', + '', + this.voucherTotal, + null + ] ] }; case 'misc-expense-ledger': @@ -442,7 +1136,15 @@ export class GlReportsComponent implements OnInit { 'Narration', 'Amount' ], - widths: [14, 20, 18, 12, 36, 46, 18], + widths: [ + 14, + 20, + 18, + 12, + 36, + 46, + 18 + ], rows: [ ...this.miscRows.map((r) => [ r.date, @@ -453,7 +1155,15 @@ export class GlReportsComponent implements OnInit { r.narration, r.amount ]), - ['TOTAL', '', '', '', '', '', this.miscTotal] + [ + 'TOTAL', + '', + '', + '', + '', + '', + this.miscTotal + ] ] }; } diff --git a/src/app/gl-reports/gl-reports.compute.ts b/src/app/gl-reports/gl-reports.compute.ts index 705d7fcaf..e33f221a2 100644 --- a/src/app/gl-reports/gl-reports.compute.ts +++ b/src/app/gl-reports/gl-reports.compute.ts @@ -15,14 +15,7 @@ * postings and Angular re-evaluates template getters on every change detection. */ -import { - GL_ACCOUNTS, - GL_PERIOD, - GlAccount, - GlPosting, - glAccount, - glPostings -} from './gl-reports-data'; +import { GL_ACCOUNTS, GL_PERIOD, GlAccount, GlPosting, glAccount, glCustomers, glPostings } from './gl-reports-data'; export interface GlDateRange { from: string; @@ -96,8 +89,22 @@ function matchesBranch(p: GlPosting, branchCode: string): boolean { return !branchCode || p.branchCode === branchCode; } +/** + * Both scans below walk the whole ~21k-posting ledger, so their results are cached + * per (date, branch). Angular re-evaluates template getters on every change-detection + * pass; without this a screen with a few hundred mapped lines would spend seconds per + * click rescanning the ledger and appear frozen. + */ +const balanceCache = new Map>(); +const turnoverCache = new Map }>>(); + /** Signed balance on the account's natural side, for postings up to and including `to`. */ function cumulativeBalances(to: string, branchCode: string): Map { + const key = `${to}|${branchCode}`; + const hit = balanceCache.get(key); + if (hit) { + return hit; + } const out = new Map(); for (const p of glPostings()) { if (p.date > to || !matchesBranch(p, branchCode)) { @@ -110,11 +117,20 @@ function cumulativeBalances(to: string, branchCode: string): Map const delta = isDebitSide(account) ? p.debit - p.credit : p.credit - p.debit; out.set(p.accountCode, (out.get(p.accountCode) ?? 0) + delta); } + balanceCache.set(key, out); return out; } /** Debit and credit turnover strictly inside the range. */ -function periodTurnover(range: GlDateRange, branchCode: string): Map }> { +function periodTurnover( + range: GlDateRange, + branchCode: string +): Map }> { + const key = `${range.from}|${range.to}|${branchCode}`; + const hit = turnoverCache.get(key); + if (hit) { + return hit; + } const out = new Map }>(); for (const p of glPostings()) { if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) { @@ -129,6 +145,7 @@ function periodTurnover(range: GlDateRange, branchCode: string): Map (a.date === b.date ? a.voucherNo.localeCompare(b.voucherNo) : a.date.localeCompare(b.date))); + return rows.sort((a, b) => + a.date === b.date ? a.voucherNo.localeCompare(b.voucherNo) : a.date.localeCompare(b.date) + ); +} + +export interface AverageBalanceRow { + code: string; + name: string; + group: string; + type: string; + opening: number; + closing: number; + average: number; + peak: number; + lowest: number; + /** Days on which the account moved. */ + activeDays: number; +} + +export interface AccountBranchRow { + code: string; + name: string; + group: string; + /** Closing balance per branch code, plus a `total`. */ + byBranch: Record; + total: number; +} + +export interface DistributionRow { + group: string; + amount: number; + share: number; + accounts: number; +} + +/** Calendar days in the range, inclusive. */ +function daysInRange(range: GlDateRange): string[] { + const out: string[] = []; + const cur = new Date(`${range.from}T00:00:00Z`); + const end = new Date(`${range.to}T00:00:00Z`); + while (cur <= end) { + out.push(cur.toISOString().slice(0, 10)); + cur.setUTCDate(cur.getUTCDate() + 1); + } + return out; +} + +/** + * Average daily balance per account across the period, plus the peak and lowest + * balance touched. Banks use average balances rather than closing snapshots for + * interest, CRR/SLR and profitability work, because a closing figure can be + * window-dressed on the last day. + * + * Computed by carrying a running balance forward day by day rather than + * recomputing a cumulative balance per day, which would be quadratic. + */ +export function averageBalances(range: GlDateRange, branchCode: string): AverageBalanceRow[] { + const opening = cumulativeBalances(previousDay(range.from), branchCode); + const days = daysInRange(range); + + // Net movement per account per day, on the account's natural side. + const dailyDelta = new Map>(); + const movedOn = new Map>(); + for (const p of glPostings()) { + if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) { + continue; + } + const account = glAccount(p.accountCode); + if (!account) { + continue; + } + const delta = isDebitSide(account) ? p.debit - p.credit : p.credit - p.debit; + let perDay = dailyDelta.get(p.accountCode); + if (!perDay) { + perDay = new Map(); + dailyDelta.set(p.accountCode, perDay); + } + perDay.set(p.date, (perDay.get(p.date) ?? 0) + delta); + let seen = movedOn.get(p.accountCode); + if (!seen) { + seen = new Set(); + movedOn.set(p.accountCode, seen); + } + seen.add(p.date); + } + + const rows: AverageBalanceRow[] = []; + for (const account of GL_ACCOUNTS) { + const open = opening.get(account.code) ?? 0; + const perDay = dailyDelta.get(account.code); + if (!open && !perDay) { + continue; + } + let running = open; + let total = 0; + let peak = open; + let lowest = open; + for (const d of days) { + running += perDay?.get(d) ?? 0; + total += running; + peak = Math.max(peak, running); + lowest = Math.min(lowest, running); + } + rows.push({ + code: account.code, + name: account.name, + group: account.group, + type: account.type, + opening: open, + closing: running, + average: days.length ? total / days.length : 0, + peak, + lowest, + activeDays: movedOn.get(account.code)?.size ?? 0 + }); + } + return rows; +} + +/** + * Closing balance of every account broken out by branch. Answers "which branch is + * carrying this balance", which a single consolidated column cannot. + */ +export function accountWiseByBranch(range: GlDateRange, branchCodes: string[]): AccountBranchRow[] { + const perBranch = new Map>(); + for (const b of branchCodes) { + perBranch.set(b, cumulativeBalances(range.to, b)); + } + const rows: AccountBranchRow[] = []; + for (const account of GL_ACCOUNTS) { + const byBranch: Record = {}; + let total = 0; + let any = false; + for (const b of branchCodes) { + const v = perBranch.get(b)?.get(account.code) ?? 0; + byBranch[b] = v; + total += v; + if (v) { + any = true; + } + } + if (!any) { + continue; + } + rows.push({ code: account.code, name: account.name, group: account.group, byBranch, total }); + } + return rows; +} + +/** + * Share of the closing book held by each account group, for one side of the + * balance sheet. Used to see concentration at a glance. + */ +export function balanceDistribution( + range: GlDateRange, + branchCode: string, + side: 'ASSET' | 'LIABILITY' +): DistributionRow[] { + const closing = cumulativeBalances(range.to, branchCode); + const byGroup = new Map(); + let grand = 0; + + for (const account of GL_ACCOUNTS) { + if (account.type !== side) { + continue; + } + const v = closing.get(account.code) ?? 0; + if (!v) { + continue; + } + const entry = byGroup.get(account.group) ?? { amount: 0, accounts: 0 }; + entry.amount += v; + entry.accounts += 1; + byGroup.set(account.group, entry); + grand += v; + } + + return [...byGroup.entries()] + .map( + ([ + group, + e + ]) => ({ + group, + amount: e.amount, + accounts: e.accounts, + share: grand ? (e.amount / grand) * 100 : 0 + }) + ) + .sort((a, b) => b.amount - a.amount); +} + +export interface ClientBalanceRow { + cif: string; + name: string; + branchCode: string; + accountNo: string; + accountCode: string; + product: string; + /** DEPOSIT or ADVANCE — which side of the relationship this is. */ + side: 'DEPOSIT' | 'ADVANCE'; + balance: number; +} + +export interface BankerHeadRow { + head: string; + opening: number; + debit: number; + credit: number; + closing: number; + accounts: number; +} + +export interface SundryItemRow { + date: string; + voucherNo: string; + branchCode: string; + code: string; + name: string; + narration: string; + amount: number; + /** Days outstanding as at the period end. */ + ageDays: number; + bucket: string; +} + +export interface VoucherRow { + voucherNo: string; + date: string; + branchCode: string; + narration: string; + debitAccounts: string; + creditAccounts: string; + amount: number; + legs: number; + /** False when debits and credits on the voucher do not agree. */ + balanced: boolean; +} + +/** + * Client-wise balances, derived from the GL rather than stored separately so the two + * can never disagree. Each customer's balance is its share of the branch's closing + * balance in the head it sits under. + */ +export function clientWiseBalances(range: GlDateRange, branchCode: string): ClientBalanceRow[] { + const rows: ClientBalanceRow[] = []; + const branchClosings = new Map>(); + + for (const c of glCustomers()) { + if (branchCode && c.branchCode !== branchCode) { + continue; + } + let closing = branchClosings.get(c.branchCode); + if (!closing) { + closing = cumulativeBalances(range.to, c.branchCode); + branchClosings.set(c.branchCode, closing); + } + const headBalance = closing.get(c.accountCode) ?? 0; + const balance = headBalance * c.share; + if (Math.abs(balance) < 0.005) { + continue; + } + const account = glAccount(c.accountCode); + rows.push({ + cif: c.cif, + name: c.name, + branchCode: c.branchCode, + accountNo: c.accountNo, + accountCode: c.accountCode, + product: account?.name ?? c.accountCode, + side: c.accountCode.startsWith('13') ? 'ADVANCE' : 'DEPOSIT', + balance + }); + } + return rows.sort((a, b) => b.balance - a.balance); +} + +/** + * Head-wise abstract: the trial balance rolled up to principal heads, the summary a + * banker reads before the detail. Opening, turnover and closing per head. + */ +export function bankerHeadSummary(range: GlDateRange, branchCode: string): BankerHeadRow[] { + const opening = cumulativeBalances(previousDay(range.from), branchCode); + const turnover = periodTurnover(range, branchCode); + const byHead = new Map(); + + for (const account of GL_ACCOUNTS) { + // Contingents are single-sided off-balance-sheet memoranda. Including them would + // make the abstract's debit and credit turnover disagree and stop it reconciling + // to the trial balance, which also excludes them. They have their own report. + if (account.type === 'CONTINGENT') { + continue; + } + const open = opening.get(account.code) ?? 0; + const move = turnover.get(account.code); + if (!open && !move) { + continue; + } + const debit = move?.debit ?? 0; + const credit = move?.credit ?? 0; + const closing = open + (isDebitSide(account) ? debit - credit : credit - debit); + + const row = byHead.get(account.group) ?? { + head: account.group, + opening: 0, + debit: 0, + credit: 0, + closing: 0, + accounts: 0 + }; + row.opening += open; + row.debit += debit; + row.credit += credit; + row.closing += closing; + row.accounts += 1; + byHead.set(account.group, row); + } + return [...byHead.values()]; +} + +const SUNDRY_CODES = [ + '140104', + '230103', + '140199', + '230102' +]; + +/** + * Outstanding items in the sundry and suspense heads, aged from the posting date. + * These accounts are where unreconciled entries accumulate, so ageing is the point + * of the register. + */ +export function sundryRegister(range: GlDateRange, branchCode: string): SundryItemRow[] { + const wanted = new Set(SUNDRY_CODES); + const end = new Date(`${range.to}T00:00:00Z`).getTime(); + const rows: SundryItemRow[] = []; + + for (const p of glPostings()) { + if (p.date > range.to || !matchesBranch(p, branchCode) || !wanted.has(p.accountCode)) { + continue; + } + const ageDays = Math.max(0, Math.round((end - new Date(`${p.date}T00:00:00Z`).getTime()) / 86400000)); + const bucket = + ageDays <= 30 + ? 'Up to 30 days' + : ageDays <= 90 + ? '31 to 90 days' + : ageDays <= 180 + ? '91 to 180 days' + : 'Over 180 days'; + rows.push({ + date: p.date, + voucherNo: p.voucherNo, + branchCode: p.branchCode, + code: p.accountCode, + name: glAccount(p.accountCode)?.name ?? p.accountCode, + narration: p.narration, + amount: p.debit || p.credit, + ageDays, + bucket + }); + } + return rows.sort((a, b) => b.ageDays - a.ageDays); +} + +/** + * Every voucher raised in the period with its debit and credit legs collapsed onto + * one line, plus a per-voucher double-entry check. + */ +export function voucherRegister(range: GlDateRange, branchCode: string): VoucherRow[] { + const grouped = new Map(); + for (const p of glPostings()) { + if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) { + continue; + } + const list = grouped.get(p.voucherNo); + if (list) { + list.push(p); + } else { + grouped.set(p.voucherNo, [p]); + } + } + + const rows: VoucherRow[] = []; + for (const [ + voucherNo, + legs + ] of grouped) { + const debits = legs.filter((l) => l.debit > 0); + const credits = legs.filter((l) => l.credit > 0); + const drTotal = debits.reduce((s, l) => s + l.debit, 0); + const crTotal = credits.reduce((s, l) => s + l.credit, 0); + const nameOf = (codes: GlPosting[], side: 'debit' | 'credit') => + [...new Set(codes.map((l) => `${l.accountCode} ${glAccount(l.accountCode)?.name ?? ''}`.trim()))].join(', '); + + rows.push({ + voucherNo, + date: legs[0].date, + branchCode: legs[0].branchCode, + narration: legs[0].narration, + debitAccounts: nameOf(debits, 'debit') || '—', + creditAccounts: nameOf(credits, 'credit') || '—', + amount: Math.max(drTotal, crTotal), + legs: legs.length, + // Contingent and opening memoranda are single-sided by design. + balanced: voucherNo.startsWith('CNT/') || voucherNo.startsWith('OB/') || Math.round(drTotal - crTotal) === 0 + }); + } + return rows.sort((a, b) => + a.date === b.date ? a.voucherNo.localeCompare(b.voucherNo) : a.date.localeCompare(b.date) + ); } // ── Memoisation ────────────────────────────────────────────────────────────── @@ -360,6 +789,41 @@ export function clampToLedger(range: GlDateRange): GlDateRange { }; } +/** + * Value of a set of GL accounts for a period, used when an RBI return line item is + * mapped to the chart of accounts. + * + * Balance-sheet accounts contribute their closing balance as at the period end; + * income and expenditure accounts contribute their movement within the period. + * That mirrors how the two kinds of figure are read on a return. + */ +export function glValueForCodes(codes: string[], range: GlDateRange, branchCode = ''): number { + if (!codes.length) { + return 0; + } + const wanted = new Set(codes); + const closing = cumulativeBalances(range.to, branchCode); + const movement = periodTurnover(range, branchCode); + let total = 0; + + for (const code of wanted) { + const account = glAccount(code); + if (!account) { + continue; + } + if (account.type === 'INCOME' || account.type === 'EXPENSE') { + const move = movement.get(code); + if (!move) { + continue; + } + total += account.type === 'INCOME' ? move.credit - move.debit : move.debit - move.credit; + } else { + total += closing.get(code) ?? 0; + } + } + return total; +} + export function sum(rows: T[], pick: (row: T) => number): number { return rows.reduce((acc, r) => acc + pick(r), 0); } diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.html b/src/app/rbi-circulars/aml-rules/aml-rules.component.html index e9c58ede8..106151acf 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.html +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.html @@ -21,6 +21,22 @@ + +
+ Applies over + +
+ @@ -45,10 +61,37 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.scss b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss index 8234e9486..e5de73029 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.scss +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.scss @@ -114,13 +114,141 @@ max-width: 400px; } -.income-limit-cell { +.col-right { + text-align: right; + justify-content: flex-end; +} + +/* What the min/max amounts are measured against. */ +.basis-cell { color: #334155; - font-size: 13px; - font-weight: 500; + font-size: 12px; + font-weight: 600; white-space: normal; - max-width: 200px; - line-height: 1.35; + max-width: 150px; + line-height: 1.3; +} + +.basis-sub { + display: block; + font-size: 11px; + font-weight: 400; + color: #64748b; +} + +.amount-cell { + font-family: 'DM Mono', monospace; + font-size: 12.5px; + color: #0f172a; + white-space: nowrap; +} + +/* An open-ended threshold rule has a floor but no ceiling. */ +.no-ceiling { + font-family: inherit; + font-size: 11px; + font-style: italic; + color: #94a3b8; +} + +/* ── Observation-window badges ─────────────────────────────────────────── */ +.period-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + letter-spacing: 0.02em; + + &.period-txn { + background: #e2e8f0; + color: #334155; + } + + &.period-day { + background: #dbeafe; + color: #1d4ed8; + } + + &.period-week { + background: #fef3c7; + color: #b45309; + } + + &.period-month { + background: #ede9fe; + color: #6d28d9; + } + + &.period-other { + background: #fee2e2; + color: #b91c1c; + } +} + +/* Chips that separate the rule list by window. */ +.period-filters { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 0 0 14px; +} + +.period-filters-label { + font-size: 11px; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-right: 2px; +} + +.period-chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 13px; + border: 1px solid #cbd5e1; + background: #fff; + border-radius: 999px; + font-size: 12.5px; + font-weight: 600; + color: #334155; + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + + &:hover:not(:disabled) { + border-color: #1a237e; + color: #1a237e; + } + + &.active { + background: #1a237e; + border-color: #1a237e; + color: #fff; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.period-chip-n { + font-size: 11px; + font-weight: 700; + padding: 1px 7px; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + + .period-chip.active & { + background: rgb(255 255 255 / 22%); + color: #fff; + } } /* Badges for Risk Category */ diff --git a/src/app/rbi-circulars/aml-rules/aml-rules.component.ts b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts index 5d5843c5e..4d72eb407 100644 --- a/src/app/rbi-circulars/aml-rules/aml-rules.component.ts +++ b/src/app/rbi-circulars/aml-rules/aml-rules.component.ts @@ -42,24 +42,185 @@ export class AmlRulesComponent implements OnInit { 'orderPosition', 'ruleName', 'riskCategory', - 'incomeLimit', + 'limitBasis', + 'minLimit', + 'maxLimit', + 'appliesOver', 'reason', 'amlCheckStatus', 'bannedEntityMatch' ]; + /** Every rule as returned by the API, before the period filter is applied. */ + private allRules: any[] = []; + + /** Period buckets the rules can be separated into. */ + readonly periodFilters: { key: string; label: string }[] = [ + { key: '', label: 'All rules' }, + { key: 'TXN', label: 'Per transaction' }, + { key: 'DAY', label: 'Daily' }, + { key: 'WEEK', label: 'Weekly' }, + { key: 'MONTH', label: 'Monthly' }, + { key: 'OTHER', label: 'Other window' } + ]; + activePeriod = ''; + @ViewChild(MatPaginator) paginator!: MatPaginator; ngOnInit(): void { this.clientsService.getClientRiskRules().subscribe({ next: (data) => { - this.rules.data = data || []; + this.allRules = data || []; + this.applyPeriodFilter(); this.rules.paginator = this.paginator; }, error: () => {} }); } + setPeriod(key: string): void { + this.activePeriod = key; + this.applyPeriodFilter(); + } + + private applyPeriodFilter(): void { + this.rules.data = this.activePeriod + ? this.allRules.filter((r) => this.periodKey(r) === this.activePeriod) + : this.allRules; + if (this.rules.paginator) { + this.rules.paginator.firstPage(); + } + } + + /** How many rules fall in each period bucket — shown on the filter chips. */ + periodCount(key: string): number { + return key ? this.allRules.filter((r) => this.periodKey(r) === key).length : this.allRules.length; + } + + /** + * Buckets a rule by the observation window it is measured over. + * A rule with no window is evaluated on the single transaction itself, so it is + * in force every day rather than accumulating over a period. + */ + periodKey(rule: any): string { + const days = rule.txnWindowDays; + if (days == null || days === 0) { + return 'TXN'; + } + if (days === 1) { + return 'DAY'; + } + if (days === 7) { + return 'WEEK'; + } + if (days >= 28 && days <= 31) { + return 'MONTH'; + } + return 'OTHER'; + } + + periodLabel(rule: any): string { + const days = rule.txnWindowDays; + switch (this.periodKey(rule)) { + case 'TXN': + return 'Per transaction'; + case 'DAY': + return 'Daily'; + case 'WEEK': + return 'Weekly'; + case 'MONTH': + return 'Monthly'; + default: + return `${days} days`; + } + } + + periodClass(rule: any): string { + return 'period-badge period-' + this.periodKey(rule).toLowerCase(); + } + + /** Sub-caption telling the reader what the min/max figures actually measure. */ + getLimitBasis(rule: any): string { + if (rule.minMonthlyIncome > 0 || rule.maxMonthlyIncome > 0) { + return 'Monthly Income'; + } + if (rule.minAnnualIncome > 0 || rule.maxAnnualIncome > 0) { + return 'Annual Income'; + } + if (rule.minAnnualTurnover > 0 || rule.maxAnnualTurnover > 0) { + return 'Annual Turnover'; + } + if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) { + return 'Txn Amount (tiered)'; + } + if (rule.txnThresholdAmount != null) { + return 'Txn Amount'; + } + if (rule.occupationName) { + return 'Occupation'; + } + if (rule.occupationCategoryName) { + return 'Occupation Category'; + } + return 'Behavioural pattern'; + } + + private inr(n: number): string { + return '₹' + Number(n).toLocaleString('en-IN'); + } + + /** Lower bound of the rule, or an em dash where the criterion is not numeric. */ + getMinLimit(rule: any): string { + if (rule.minMonthlyIncome > 0) { + return this.inr(rule.minMonthlyIncome); + } + if (rule.minAnnualIncome > 0) { + return this.inr(rule.minAnnualIncome); + } + if (rule.minAnnualTurnover > 0) { + return this.inr(rule.minAnnualTurnover); + } + // Declarative band from m_aml_transaction_rule takes precedence over the raw threshold. + if (rule.txnMinLimitAmount != null) { + return this.inr(rule.txnMinLimitAmount); + } + if (rule.txnThresholdAmount != null) { + return this.inr(rule.txnThresholdAmount); + } + return '—'; + } + + /** Upper bound, or "No upper limit" where the rule is genuinely open-ended. */ + getMaxLimit(rule: any): string { + if (rule.maxMonthlyIncome > 0) { + return this.inr(rule.maxMonthlyIncome); + } + if (rule.maxAnnualIncome > 0) { + return this.inr(rule.maxAnnualIncome); + } + if (rule.maxAnnualTurnover > 0) { + return this.inr(rule.maxAnnualTurnover); + } + if (rule.txnMaxLimitAmount != null) { + return this.inr(rule.txnMaxLimitAmount); + } + // A floor with no configured ceiling means the rule has no upper bound. + if ( + rule.minMonthlyIncome > 0 || + rule.minAnnualIncome > 0 || + rule.minAnnualTurnover > 0 || + rule.txnThresholdAmount != null + ) { + return 'No upper limit'; + } + return '—'; + } + + /** Occupation-driven rules carry no amount; show the criterion instead. */ + getNonNumericCriterion(rule: any): string { + return rule.occupationName || rule.occupationCategoryName || ''; + } + getRiskCategoryLabel(category: number): string { switch (category) { case 1: @@ -85,39 +246,4 @@ export class AmlRulesComponent implements OnInit { return ''; } } - - /** Formats whichever income/turnover/occupation criterion drives this rule's risk mark. */ - getIncomeLimitLabel(rule: any): string { - const inr = (n: number) => '₹' + Number(n).toLocaleString('en-IN'); - if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0) { - return rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 - ? `Monthly Income ${inr(rule.minMonthlyIncome)} - ${inr(rule.maxMonthlyIncome)}` - : `Monthly Income > ${inr(rule.minMonthlyIncome)}`; - } - if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0) { - return rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 - ? `Annual Income ${inr(rule.minAnnualIncome)} - ${inr(rule.maxAnnualIncome)}` - : `Annual Income > ${inr(rule.minAnnualIncome)}`; - } - if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0) { - return rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 - ? `Annual Turnover ${inr(rule.minAnnualTurnover)} - ${inr(rule.maxAnnualTurnover)}` - : `Annual Turnover > ${inr(rule.minAnnualTurnover)}`; - } - if (rule.occupationName) return `Occupation: ${rule.occupationName}`; - if (rule.occupationCategoryName) return `Occupation Category: ${rule.occupationCategoryName}`; - if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) { - const parts: string[] = []; - if (rule.txnThresholdAmountMediumRisk != null) parts.push(`${inr(rule.txnThresholdAmountMediumRisk)} (Medium Risk)`); - if (rule.txnThresholdAmountHighRisk != null) parts.push(`${inr(rule.txnThresholdAmountHighRisk)} (High Risk)`); - return `Txn Amount ≥ ${parts.join(' / ')}`; - } - if (rule.txnThresholdAmount != null) { - const amt = inr(rule.txnThresholdAmount); - if (rule.txnRuleType === 'THRESHOLD') return `Txn Amount ≥ ${amt}`; - if (rule.txnWindowDays != null) return `${amt} within ${rule.txnWindowDays} days`; - return `Txn Amount ≥ ${amt}`; - } - return 'Behavioral / Transaction Pattern'; - } } diff --git a/src/app/rbi-circulars/gl-mapping-dialog.component.html b/src/app/rbi-circulars/gl-mapping-dialog.component.html new file mode 100644 index 000000000..f04772e96 --- /dev/null +++ b/src/app/rbi-circulars/gl-mapping-dialog.component.html @@ -0,0 +1,126 @@ + + +
+
+
+

Map General Ledger accounts

+

{{ data.returnCode }} · {{ data.lineLabel }}

+
+ +
+ + +
+
+ On the return + {{ data.filedValue === null ? '—' : (data.filedValue | number: '1.2-2') }} +
+ +
+ From the ledger + {{ selected.length ? (derivedValue | number: '1.2-2') : '—' }} +
+ @if (selected.length && variance !== null) { +
+ Variance + {{ variance | number: '1.2-2' }} +
+ } +
+ @if (!selected.length) { + Select accounts below to derive this line from the ledger. + } @else if (matchesFiled) { + Ledger agrees with the filed figure. + } @else { + Ledger differs from the filed figure — the mapped value will be shown on the return. + } +
+
+ + +
+
+ Mapped accounts ({{ selected.length }}) + @if (selected.length) { + + } +
+ @if (selected.length) { +
+ @for (code of selected; track code) { + + {{ code }} {{ accountName(code) }} + + + } +
+ } @else { +

No accounts mapped — this line still uses the figure filed on the return.

+ } +
+ + +
+ + +
+ +
+
Income / Occupation Limit{{ getIncomeLimitLabel(element) }}Limit Basis + {{ getLimitBasis(element) }} + + {{ getNonNumericCriterion(element) }} + + Minimum Limit{{ getMinLimit(element) }}Maximum Limit + {{ getMaxLimit(element) }} + Applies Over + {{ periodLabel(element) }} +
+ + + + + + + + + + + @for (a of filtered; track a.code) { + + + + + + + + } @empty { + + + + } + +
MapGL CodeAccountTypeGroup
+ + @if (isSelected(a.code)) { + + } + + {{ a.code }}{{ a.name }} + {{ a.type }} + {{ a.group }}
No accounts match that search.
+ + +
+ + +
+ diff --git a/src/app/rbi-circulars/gl-mapping-dialog.component.scss b/src/app/rbi-circulars/gl-mapping-dialog.component.scss new file mode 100644 index 000000000..4f477dfa6 --- /dev/null +++ b/src/app/rbi-circulars/gl-mapping-dialog.component.scss @@ -0,0 +1,358 @@ +/** + * 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/. + */ + +:host { + --m-navy: #002f5f; + --m-green: #15803d; + --m-green-l: #dcfce7; + --m-amber: #b45309; + --m-amber-l: #fef3c7; + --m-red: #c0392b; + --m-red-l: #fdecea; + --m-g50: #f8fafc; + --m-g100: #f1f5f9; + --m-g200: #e2e8f0; + --m-g300: #cbd5e1; + --m-g500: #64748b; + --m-g700: #334155; + --m-g900: #0f172a; + + display: block; + font-family: 'DM Sans', system-ui, sans-serif; + color: var(--m-g900); +} + +.glm-root { + display: flex; + flex-direction: column; + gap: 14px; + padding: 18px 20px; + max-height: 88vh; +} + +.glm-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + + h3 { + margin: 0; + font-size: 17px; + font-weight: 700; + } + + p { + margin: 2px 0 0; + font-size: 12px; + color: var(--m-g500); + } +} + +/* Ledger vs filed figure */ +.glm-compare { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + padding: 12px 16px; + border-radius: 10px; + background: var(--m-g100); + border: 1px solid var(--m-g200); + + &--match { + background: var(--m-green-l); + border-color: #86efac; + } + + &--diff { + background: var(--m-amber-l); + border-color: #fcd34d; + } +} + +.glm-fig { + display: flex; + flex-direction: column; + + &--var .glm-fig-v { + color: var(--m-amber); + } +} + +.glm-fig-l { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--m-g500); +} + +.glm-fig-v { + font-family: 'DM Mono', monospace; + font-size: 16px; + font-weight: 700; +} + +.glm-fig-ic { + color: var(--m-g500); +} + +.glm-compare-note { + flex: 1 1 220px; + font-size: 12px; + color: var(--m-g700); +} + +/* Pinned accounts */ +.glm-selected-h { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--m-g500); + margin-bottom: 7px; +} + +.glm-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.glm-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 6px 4px 10px; + border-radius: 999px; + background: #e0f2fe; + color: #075985; + font-size: 12px; + font-weight: 500; +} + +.glm-chip-x { + border: none; + background: rgb(7 89 133 / 12%); + color: #075985; + width: 18px; + height: 18px; + border-radius: 50%; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + + &:hover { + background: var(--m-red); + color: #fff; + } +} + +.glm-empty { + margin: 0; + font-size: 12px; + color: var(--m-g500); + font-style: italic; +} + +/* Picker */ +.glm-filters { + display: flex; + gap: 8px; +} + +.glm-inp { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--m-g200); + border-radius: 8px; + font-size: 13px; + font-family: inherit; + + &--sm { + flex: 0 0 160px; + } + + &:focus { + outline: none; + border-color: var(--m-navy); + } +} + +.glm-list { + flex: 1; + min-height: 180px; + max-height: 40vh; + overflow: auto; + border: 1px solid var(--m-g200); + border-radius: 8px; +} + +.glm-tbl { + width: 100%; + border-collapse: collapse; + + th { + position: sticky; + top: 0; + z-index: 1; + text-align: left; + padding: 8px 12px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--m-g500); + background: var(--m-g50); + border-bottom: 1px solid var(--m-g200); + } + + td { + padding: 7px 12px; + border-bottom: 1px solid var(--m-g100); + font-size: 13px; + color: var(--m-g700); + } + + tbody tr { + cursor: pointer; + + &:hover td { + background: var(--m-g50); + } + + &.on td { + background: #eff6ff; + } + } +} + +.glm-check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 2px solid var(--m-g300); + border-radius: 5px; + color: transparent; + font-size: 11px; + + &.on { + background: var(--m-green); + border-color: var(--m-green); + color: #fff; + } +} + +.glm-type { + display: inline-block; + padding: 1px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: var(--m-g100); + color: var(--m-g700); + + &.t-asset { + background: #dbeafe; + color: #1d4ed8; + } + + &.t-liability { + background: #e0f4f0; + color: #0e7e6b; + } + + &.t-income { + background: var(--m-green-l); + color: var(--m-green); + } + + &.t-expense { + background: var(--m-amber-l); + color: var(--m-amber); + } + + &.t-contingent { + background: var(--m-red-l); + color: var(--m-red); + } +} + +.glm-empty-row { + text-align: center; + padding: 26px; + color: var(--m-g500); + font-style: italic; +} + +.mono { + font-family: 'DM Mono', monospace; + font-size: 12px; +} + +.muted { + color: var(--m-g500); + font-size: 11px; +} + +.ta-c { + text-align: center; +} + +/* Actions */ +.glm-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.glm-btn { + padding: 8px 16px; + border: 1px solid var(--m-g300); + background: #fff; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + color: var(--m-g700); + cursor: pointer; + font-family: inherit; + + &:hover { + background: var(--m-g50); + } +} + +.glm-btn-primary { + background: var(--m-navy); + border-color: var(--m-navy); + color: #fff; + + &:hover { + background: #013a75; + color: #fff; + } +} + +.glm-link { + border: none; + background: none; + color: var(--m-red); + font-size: 11px; + font-weight: 700; + cursor: pointer; + text-transform: none; + letter-spacing: 0; +} diff --git a/src/app/rbi-circulars/gl-mapping-dialog.component.ts b/src/app/rbi-circulars/gl-mapping-dialog.component.ts new file mode 100644 index 000000000..2df509a9e --- /dev/null +++ b/src/app/rbi-circulars/gl-mapping-dialog.component.ts @@ -0,0 +1,121 @@ +/** + * 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, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { GlAccount } from 'app/gl-reports/gl-reports-data'; +import { GlDateRange, glValueForCodes } from 'app/gl-reports/gl-reports.compute'; +import { RETURN_SCALE, glAccountsForPicker } from './gl-mapping'; + +export interface GlMappingDialogData { + returnCode: string; + lineLabel: string; + /** The figure currently printed on the return, in thousands. */ + filedValue: number | null; + selected: string[]; + range: GlDateRange; + branchCode: string; +} + +/** + * Lets a user pin one or more GL accounts to a single return line, showing live what + * the ledger would produce for the selection against the figure currently on the return. + */ +@Component({ + selector: 'mifosx-gl-mapping-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatDialogModule, + FaIconComponent + ], + templateUrl: './gl-mapping-dialog.component.html', + styleUrls: ['./gl-mapping-dialog.component.scss'] +}) +export class GlMappingDialogComponent { + private dialogRef = inject(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + + readonly accounts: GlAccount[] = glAccountsForPicker(); + search = ''; + typeFilter = ''; + selected: string[] = []; + + readonly types = [ + 'ASSET', + 'LIABILITY', + 'INCOME', + 'EXPENSE', + 'CONTINGENT' + ]; + + constructor() { + this.selected = [...(this.data.selected ?? [])]; + } + + get filtered(): GlAccount[] { + const q = this.search.trim().toLowerCase(); + return this.accounts.filter((a) => { + if (this.typeFilter && a.type !== this.typeFilter) { + return false; + } + if (!q) { + return true; + } + return a.code.includes(q) || a.name.toLowerCase().includes(q) || a.group.toLowerCase().includes(q); + }); + } + + isSelected(code: string): boolean { + return this.selected.includes(code); + } + + toggle(code: string): void { + this.selected = this.isSelected(code) ? this.selected.filter((c) => c !== code) : [ + ...this.selected, + code + ]; + } + + removeSelected(code: string): void { + this.selected = this.selected.filter((c) => c !== code); + } + + clearAll(): void { + this.selected = []; + } + + accountName(code: string): string { + return this.accounts.find((a) => a.code === code)?.name ?? code; + } + + /** Live ledger figure for the current selection, in the return's scale. */ + get derivedValue(): number { + return glValueForCodes(this.selected, this.data.range, this.data.branchCode) / RETURN_SCALE; + } + + get variance(): number | null { + return this.data.filedValue == null ? null : this.derivedValue - this.data.filedValue; + } + + get matchesFiled(): boolean { + return this.variance !== null && Math.abs(this.variance) < 0.005; + } + + save(): void { + this.dialogRef.close(this.selected); + } + + cancel(): void { + this.dialogRef.close(undefined); + } +} diff --git a/src/app/rbi-circulars/gl-mapping.host.ts b/src/app/rbi-circulars/gl-mapping.host.ts new file mode 100644 index 000000000..d36144d32 --- /dev/null +++ b/src/app/rbi-circulars/gl-mapping.host.ts @@ -0,0 +1,140 @@ +/** + * 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 { inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BranchScopeService } from 'app/core/branch-scope.service'; +import { GlDateRange } from 'app/gl-reports/gl-reports.compute'; +import { GL_BRANCHES } from 'app/gl-reports/gl-reports-data'; +import { GlMappingDialogComponent, GlMappingDialogData } from './gl-mapping-dialog.component'; +import { + clearReturnMappings, + getMapping, + lineKeyFor, + mappedLineCount, + mappedValue, + removeGl, + setMapping +} from './gl-mapping'; + +/** + * Shared GL-mapping behaviour for the OSS return screens. + * + * Each return line can be pinned to GL accounts; where it is, the figure shown comes + * from the ledger rather than the static filed value. Subclasses only supply their + * return code — everything else, including the ledger period and branch scope, is + * common. + */ +export abstract class GlMappingHost { + private dialog = inject(MatDialog); + private branchScope = inject(BranchScopeService); + + /** e.g. 'R070'. Used to namespace this return's mappings. */ + abstract readonly glReturnCode: string; + + /** Ledger period the mapped figures are drawn from. */ + abstract get glRange(): GlDateRange; + + /** Whether the per-line mapping controls are visible. */ + glMapMode = false; + + /** '' = all branches consolidated. Seeded from the login's own scope. */ + glBranchCode = this.branchScope.defaultBranchCode; + readonly glBranches = GL_BRANCHES; + + /** Head Office may derive figures from any branch; a branch login cannot. */ + get canSelectGlBranch(): boolean { + return this.branchScope.canSelectBranch; + } + get glScopeName(): string { + return this.branchScope.scopeLabel(this.glBranchCode); + } + + /** Branch actually used for every ledger lookup, enforced rather than trusted. */ + private get effectiveGlBranch(): string { + return this.branchScope.enforce(this.glBranchCode); + } + + toggleGlMapMode(): void { + this.glMapMode = !this.glMapMode; + } + + glLineKey(label: string): string { + return lineKeyFor(label); + } + + glCodesFor(label: string): string[] { + return getMapping(this.glReturnCode, this.glLineKey(label)); + } + + isGlMapped(label: string): boolean { + return this.glCodesFor(label).length > 0; + } + + get glMappedCount(): number { + return mappedLineCount(this.glReturnCode); + } + + /** Ledger figure for a line, or null when it has no mapping. */ + glValue(label: string): number | null { + return mappedValue(this.glReturnCode, this.glLineKey(label), this.glRange, this.effectiveGlBranch); + } + + /** + * The number to print for a line: the ledger value when mapped, otherwise the + * value filed on the return. + */ + effectiveValue(label: string, filed: number | null): number | null { + const mapped = this.glValue(label); + return mapped === null ? filed : mapped; + } + + /** True when a mapped line disagrees with the figure originally filed. */ + glDiffers(label: string, filed: number | null): boolean { + const mapped = this.glValue(label); + if (mapped === null || filed === null) { + return false; + } + return Math.abs(mapped - filed) >= 0.005; + } + + removeGlFromLine(label: string, code: string): void { + removeGl(this.glReturnCode, this.glLineKey(label), code); + } + + clearAllGlMappings(): void { + clearReturnMappings(this.glReturnCode); + } + + /** Opens the chart-of-accounts picker for one return line. */ + openGlPicker(label: string, filed: number | null): void { + const key = this.glLineKey(label); + const data: GlMappingDialogData = { + returnCode: this.glReturnCode, + lineLabel: label, + filedValue: filed, + selected: getMapping(this.glReturnCode, key), + range: this.glRange, + branchCode: this.effectiveGlBranch + }; + this.dialog + .open(GlMappingDialogComponent, { + width: '92vw', + maxWidth: '980px', + maxHeight: '92vh', + autoFocus: false, + data + }) + .afterClosed() + .subscribe((codes: string[] | undefined) => { + if (codes) { + setMapping(this.glReturnCode, key, codes); + } + }); + } +} diff --git a/src/app/rbi-circulars/gl-mapping.ts b/src/app/rbi-circulars/gl-mapping.ts new file mode 100644 index 000000000..9b1e18544 --- /dev/null +++ b/src/app/rbi-circulars/gl-mapping.ts @@ -0,0 +1,170 @@ +/** + * 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/. + */ + +/** + * Maps RBI return line items to General Ledger accounts. + * + * A return line can be pinned to one or more GL codes; the line's figure is then + * derived from the ledger instead of being a static number, so the filed return and + * the books cannot drift apart silently. Mappings are keyed by return code plus a + * stable line key and persisted locally. + * + * The OSS returns are denominated in thousands while the ledger is in rupees, so + * every derived figure is scaled on the way out. + */ + +import { GL_ACCOUNTS, GlAccount } from 'app/gl-reports/gl-reports-data'; +import { GlDateRange, glValueForCodes } from 'app/gl-reports/gl-reports.compute'; + +const STORAGE_KEY = 'rbi_return_gl_mappings'; + +/** Returns are reported in thousands of rupees. */ +export const RETURN_SCALE = 1000; + +export type GlMappingStore = Record; + +function mapKey(returnCode: string, lineKey: string): string { + return `${returnCode}::${lineKey}`; +} + +/** + * The store is read from template getters on every change-detection pass, so it is + * held in memory and only re-parsed when a write invalidates it. Parsing JSON a few + * hundred times per click is otherwise a measurable cost on the larger returns. + */ +let storeCache: GlMappingStore | null = null; + +function readStore(): GlMappingStore { + if (storeCache) { + return storeCache; + } + try { + storeCache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as GlMappingStore; + } catch { + storeCache = {}; + } + return storeCache; +} + +function writeStore(store: GlMappingStore): void { + storeCache = store; + localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); +} + +/** GL codes currently pinned to a return line. */ +export function getMapping(returnCode: string, lineKey: string): string[] { + return readStore()[mapKey(returnCode, lineKey)] ?? []; +} + +export function setMapping(returnCode: string, lineKey: string, codes: string[]): void { + const store = readStore(); + const key = mapKey(returnCode, lineKey); + if (codes.length) { + store[key] = codes; + } else { + delete store[key]; + } + writeStore(store); +} + +export function addGl(returnCode: string, lineKey: string, code: string): void { + const current = getMapping(returnCode, lineKey); + if (!current.includes(code)) { + setMapping(returnCode, lineKey, [ + ...current, + code + ]); + } +} + +export function removeGl(returnCode: string, lineKey: string, code: string): void { + setMapping( + returnCode, + lineKey, + getMapping(returnCode, lineKey).filter((c) => c !== code) + ); +} + +/** Every line in a return that currently has a mapping. */ +export function mappedLineCount(returnCode: string): number { + const prefix = `${returnCode}::`; + return Object.keys(readStore()).filter((k) => k.startsWith(prefix)).length; +} + +export function clearReturnMappings(returnCode: string): void { + const store = readStore(); + const prefix = `${returnCode}::`; + for (const k of Object.keys(store)) { + if (k.startsWith(prefix)) { + delete store[k]; + } + } + writeStore(store); +} + +/** + * Ledger value for a mapped line, expressed in the return's own scale (thousands). + * Returns null when the line has no mapping, so callers can fall back to the filed figure. + */ +export function mappedValue(returnCode: string, lineKey: string, range: GlDateRange, branchCode = ''): number | null { + const codes = getMapping(returnCode, lineKey); + if (!codes.length) { + return null; + } + return glValueForCodes(codes, range, branchCode) / RETURN_SCALE; +} + +/** A line key that stays stable across reorderings — derived from the label itself. */ +export function lineKeyFor(label: string): string { + return label + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 80); +} + +export interface GlMappingEntry { + returnCode: string; + lineKey: string; + codes: string[]; +} + +/** Every mapping across all returns — drives the GL Mapping for Report listing. */ +export function allMappings(): GlMappingEntry[] { + return Object.entries(readStore()).map( + ([ + key, + codes + ]) => { + const [ + returnCode, + lineKey + ] = key.split('::'); + return { returnCode, lineKey, codes }; + } + ); +} + +/** Human-readable name for a return code. */ +export function returnName(code: string): string { + const names: Record = { + R070: 'OSS 1 — Return on Assets and Liabilities', + R071: 'OSS 2 — Return on Earnings', + R323: 'OSS 3 — Return on Asset Quality' + }; + return names[code] ?? code; +} + +export function glAccountsForPicker(): GlAccount[] { + return GL_ACCOUNTS; +} + +export function glLabel(code: string): string { + const a = GL_ACCOUNTS.find((x) => x.code === code); + return a ? `${a.code} — ${a.name}` : code; +} diff --git a/src/app/rbi-circulars/oss-return.scss b/src/app/rbi-circulars/oss-return.scss index fd59d774d..ade2b179c 100644 --- a/src/app/rbi-circulars/oss-return.scss +++ b/src/app/rbi-circulars/oss-return.scss @@ -360,6 +360,158 @@ td.derived { color: var(--o-g500); } +/* ── General Ledger mapping ─────────────────────────────────────────────── */ +.oss-gl-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 14px; + flex-wrap: wrap; + padding: 10px 16px; + border-radius: 10px; + background: #fff; + border: 1px solid var(--o-g200); + + &.on { + background: var(--o-blue-l); + border-color: #93c5fd; + } +} + +.oss-gl-bar-l { + display: flex; + align-items: center; + gap: 9px; + font-size: 13px; + color: var(--o-g700); +} + +.oss-gl-bar-r { + display: flex; + align-items: center; + gap: 8px; +} + +.oss-gl-count { + display: inline-block; + margin-left: 8px; + padding: 2px 9px; + border-radius: 999px; + background: var(--o-green-l); + color: var(--o-green); + font-size: 11px; + font-weight: 700; +} + +.oss-gl-inp { + padding: 7px 11px; + border: 1px solid var(--o-g300); + border-radius: 8px; + font-size: 12.5px; + font-family: inherit; + background: #fff; +} + +.oss-btn-clear { + color: var(--o-red); + border-color: var(--o-red); +} + +.oss-gl-col { + min-width: 240px; +} + +/* Read-only branch shown to a branch-level login in place of the selector. */ +.oss-gl-scope { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border: 1px solid var(--o-g300); + border-radius: 8px; + background: var(--o-g100); + font-size: 12.5px; + font-weight: 600; + color: var(--o-g700); +} + +.oss-gl-cell { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + +.oss-gl-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 4px 2px 8px; + border-radius: 999px; + background: #e0f2fe; + color: #075985; + font-size: 11px; + font-weight: 600; +} + +.oss-gl-chip-x { + border: none; + background: rgb(7 89 133 / 14%); + color: #075985; + width: 15px; + height: 15px; + border-radius: 50%; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 8px; + + &:hover { + background: var(--o-red); + color: #fff; + } +} + +.oss-gl-add { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 9px; + border: 1px dashed var(--o-g300); + background: #fff; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: var(--o-g500); + cursor: pointer; + font-family: inherit; + + &:hover { + border-color: var(--o-navy); + color: var(--o-navy); + border-style: solid; + } +} + +.oss-gl-warn { + font-size: 10.5px; + font-weight: 700; + color: var(--o-amber-d); + white-space: nowrap; +} + +/* A figure sourced from the ledger rather than the filed return. */ +td.gl-sourced { + background: #ecfdf5; + box-shadow: inset 3px 0 0 var(--o-green); +} + +td.gl-diff { + background: #fffbeb; + box-shadow: inset 3px 0 0 #d97706; +} + /* Tie-up status pills (cross-return reconciliation checks). */ .report-pill { display: inline-block; diff --git a/src/app/rbi-circulars/oss-workbook.ts b/src/app/rbi-circulars/oss-workbook.ts index f355ac2c7..edddbe9ac 100644 --- a/src/app/rbi-circulars/oss-workbook.ts +++ b/src/app/rbi-circulars/oss-workbook.ts @@ -7,13 +7,7 @@ */ import * as ExcelJS from 'exceljs'; -import { - MatrixTable, - ReturnGeneralInfoRow, - ReturnLine, - ReturnSignatory, - SIGNATORY_FIELDS -} from './oss-return.model'; +import { MatrixTable, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory, SIGNATORY_FIELDS } from './oss-return.model'; /** Palette lifted from the RBI CIMS workbooks so exports look like the originals. */ export const XL = { @@ -45,12 +39,7 @@ export function addBanner(sheet: ExcelJS.Worksheet, title: string, lastCol: stri sheet.addRow([]); } -export function boxAllCells( - sheet: ExcelJS.Worksheet, - from: number, - to: number, - lastColIndex: number -): void { +export function boxAllCells(sheet: ExcelJS.Worksheet, from: number, to: number, lastColIndex: number): void { for (let r = from; r <= to; r++) { for (let c = 1; c <= lastColIndex; c++) { sheet.getRow(r).getCell(c).border = { @@ -64,7 +53,10 @@ export function boxAllCells( } export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLabel?: string): void { - const row = sheet.addRow([label, unitLabel ?? '']); + const row = sheet.addRow([ + label, + unitLabel ?? '' + ]); row.font = { bold: true }; row.eachCell((cell) => (cell.fill = solidFill(XL.headerGrey))); row.getCell(2).alignment = { horizontal: 'right' }; @@ -73,7 +65,10 @@ export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLa /** Renders a labelled block; indents by level and shades derived totals. */ export function addLineRows(sheet: ExcelJS.Worksheet, lines: ReturnLine[]): void { for (const line of lines) { - const row = sheet.addRow([line.label, line.value]); + const row = sheet.addRow([ + line.label, + line.value + ]); const labelCell = row.getCell(1); const valueCell = row.getCell(2); @@ -98,7 +93,10 @@ export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): vo const colCount = table.columns.length + 1; const start = sheet.rowCount + 1; - const titleRow = sheet.addRow([table.title, ...(table.unit ? [table.unit] : [])]); + const titleRow = sheet.addRow([ + table.title, + ...(table.unit ? [table.unit] : []) + ]); if (!table.unit) { sheet.mergeCells(titleRow.number, 1, titleRow.number, colCount); } @@ -115,13 +113,19 @@ export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): vo sub.getCell(1).fill = solidFill(XL.headerGrey); } - const head = sheet.addRow(['Particulars', ...table.columns]); + const head = sheet.addRow([ + 'Particulars', + ...table.columns + ]); head.font = { bold: true }; head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }; head.eachCell((c) => (c.fill = solidFill(XL.headerGrey))); for (const row of table.rows) { - const excelRow = sheet.addRow([row.label, ...row.values]); + const excelRow = sheet.addRow([ + row.label, + ...row.values + ]); const labelCell = excelRow.getCell(1); labelCell.alignment = { wrapText: true, vertical: 'middle', indent: row.level }; if (row.level === 0 || row.header) { @@ -158,13 +162,15 @@ export function buildGeneralInfoSheet(wb: ExcelJS.Workbook, rows: ReturnGeneralI const sheet = wb.addWorksheet('General Information'); sheet.columns = [ { width: 26 }, - { width: 58 } - ]; + { width: 58 }]; addBanner(sheet, 'General Information', 'B'); const first = sheet.rowCount + 1; for (const item of rows) { - const row = sheet.addRow([item.label, item.value]); + const row = sheet.addRow([ + item.label, + item.value + ]); row.getCell(1).fill = solidFill(XL.keyCell); // The workbook tints the audit flag yellow and leaves unfilled cells grey. const fill = item.label === 'Audit Flag' ? XL.auditYellow : item.value ? XL.derivedBlue : XL.headerGrey; @@ -182,19 +188,24 @@ export function buildSignatorySheet( const sheet = wb.addWorksheet(sheetName); sheet.columns = [ { width: 44 }, - { width: 24 } - ]; + { width: 24 }]; addBanner(sheet, sheetName, 'B'); for (const person of signatories) { const start = sheet.rowCount + 1; - const head = sheet.addRow([person.heading, '']); + const head = sheet.addRow([ + person.heading, + '' + ]); head.font = { bold: true }; head.alignment = { horizontal: 'center' }; head.getCell(1).fill = solidFill(XL.keyCell); for (const f of SIGNATORY_FIELDS) { - const row = sheet.addRow([person[f.key], f.caption]); + const row = sheet.addRow([ + person[f.key], + f.caption + ]); row.getCell(2).font = { bold: true }; row.getCell(2).alignment = { horizontal: 'center' }; row.getCell(2).fill = solidFill(XL.keyCell); diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts b/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts index fb9243c85..a21851011 100644 --- a/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral-data.ts @@ -153,8 +153,7 @@ export const RAL_LIABILITIES: RalLine[] = [ { label: 'ii. Borrowings', level: 1, value: null }, { - label: - '12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)', + label: '12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)', level: 0, value: 39440, total: true @@ -192,7 +191,12 @@ export const RAL_ASSETS: RalLine[] = [ { label: '2. I. Due from Banks', level: 0, value: 57600, total: true }, { label: 'A. Balances with Banks', level: 1, value: 57600, total: true }, - { label: 'i. State/ Central Cooperative Bank of the State/District concerned in', level: 2, value: 32268, total: true }, + { + label: 'i. State/ Central Cooperative Bank of the State/District concerned in', + level: 2, + value: 32268, + total: true + }, { label: 'a. Current', level: 2, value: 5980 }, { label: 'b. Savings', level: 2, value: null }, { label: 'c. Fixed', level: 2, value: 26288 }, @@ -353,26 +357,73 @@ export const RAL_AGEING_HEADERS: string[] = [ /** Sheet 3 — Annexure 1 :: 3. Age-wise Inter Branch Adjustment Account */ export const RAL_INTER_BRANCH_AGEING: RalAgeingRow[] = [ - { period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { + period: '3 months to 6 months', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 6 months to 1 year', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 1 year to 2 years', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 2 years', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, { period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 } ]; /** Sheet 3 — Annexure 1 :: 4. Age-wise Inter Bank Reconciliation Account */ export const RAL_INTER_BANK_AGEING: RalAgeingRow[] = [ - { period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, - { period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null }, + { + period: '3 months to 6 months', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 6 months to 1 year', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 1 year to 2 years', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, + { + period: 'Above 2 years', + noOfDebitEntries: null, + amtOfDebitEntries: null, + noOfCreditEntries: null, + amtOfCreditEntries: null + }, { period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 } ]; /** Sheet 3 — Annexure 1 :: 5. Unsecured guarantees issued by the bank */ export const RAL_UNSECURED_GUARANTEES: RalUnsecuredGuarantee[] = [ - { srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null } -]; + { srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null }]; export const RAL_UNSECURED_GUARANTEES_TOTAL = 0; /** Sheet 4 — Annexure 2 :: Sundry Debtors ageing buckets */ @@ -401,20 +452,49 @@ export const RAL_SUNDRY_DEBTORS_NOTE = export const RAL_DEPOSITORS_BORROWERS: RalLine[] = [ { label: '1.Total No of Depositors', level: 0, value: 8156, total: true, count: true }, { label: 'Total Customer Deposits', level: 0, value: 634309, total: true }, - { label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh', level: 1, value: 6568, count: true }, + { + label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh', + level: 1, + value: 6568, + count: true + }, { label: 'ii) Of which total Deposit Amount', level: 1, value: 109992 }, - { label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 739, count: true }, + { + label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh', + level: 1, + value: 739, + count: true + }, { label: 'ii) Of which total Deposit Amount', level: 1, value: 102475 }, - { label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 580, count: true }, + { + label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh', + level: 1, + value: 580, + count: true + }, { label: 'ii) Of which total Deposit Amount', level: 1, value: 176820 }, - { label: '2.Total No of Borrowers', level: 0, value: 363, total: true, count: true }, { label: 'Total Loans and Advances (Other than Banks)', level: 0, value: 391288, total: true }, - { label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh', level: 1, value: 55, count: true }, + { + label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh', + level: 1, + value: 55, + count: true + }, { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2061 }, - { label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 18, count: true }, + { + label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh', + level: 1, + value: 18, + count: true + }, { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2837 }, - { label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 55, count: true }, + { + label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh', + level: 1, + value: 55, + count: true + }, { label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 21260 } ]; diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html index c59c196f1..1dea7996a 100644 --- a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.html @@ -81,11 +81,16 @@
Assets & Liabilities
+ +
+ @if (glMapMode) { + + } @@ -93,8 +98,25 @@ @for (line of liabilities; track $index) { - + } + } @@ -107,6 +129,9 @@ + @if (glMapMode) { + + } @@ -114,8 +139,25 @@ @for (line of assets; track $index) { - + } + } @@ -343,7 +385,7 @@ } @@ -417,6 +459,68 @@ } + + +
+
+ + + General Ledger mapping + @if (glMappedCount) { + {{ glMappedCount }} line(s) mapped + } @else { + — pin GL accounts to a line and its figure comes from the ledger + } + +
+
+ @if (glMapMode) { + @if (canSelectGlBranch) { + + } @else { + + {{ glScopeName }} + + } + @if (glMappedCount) { + + } + } + +
+
+
+ + + +
+ @for (code of glCodesFor(label); track code) { + + {{ code }} + + + } + + @if (glDiffers(label, filed)) { + + differs from filed + + } +
+
+
{{ toastMsg }}
diff --git a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts index 5329297c4..5d8d0a630 100644 --- a/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts +++ b/src/app/rbi-circulars/oss1-ral/oss1-ral.component.ts @@ -11,6 +11,7 @@ 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 { GlMappingHost } from '../gl-mapping.host'; import { addBanner, addLineRows, @@ -55,7 +56,14 @@ type Oss1Tab = 'general' | 'balance-sheet' | 'annexure1' | 'annexure2' | 'signat FormsModule ] }) -export class Oss1RalComponent { +export class Oss1RalComponent extends GlMappingHost { + readonly glReturnCode = 'R070'; + + /** RAL covers the reporting quarter; mapped figures are drawn from the same span. */ + get glRange() { + return { from: '2026-04-01', to: '2026-06-30' }; + } + activeTab: Oss1Tab = 'general'; generalInfo = RAL_GENERAL_INFO; @@ -146,8 +154,7 @@ export class Oss1RalComponent { const sheet = wb.addWorksheet('Assets & Liabilities'); sheet.columns = [ { width: 78 }, - { width: 20 } - ]; + { width: 20 }]; addBanner(sheet, 'Assets & Liabilities', 'B'); const first = sheet.rowCount + 1; @@ -187,18 +194,25 @@ export class Oss1RalComponent { { width: 16 }, { width: 12 }, { 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']); + 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]); + 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'; @@ -281,10 +295,20 @@ export class Oss1RalComponent { 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]); + 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]); + const gTotal = sheet.addRow([ + 'Total', + '', + '', + this.unsecuredGuaranteesTotal + ]); gTotal.font = { bold: true }; gTotal.getCell(4).numFmt = '#,##0.00'; gTotal.getCell(4).fill = solidFill(XL.derivedBlue); @@ -327,9 +351,7 @@ export class Oss1RalComponent { 3, 4, 5 - ].forEach( - (c) => (row.getCell(c).fill = solidFill(XL.derivedBlue)) - ); + ].forEach((c) => (row.getCell(c).fill = solidFill(XL.derivedBlue))); } } boxAllCells(sheet, start, sheet.rowCount, 5); @@ -341,8 +363,7 @@ export class Oss1RalComponent { { width: 10 }, { width: 62 }, { width: 20 }, - { width: 20 } - ]; + { width: 20 }]; addBanner(sheet, 'Annexure 2', 'D'); for (const block of this.sundryDebtors) { @@ -357,13 +378,23 @@ export class Oss1RalComponent { 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']); + 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]); + const row = sheet.addRow([ + r.srNo, + r.dueFromName, + r.amountDue, + r.provisionMade + ]); row.getCell(3).numFmt = '#,##0.00'; row.getCell(4).numFmt = '#,##0.00'; } @@ -378,14 +409,22 @@ export class Oss1RalComponent { // Details of Depositors & Borrowers let start = sheet.rowCount + 1; - const dbTitle = sheet.addRow(['Details of Depositors & Borrowers', '', 'Rs. In Thousands']); + 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]); + 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'; @@ -404,7 +443,11 @@ export class Oss1RalComponent { 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]); + 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'; diff --git a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html index 60328bdc9..d528accf5 100644 --- a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html +++ b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.html @@ -122,11 +122,52 @@
LiabilitiesMapped GL accountsRs in Thousands
{{ line.label }} - {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} + @if (glMapMode) { + + + + {{ + effectiveValue(line.label, line.value) === null + ? '' + : (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2')) + }}
AssetsMapped GL accountsRs in Thousands
{{ line.label }} - {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} + @if (glMapMode) { + + + + {{ + effectiveValue(line.label, line.value) === null + ? '' + : (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2')) + }}
{{ line.label }} - {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} + {{ line.value === null ? '' : (line.value | number: (line.count ? '1.0-0' : '1.2-2')) }}
+ +
+
+ + + General Ledger mapping + @if (glMappedCount) { + {{ glMappedCount }} line(s) mapped + } @else { + — pin GL accounts to a line and its figure comes from the ledger + } + +
+
+ @if (glMapMode) { + @if (canSelectGlBranch) { + + } @else { + + {{ glScopeName }} + + } + @if (glMappedCount) { + + } + } + +
+
+
+ @if (glMapMode) { + + } @@ -134,8 +175,44 @@ @for (line of earnings; track $index) { - + } + } @@ -186,9 +263,10 @@
- Detail total ₹{{ otherExpensesTotal | number: '1.2-2' }} vs Earnings line 12 ix) - Others - ₹{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }} + Detail total ₹{{ otherExpensesTotal | number: '1.2-2' }} vs Earnings line 12 ix) Others + ₹{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }} — {{ otherExpensesTally ? 'tallies' : 'MISMATCH — resolve before filing' }}
diff --git a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts index 9fb6d1483..b9d86b689 100644 --- a/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts +++ b/src/app/rbi-circulars/oss2-roe/oss2-roe.component.ts @@ -11,6 +11,7 @@ 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 { GlMappingHost } from '../gl-mapping.host'; import { addBanner, addLineRows, @@ -54,7 +55,14 @@ interface TieUp { FormsModule ] }) -export class Oss2RoeComponent { +export class Oss2RoeComponent extends GlMappingHost { + readonly glReturnCode = 'R071'; + + /** RoE reports the quarter; mapped income and expense figures use the same span. */ + get glRange() { + return { from: '2026-04-01', to: '2026-06-30' }; + } + activeTab: Oss2Tab = 'general'; generalInfo = ROE_GENERAL_INFO; @@ -178,8 +186,7 @@ export class Oss2RoeComponent { const sheet = wb.addWorksheet('Earnings'); sheet.columns = [ { width: 90 }, - { width: 20 } - ]; + { width: 20 }]; addBanner(sheet, 'Earnings', 'B'); const first = sheet.rowCount + 1; @@ -205,23 +212,31 @@ export class Oss2RoeComponent { const sheet = wb.addWorksheet('Interest Receivable Not Recog'); sheet.columns = [ { width: 72 }, - { width: 22 } - ]; + { width: 22 }]; addBanner(sheet, 'Interest Receivable Not Recognized', 'B'); const start = sheet.rowCount + 1; - const unit = sheet.addRow(['', 'Rs in Thousand']); + 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]); + 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]); + 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'; @@ -238,28 +253,43 @@ export class Oss2RoeComponent { sheet.columns = [ { width: 10 }, { width: 56 }, - { width: 20 } - ]; + { 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']); + 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']); + 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]); + const row = sheet.addRow([ + e.srNo, + e.particulars, + e.amount + ]); row.getCell(3).numFmt = '#,##0.00'; } - const total = sheet.addRow(['Total', '', this.otherExpensesTotal]); + 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); @@ -268,7 +298,11 @@ export class Oss2RoeComponent { boxAllCells(sheet, start, sheet.rowCount, 3); - const ref = sheet.addRow(['', '', 'Refer Earnings Sheet — line 12 ix) Others']); + const ref = sheet.addRow([ + '', + '', + 'Refer Earnings Sheet — line 12 ix) Others' + ]); ref.getCell(3).font = { italic: true, size: 10 }; } diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts b/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts index 907e9fd74..5ea27e0b2 100644 --- a/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq-data.ts @@ -19,13 +19,7 @@ * the cell blank — RBI treats blank and zero differently, so the two are never merged. */ -import { - MatrixTable, - OSS_BANK, - OSS_SIGNATORIES, - ReturnGeneralInfoRow, - ReturnSignatory -} from '../oss-return.model'; +import { MatrixTable, OSS_BANK, OSS_SIGNATORIES, ReturnGeneralInfoRow, ReturnSignatory } from '../oss-return.model'; /** A row of Table 5 on the Top Impaired Credits statement. */ export interface Oss3ImpairedCredit { @@ -356,16 +350,56 @@ export const RAQ_CLASSIFICATION_PART_A: MatrixTable = { 'Net Loans and Advances Outstanding' ], rows: [ - { label: '1. Standard#', level: 0, values: [384588, 3750, 380838] }, - { label: '1.1. Of which Restructured Standard', level: 1, values: [null, null, 0] }, - { label: '2. Sub-standard', level: 0, values: [10241, 1024, 9217] }, - { label: '2.1. Of which restructured Sub-standard', level: 1, values: [null, null, 0] }, - { label: '3. Doubtful', level: 0, total: true, values: [16375, 35225, -18850] }, - { label: '3.1. Doubtful upto 1 year', level: 1, values: [911, 182, 729] }, - { label: '3.2. Doubtful more than 1 year and upto 3 years', level: 1, values: [5174, 1552, 3622] }, - { label: '3.3. Doubtful more than 3 years', level: 1, values: [10290, 33491, -23201] }, - { label: '4. Loss', level: 0, values: [null, null, 0] }, - { label: 'TOTAL', level: 0, total: true, values: [411204, 39999, 371205] } + { label: '1. Standard#', level: 0, values: [ + 384588, + 3750, + 380838 + ] }, + { label: '1.1. Of which Restructured Standard', level: 1, values: [ + null, + null, + 0 + ] }, + { label: '2. Sub-standard', level: 0, values: [ + 10241, + 1024, + 9217 + ] }, + { label: '2.1. Of which restructured Sub-standard', level: 1, values: [ + null, + null, + 0 + ] }, + { label: '3. Doubtful', level: 0, total: true, values: [ + 16375, + 35225, + -18850 + ] }, + { label: '3.1. Doubtful upto 1 year', level: 1, values: [ + 911, + 182, + 729 + ] }, + { label: '3.2. Doubtful more than 1 year and upto 3 years', level: 1, values: [ + 5174, + 1552, + 3622 + ] }, + { label: '3.3. Doubtful more than 3 years', level: 1, values: [ + 10290, + 33491, + -23201 + ] }, + { label: '4. Loss', level: 0, values: [ + null, + null, + 0 + ] }, + { label: 'TOTAL', level: 0, total: true, values: [ + 411204, + 39999, + 371205 + ] } ] }; @@ -436,39 +470,123 @@ export const RAQ_MOVEMENT_PART_1: MatrixTable = { ], rows: [ { label: 'Migration of Advances', level: 0, header: true, values: [] }, - { label: 'From Standard', level: 1, values: [null, null, null, null, 0] }, - { label: 'From Sub-standard', level: 1, values: [null, null, null, null, 0] }, - { label: 'From Doubtful', level: 1, values: [null, null, null, null, 0] }, - { label: 'From Loss', level: 1, values: [null, null, null, null, 0] }, - { label: 'Total Upgrade/Downgrade', level: 1, total: true, values: [0, 0, 0, 0, 0] }, + { label: 'From Standard', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'From Sub-standard', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'From Doubtful', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'From Loss', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'Total Upgrade/Downgrade', level: 1, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] }, { label: 'Movement in Loans and Advances', level: 0, header: true, values: [] }, { label: 'A. Loans and advances at the beginning of the quarter (Should be equal to the figure at end of previous quarter)', level: 1, - values: [388243, 6214, 16924, null, 411381] + values: [ + 388243, + 6214, + 16924, + null, + 411381 + ] }, - { label: 'B. Loans and Advances Disbursed during the quarter', level: 1, values: [23072, 4027, null, null, 27099] }, - { label: 'C. Actual Repayments / Recoveries during the quarter', level: 1, values: [26727, null, 549, null, 27276] }, + { label: 'B. Loans and Advances Disbursed during the quarter', level: 1, values: [ + 23072, + 4027, + null, + null, + 27099 + ] }, + { label: 'C. Actual Repayments / Recoveries during the quarter', level: 1, values: [ + 26727, + null, + 549, + null, + 27276 + ] }, { label: 'D. Write-offs / Technical Write offs / Compromise etc. during the quarter', level: 1, - values: [null, null, null, null, 0] + values: [ + null, + null, + null, + null, + 0 + ] }, - { label: 'E. Others (+ / -)', level: 1, values: [null, null, null, null, 0] }, + { label: 'E. Others (+ / -)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, { label: 'F. Outstanding Loans and Advances at end of period (A+B-C-D+E)', level: 1, total: true, - values: [384588, 10241, 16375, 0, 411204] + values: [ + 384588, + 10241, + 16375, + 0, + 411204 + ] }, - { label: 'G. Provisions Required on Current Balance (Estimate)', level: 1, values: [970, 1024, 12024, null, 14018] }, - { label: 'H. Provisions Held', level: 1, values: [3750, 1024, 35225, null, 39999] }, + { label: 'G. Provisions Required on Current Balance (Estimate)', level: 1, values: [ + 970, + 1024, + 12024, + null, + 14018 + ] }, + { label: 'H. Provisions Held', level: 1, values: [ + 3750, + 1024, + 35225, + null, + 39999 + ] }, { label: 'I. Excess/Deficit in Provisions Held (G-H)', level: 1, total: true, - values: [-2780, 0, -23201, 0, -25981] + values: [ + -2780, + 0, + -23201, + 0, + -25981 + ] } ] }; @@ -484,29 +602,79 @@ export const RAQ_MOVEMENT_PART_2: MatrixTable = { 'Total' ], rows: [ - { label: 'A. NPAs at the beginning of the quarter', level: 0, values: [6214, 16924, null, 23138] }, - { label: 'B. New Addition to NPAs during the current quarter', level: 0, values: [4027, null, null, 4027] }, - { label: 'C. Reduction in NPAs during the current quarter', level: 0, total: true, values: [0, 549, 0, 549] }, - { label: 'C.1 Due to Upgradation', level: 1, values: [null, null, null, 0] }, - { label: 'C.2 Due to Actual Recoveries', level: 1, values: [null, 549, null, 549] }, - { label: 'C.3 Due to Write-offs', level: 1, values: [null, null, null, 0] }, + { label: 'A. NPAs at the beginning of the quarter', level: 0, values: [ + 6214, + 16924, + null, + 23138 + ] }, + { label: 'B. New Addition to NPAs during the current quarter', level: 0, values: [ + 4027, + null, + null, + 4027 + ] }, + { label: 'C. Reduction in NPAs during the current quarter', level: 0, total: true, values: [ + 0, + 549, + 0, + 549 + ] }, + { label: 'C.1 Due to Upgradation', level: 1, values: [ + null, + null, + null, + 0 + ] }, + { label: 'C.2 Due to Actual Recoveries', level: 1, values: [ + null, + 549, + null, + 549 + ] }, + { label: 'C.3 Due to Write-offs', level: 1, values: [ + null, + null, + null, + 0 + ] }, { label: 'C.4 Due to Regulatory Instructions, Amalgamation, Compliance to RBI Inspection Action Points, any other reason etc.', level: 1, - values: [null, null, null, 0] + values: [ + null, + null, + null, + 0 + ] }, { label: 'D. Increase due to change in Asset Classification (excl. up-gradation) during the quarter', level: 0, - values: [null, null, null, 0] + values: [ + null, + null, + null, + 0 + ] }, { label: 'E. Decrease due to change in Asset Classification (excl. up-gradation) during the quarter', level: 0, - values: [null, null, null, 0] + values: [ + null, + null, + null, + 0 + ] }, - { label: 'F. NPAs at the end of the Quarter', level: 0, total: true, values: [10241, 16375, 0, 26616] } + { label: 'F. NPAs at the end of the Quarter', level: 0, total: true, values: [ + 10241, + 16375, + 0, + 26616 + ] } ], notes: [ '1. Reporting of flow figures for New Accretion to NPAs, up-gradation, recoveries and write-offs should be on gross basis (i.e., even though the slippage and the related upgradation/recoveries is within the same reporting period). In other words, the data on up-gradation, recoveries and write-offs from new NPAs during the year should also be reported in respective categories.', @@ -546,16 +714,64 @@ export const RAQ_OTHER_RISK_ON_BS: MatrixTable = { ], rows: [ { label: '1. Inter-bank assets', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [109359, null, null, null, 109359] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Amount', level: 1, values: [ + 109359, + null, + null, + null, + 109359 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, { label: '2. SLR Securities', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [211734, null, null, null, 211734] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Amount', level: 1, values: [ + 211734, + null, + null, + null, + 211734 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, { label: '3. Receivables', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, - { label: 'Sub-total (Amount) (1+2+3)', level: 0, total: true, values: [321093, 0, 0, 0, 321093] }, - { label: 'Sub-total (Loss Provision) (1+2+3)', level: 0, total: true, values: [0, 0, 0, 0, 0] } + { label: 'Amount', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'Sub-total (Amount) (1+2+3)', level: 0, total: true, values: [ + 321093, + 0, + 0, + 0, + 321093 + ] }, + { label: 'Sub-total (Loss Provision) (1+2+3)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] } ] }; @@ -572,18 +788,78 @@ export const RAQ_OTHER_RISK_OFF_BS: MatrixTable = { ], rows: [ { label: '4. Guarantee outstanding', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Amount', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, { label: '5. Letters of credit outstanding', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, + { label: 'Amount', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, { label: '6. Other contingent credit exposures', level: 0, header: true, values: [] }, - { label: 'Amount', level: 1, values: [null, null, null, null, 0] }, - { label: '(Loss provision)', level: 1, values: [null, null, null, null, 0] }, - { label: 'Sub-total (Amount) (4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, - { label: 'Sub-total (Loss provision) (4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, - { label: 'Grand-total (Amount) (1+2+3+4+5+6)', level: 0, total: true, values: [321093, 0, 0, 0, 321093] }, - { label: 'Grand-total (Loss provision) (1+2+3+4+5+6)', level: 0, total: true, values: [0, 0, 0, 0, 0] } + { label: 'Amount', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: '(Loss provision)', level: 1, values: [ + null, + null, + null, + null, + 0 + ] }, + { label: 'Sub-total (Amount) (4+5+6)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'Sub-total (Loss provision) (4+5+6)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'Grand-total (Amount) (1+2+3+4+5+6)', level: 0, total: true, values: [ + 321093, + 0, + 0, + 0, + 321093 + ] }, + { label: 'Grand-total (Loss provision) (1+2+3+4+5+6)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] } ] }; @@ -611,7 +887,14 @@ export const RAQ_RECOVERY_TABLES: MatrixTable[] = [ 'Still outstanding — No. of accounts', 'Still outstanding — Amount involved' ], - rows: [{ label: '—', level: 0, values: [null, null, null, null, null, null] }] + rows: [{ label: '—', level: 0, values: [ + null, + null, + null, + null, + null, + null + ] }] }, { title: 'Table 3: Waiver of Debts/Written offs (Other than Compromise Settlement)', @@ -621,7 +904,12 @@ export const RAQ_RECOVERY_TABLES: MatrixTable[] = [ 'April till date — No. of accounts', 'April till date — Amount involved' ], - rows: [{ label: '—', level: 0, values: [null, null, null, null] }] + rows: [{ label: '—', level: 0, values: [ + null, + null, + null, + null + ] }] }, { title: 'Table 4: Compromise Settlements', @@ -641,7 +929,18 @@ export const RAQ_RECOVERY_TABLES: MatrixTable[] = [ { label: '—', level: 0, - values: [null, null, null, null, null, null, null, null, null, null] + values: [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] } ] } @@ -649,35 +948,325 @@ export const RAQ_RECOVERY_TABLES: MatrixTable[] = [ /** Statement 5 — Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs */ export const RAQ_IMPAIRED_CREDITS: Oss3ImpairedCredit[] = [ - { pan: 'ABKPP9779L', name: 'PAWAN TRADERS', loansAdvances: 7500, funded: 7535, interestInArrear: 329, pctTotalAdvances: 1.83, pctCapitalFunds: 7.72, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, - { pan: 'AUAPD8276F', name: 'M/S DEV ENTERPRISES', loansAdvances: 2500, funded: 2550, interestInArrear: 1346, pctTotalAdvances: 0.62, pctCapitalFunds: 2.61, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, - { pan: 'AHKPR8438E', name: 'SHRI RAM MOTERS', loansAdvances: 2000, funded: 1951, interestInArrear: 15358, pctTotalAdvances: 0.47, pctCapitalFunds: 2.0, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'BFXPD4028Q', name: 'HEMANT TRADERS', loansAdvances: 1900, funded: 1989, interestInArrear: 1265, pctTotalAdvances: 0.48, pctCapitalFunds: 2.04, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'CDXPD8520J', name: 'THAKUR TRADERS', loansAdvances: 1600, funded: 1788, interestInArrear: 1121, pctTotalAdvances: 0.43, pctCapitalFunds: 1.83, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'ALYPK3932E', name: 'KATARE KRISHI SEVA', loansAdvances: 1200, funded: 1224, interestInArrear: 431, pctTotalAdvances: 0.3, pctCapitalFunds: 1.25, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, - { pan: 'CDXPR0380J', name: 'SONU RAGHUWANSHI', loansAdvances: 1000, funded: 1040, interestInArrear: 2109, pctTotalAdvances: 0.25, pctCapitalFunds: 1.07, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'ALEPA6589P', name: 'BALAJI ENTERPRISES', loansAdvances: 950, funded: 999, interestInArrear: 80, pctTotalAdvances: 0.24, pctCapitalFunds: 1.02, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, - { pan: 'AEEPA0954P', name: 'M/S GIRRAJ TRAETOR', loansAdvances: 900, funded: 910, interestInArrear: 63, pctTotalAdvances: 0.22, pctCapitalFunds: 0.93, assetClassification: 'Doubtful 1', lossProvisionHeld: 0 }, - { pan: 'APNPJ9600B', name: 'SHREE TARAN CREATION', loansAdvances: 700, funded: 730, interestInArrear: 856, pctTotalAdvances: 0.18, pctCapitalFunds: 0.75, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'GYEPS4039B', name: 'M/S LAKSHYA ENTERPRISES', loansAdvances: 700, funded: 712, interestInArrear: 332, pctTotalAdvances: 0.17, pctCapitalFunds: 0.73, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, - { pan: 'AIKPJ8057E', name: 'M/S PRADEEP KUMAR', loansAdvances: 650, funded: 655, interestInArrear: 220, pctTotalAdvances: 0.16, pctCapitalFunds: 0.67, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, - { pan: 'AWQPA1615F', name: 'AGARAWAL SADI', loansAdvances: 600, funded: 605, interestInArrear: 21, pctTotalAdvances: 0.15, pctCapitalFunds: 0.62, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, - { pan: 'CWAPM2065H', name: 'SHANTANU MATHUR', loansAdvances: 600, funded: 593, interestInArrear: 53, pctTotalAdvances: 0.14, pctCapitalFunds: 0.61, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, - { pan: 'BJDPM9396J', name: 'SHRI NARAYAN JEWELLERS', loansAdvances: 500, funded: 506, interestInArrear: 28, pctTotalAdvances: 0.12, pctCapitalFunds: 0.52, assetClassification: 'Sub-standard', lossProvisionHeld: 0 }, - { pan: 'CEFPK8768G', name: 'KAILAS KIRANA STORE', loansAdvances: 400, funded: 414, interestInArrear: 356, pctTotalAdvances: 0.1, pctCapitalFunds: 0.42, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'CEFPK8768G', name: 'MISHRA BHANDU TRADERS', loansAdvances: 400, funded: 328, interestInArrear: 926, pctTotalAdvances: 0.08, pctCapitalFunds: 0.34, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'FKZPS2638E', name: 'CHANCHAL KANGAN STORES', loansAdvances: 400, funded: 322, interestInArrear: 215, pctTotalAdvances: 0.08, pctCapitalFunds: 0.33, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'RBIIR1099T', name: 'RAGUVEER SHING BAGHEL', loansAdvances: 400, funded: 420, interestInArrear: 2540, pctTotalAdvances: 0.1, pctCapitalFunds: 0.43, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'FQHPS2985A', name: 'SHRI KRISHNA AUTO PARTS', loansAdvances: 300, funded: 319, interestInArrear: 327, pctTotalAdvances: 0.08, pctCapitalFunds: 0.33, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'BAPPS2459K', name: 'SIGNA PUR JADI BUTI', loansAdvances: 250, funded: 26, interestInArrear: 3, pctTotalAdvances: 0.01, pctCapitalFunds: 0.03, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'AIYPY3947N', name: 'MUSKAN BEAUTI PARLAR &', loansAdvances: 200, funded: 208, interestInArrear: 173, pctTotalAdvances: 0.05, pctCapitalFunds: 0.21, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'DMYPS7044P', name: 'MANOJ SHARMA', loansAdvances: 194, funded: 229, interestInArrear: 267, pctTotalAdvances: 0.06, pctCapitalFunds: 0.23, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'BSSPP6047B', name: 'SHRI SHANTI CYCLE SARVICE', loansAdvances: 175, funded: 192, interestInArrear: 1299, pctTotalAdvances: 0.05, pctCapitalFunds: 0.2, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'RBIIM1528T', name: 'MOHAR SINGH RAJPUT', loansAdvances: 100, funded: 106, interestInArrear: 2581, pctTotalAdvances: 0.03, pctCapitalFunds: 0.11, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'RBIIH0522T', name: 'HARNAAM SINGH S/O', loansAdvances: 100, funded: 93, interestInArrear: 27, pctTotalAdvances: 0.02, pctCapitalFunds: 0.1, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'RBIIM1527T', name: 'MAHESHWARI AGENCIES', loansAdvances: 75, funded: 88, interestInArrear: 1105, pctTotalAdvances: 0.02, pctCapitalFunds: 0.09, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 }, - { pan: 'ACJPD2095F', name: 'MAHENDRA KUMAR', loansAdvances: 50, funded: 32, interestInArrear: 5, pctTotalAdvances: 0.01, pctCapitalFunds: 0.03, assetClassification: 'Doubtful 2', lossProvisionHeld: 0 }, - { pan: 'RBIIA2002T', name: 'ANAND SHARMA', loansAdvances: 50, funded: 42, interestInArrear: 0, pctTotalAdvances: 0.01, pctCapitalFunds: 0.04, assetClassification: 'Doubtful 3', lossProvisionHeld: 0 } + { + pan: 'ABKPP9779L', + name: 'PAWAN TRADERS', + loansAdvances: 7500, + funded: 7535, + interestInArrear: 329, + pctTotalAdvances: 1.83, + pctCapitalFunds: 7.72, + assetClassification: 'Sub-standard', + lossProvisionHeld: 0 + }, + { + pan: 'AUAPD8276F', + name: 'M/S DEV ENTERPRISES', + loansAdvances: 2500, + funded: 2550, + interestInArrear: 1346, + pctTotalAdvances: 0.62, + pctCapitalFunds: 2.61, + assetClassification: 'Doubtful 2', + lossProvisionHeld: 0 + }, + { + pan: 'AHKPR8438E', + name: 'SHRI RAM MOTERS', + loansAdvances: 2000, + funded: 1951, + interestInArrear: 15358, + pctTotalAdvances: 0.47, + pctCapitalFunds: 2.0, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'BFXPD4028Q', + name: 'HEMANT TRADERS', + loansAdvances: 1900, + funded: 1989, + interestInArrear: 1265, + pctTotalAdvances: 0.48, + pctCapitalFunds: 2.04, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'CDXPD8520J', + name: 'THAKUR TRADERS', + loansAdvances: 1600, + funded: 1788, + interestInArrear: 1121, + pctTotalAdvances: 0.43, + pctCapitalFunds: 1.83, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'ALYPK3932E', + name: 'KATARE KRISHI SEVA', + loansAdvances: 1200, + funded: 1224, + interestInArrear: 431, + pctTotalAdvances: 0.3, + pctCapitalFunds: 1.25, + assetClassification: 'Doubtful 2', + lossProvisionHeld: 0 + }, + { + pan: 'CDXPR0380J', + name: 'SONU RAGHUWANSHI', + loansAdvances: 1000, + funded: 1040, + interestInArrear: 2109, + pctTotalAdvances: 0.25, + pctCapitalFunds: 1.07, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'ALEPA6589P', + name: 'BALAJI ENTERPRISES', + loansAdvances: 950, + funded: 999, + interestInArrear: 80, + pctTotalAdvances: 0.24, + pctCapitalFunds: 1.02, + assetClassification: 'Sub-standard', + lossProvisionHeld: 0 + }, + { + pan: 'AEEPA0954P', + name: 'M/S GIRRAJ TRAETOR', + loansAdvances: 900, + funded: 910, + interestInArrear: 63, + pctTotalAdvances: 0.22, + pctCapitalFunds: 0.93, + assetClassification: 'Doubtful 1', + lossProvisionHeld: 0 + }, + { + pan: 'APNPJ9600B', + name: 'SHREE TARAN CREATION', + loansAdvances: 700, + funded: 730, + interestInArrear: 856, + pctTotalAdvances: 0.18, + pctCapitalFunds: 0.75, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'GYEPS4039B', + name: 'M/S LAKSHYA ENTERPRISES', + loansAdvances: 700, + funded: 712, + interestInArrear: 332, + pctTotalAdvances: 0.17, + pctCapitalFunds: 0.73, + assetClassification: 'Doubtful 2', + lossProvisionHeld: 0 + }, + { + pan: 'AIKPJ8057E', + name: 'M/S PRADEEP KUMAR', + loansAdvances: 650, + funded: 655, + interestInArrear: 220, + pctTotalAdvances: 0.16, + pctCapitalFunds: 0.67, + assetClassification: 'Doubtful 2', + lossProvisionHeld: 0 + }, + { + pan: 'AWQPA1615F', + name: 'AGARAWAL SADI', + loansAdvances: 600, + funded: 605, + interestInArrear: 21, + pctTotalAdvances: 0.15, + pctCapitalFunds: 0.62, + assetClassification: 'Sub-standard', + lossProvisionHeld: 0 + }, + { + pan: 'CWAPM2065H', + name: 'SHANTANU MATHUR', + loansAdvances: 600, + funded: 593, + interestInArrear: 53, + pctTotalAdvances: 0.14, + pctCapitalFunds: 0.61, + assetClassification: 'Sub-standard', + lossProvisionHeld: 0 + }, + { + pan: 'BJDPM9396J', + name: 'SHRI NARAYAN JEWELLERS', + loansAdvances: 500, + funded: 506, + interestInArrear: 28, + pctTotalAdvances: 0.12, + pctCapitalFunds: 0.52, + assetClassification: 'Sub-standard', + lossProvisionHeld: 0 + }, + { + pan: 'CEFPK8768G', + name: 'KAILAS KIRANA STORE', + loansAdvances: 400, + funded: 414, + interestInArrear: 356, + pctTotalAdvances: 0.1, + pctCapitalFunds: 0.42, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'CEFPK8768G', + name: 'MISHRA BHANDU TRADERS', + loansAdvances: 400, + funded: 328, + interestInArrear: 926, + pctTotalAdvances: 0.08, + pctCapitalFunds: 0.34, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'FKZPS2638E', + name: 'CHANCHAL KANGAN STORES', + loansAdvances: 400, + funded: 322, + interestInArrear: 215, + pctTotalAdvances: 0.08, + pctCapitalFunds: 0.33, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'RBIIR1099T', + name: 'RAGUVEER SHING BAGHEL', + loansAdvances: 400, + funded: 420, + interestInArrear: 2540, + pctTotalAdvances: 0.1, + pctCapitalFunds: 0.43, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'FQHPS2985A', + name: 'SHRI KRISHNA AUTO PARTS', + loansAdvances: 300, + funded: 319, + interestInArrear: 327, + pctTotalAdvances: 0.08, + pctCapitalFunds: 0.33, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'BAPPS2459K', + name: 'SIGNA PUR JADI BUTI', + loansAdvances: 250, + funded: 26, + interestInArrear: 3, + pctTotalAdvances: 0.01, + pctCapitalFunds: 0.03, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'AIYPY3947N', + name: 'MUSKAN BEAUTI PARLAR &', + loansAdvances: 200, + funded: 208, + interestInArrear: 173, + pctTotalAdvances: 0.05, + pctCapitalFunds: 0.21, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'DMYPS7044P', + name: 'MANOJ SHARMA', + loansAdvances: 194, + funded: 229, + interestInArrear: 267, + pctTotalAdvances: 0.06, + pctCapitalFunds: 0.23, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'BSSPP6047B', + name: 'SHRI SHANTI CYCLE SARVICE', + loansAdvances: 175, + funded: 192, + interestInArrear: 1299, + pctTotalAdvances: 0.05, + pctCapitalFunds: 0.2, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'RBIIM1528T', + name: 'MOHAR SINGH RAJPUT', + loansAdvances: 100, + funded: 106, + interestInArrear: 2581, + pctTotalAdvances: 0.03, + pctCapitalFunds: 0.11, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'RBIIH0522T', + name: 'HARNAAM SINGH S/O', + loansAdvances: 100, + funded: 93, + interestInArrear: 27, + pctTotalAdvances: 0.02, + pctCapitalFunds: 0.1, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'RBIIM1527T', + name: 'MAHESHWARI AGENCIES', + loansAdvances: 75, + funded: 88, + interestInArrear: 1105, + pctTotalAdvances: 0.02, + pctCapitalFunds: 0.09, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + }, + { + pan: 'ACJPD2095F', + name: 'MAHENDRA KUMAR', + loansAdvances: 50, + funded: 32, + interestInArrear: 5, + pctTotalAdvances: 0.01, + pctCapitalFunds: 0.03, + assetClassification: 'Doubtful 2', + lossProvisionHeld: 0 + }, + { + pan: 'RBIIA2002T', + name: 'ANAND SHARMA', + loansAdvances: 50, + funded: 42, + interestInArrear: 0, + pctTotalAdvances: 0.01, + pctCapitalFunds: 0.04, + assetClassification: 'Doubtful 3', + lossProvisionHeld: 0 + } ]; /** Statement 6 — Table 1: Capital Position as on Previous March */ @@ -694,25 +1283,177 @@ export const RAQ_CAPITAL_PREVIOUS_MARCH: MatrixTable = { /** Statement 6 — Table 2: Large exposure, single borrower */ export const RAQ_LARGE_EXPOSURES: Oss3LargeExposure[] = [ - { pan: 'AAEFR4731M', name: 'RATAN CHAND VIMAL CHAND', limitSanctioned: 10000, pctTotalAdvances: 2.43, outstandingBalance: 10002, pctTier1Capital: 11.12, assetClassification: 'Standard' }, - { pan: 'ADMPJ6589N', name: 'DEEPAK TRADING COMPANY', limitSanctioned: 8000, pctTotalAdvances: 1.95, outstandingBalance: 7601, pctTier1Capital: 8.45, assetClassification: 'Standard' }, - { pan: 'ACRPT3542M', name: 'VISHWAMBHARA STORES', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7524, pctTier1Capital: 8.37, assetClassification: 'Standard' }, - { pan: 'ABKPP9779L', name: 'PAWAN TRADERS', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7627, pctTier1Capital: 8.48, assetClassification: 'Standard' }, - { pan: 'AAHPT8829R', name: 'VISHWAMBHARA AGENCIES', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7533, pctTier1Capital: 8.38, assetClassification: 'Standard' }, - { pan: 'ABKPP9778M', name: 'M/S TIRUMALA TRADERS', limitSanctioned: 7500, pctTotalAdvances: 1.82, outstandingBalance: 7562, pctTier1Capital: 8.41, assetClassification: 'Standard' }, - { pan: 'ANGPG7237G', name: 'YASHI ENTERPRISE', limitSanctioned: 6500, pctTotalAdvances: 1.58, outstandingBalance: 6539, pctTier1Capital: 7.27, assetClassification: 'Standard' }, - { pan: 'ADDPS3571L', name: 'PRATEEK ENTERPRISES', limitSanctioned: 6400, pctTotalAdvances: 1.56, outstandingBalance: 6438, pctTier1Capital: 7.16, assetClassification: 'Standard' }, - { pan: 'ANYPS4198L', name: 'RAJA RAM RAMPRASAD SAHU', limitSanctioned: 6000, pctTotalAdvances: 1.46, outstandingBalance: 6059, pctTier1Capital: 6.74, assetClassification: 'Standard' }, - { pan: 'AOVPJ4417P', name: 'JAIN SHREE FLORA MIL', limitSanctioned: 6000, pctTotalAdvances: 1.46, outstandingBalance: 5305, pctTier1Capital: 5.9, assetClassification: 'Standard' }, - { pan: 'BFMPJ0127D', name: 'NEHA JAIN', limitSanctioned: 5800, pctTotalAdvances: 1.41, outstandingBalance: 5549, pctTier1Capital: 6.17, assetClassification: 'Standard' }, - { pan: 'AFWPP7649M', name: 'M/S SANJAY PALOD &CO. PRO. SANJAY PALOD', limitSanctioned: 5500, pctTotalAdvances: 1.34, outstandingBalance: 5475, pctTier1Capital: 6.09, assetClassification: 'Standard' }, - { pan: 'ADPPA9025K', name: 'RAJNI ENTERPRISES PRO. RAJNI DEVI AGARWAL', limitSanctioned: 5250, pctTotalAdvances: 1.28, outstandingBalance: 5605, pctTier1Capital: 6.23, assetClassification: 'Standard' }, - { pan: 'ADPPA6372F', name: 'LAXMINARAYANRAJESH KUMAR', limitSanctioned: 5010, pctTotalAdvances: 1.22, outstandingBalance: 4134, pctTier1Capital: 4.6, assetClassification: 'Standard' }, - { pan: 'CFSPR8020J', name: 'M/S KRIT TRADERS PRO.RANI RAGHUWANSHI', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 5004, pctTier1Capital: 5.56, assetClassification: 'Standard' }, - { pan: 'AATPA9975H', name: 'M/S SUDARSHAN ENTERPRISES PRO.BHARTI AGRAWAL', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 3357, pctTier1Capital: 3.73, assetClassification: 'Standard' }, - { pan: 'ATEPB5535A', name: 'ANSHLEKHA BHAWSAR', limitSanctioned: 5000, pctTotalAdvances: 1.22, outstandingBalance: 4909, pctTier1Capital: 5.46, assetClassification: 'Standard' }, - { pan: 'AXNPA7126R', name: 'IFTI KHAR AHMAD', limitSanctioned: 4700, pctTotalAdvances: 1.14, outstandingBalance: 3590, pctTier1Capital: 3.99, assetClassification: 'Standard' }, - { pan: 'ABBPP3524A', name: 'RAM NARAYAN PALOD', limitSanctioned: 4625, pctTotalAdvances: 1.12, outstandingBalance: 4667, pctTier1Capital: 5.19, assetClassification: 'Standard' } + { + pan: 'AAEFR4731M', + name: 'RATAN CHAND VIMAL CHAND', + limitSanctioned: 10000, + pctTotalAdvances: 2.43, + outstandingBalance: 10002, + pctTier1Capital: 11.12, + assetClassification: 'Standard' + }, + { + pan: 'ADMPJ6589N', + name: 'DEEPAK TRADING COMPANY', + limitSanctioned: 8000, + pctTotalAdvances: 1.95, + outstandingBalance: 7601, + pctTier1Capital: 8.45, + assetClassification: 'Standard' + }, + { + pan: 'ACRPT3542M', + name: 'VISHWAMBHARA STORES', + limitSanctioned: 7500, + pctTotalAdvances: 1.82, + outstandingBalance: 7524, + pctTier1Capital: 8.37, + assetClassification: 'Standard' + }, + { + pan: 'ABKPP9779L', + name: 'PAWAN TRADERS', + limitSanctioned: 7500, + pctTotalAdvances: 1.82, + outstandingBalance: 7627, + pctTier1Capital: 8.48, + assetClassification: 'Standard' + }, + { + pan: 'AAHPT8829R', + name: 'VISHWAMBHARA AGENCIES', + limitSanctioned: 7500, + pctTotalAdvances: 1.82, + outstandingBalance: 7533, + pctTier1Capital: 8.38, + assetClassification: 'Standard' + }, + { + pan: 'ABKPP9778M', + name: 'M/S TIRUMALA TRADERS', + limitSanctioned: 7500, + pctTotalAdvances: 1.82, + outstandingBalance: 7562, + pctTier1Capital: 8.41, + assetClassification: 'Standard' + }, + { + pan: 'ANGPG7237G', + name: 'YASHI ENTERPRISE', + limitSanctioned: 6500, + pctTotalAdvances: 1.58, + outstandingBalance: 6539, + pctTier1Capital: 7.27, + assetClassification: 'Standard' + }, + { + pan: 'ADDPS3571L', + name: 'PRATEEK ENTERPRISES', + limitSanctioned: 6400, + pctTotalAdvances: 1.56, + outstandingBalance: 6438, + pctTier1Capital: 7.16, + assetClassification: 'Standard' + }, + { + pan: 'ANYPS4198L', + name: 'RAJA RAM RAMPRASAD SAHU', + limitSanctioned: 6000, + pctTotalAdvances: 1.46, + outstandingBalance: 6059, + pctTier1Capital: 6.74, + assetClassification: 'Standard' + }, + { + pan: 'AOVPJ4417P', + name: 'JAIN SHREE FLORA MIL', + limitSanctioned: 6000, + pctTotalAdvances: 1.46, + outstandingBalance: 5305, + pctTier1Capital: 5.9, + assetClassification: 'Standard' + }, + { + pan: 'BFMPJ0127D', + name: 'NEHA JAIN', + limitSanctioned: 5800, + pctTotalAdvances: 1.41, + outstandingBalance: 5549, + pctTier1Capital: 6.17, + assetClassification: 'Standard' + }, + { + pan: 'AFWPP7649M', + name: 'M/S SANJAY PALOD &CO. PRO. SANJAY PALOD', + limitSanctioned: 5500, + pctTotalAdvances: 1.34, + outstandingBalance: 5475, + pctTier1Capital: 6.09, + assetClassification: 'Standard' + }, + { + pan: 'ADPPA9025K', + name: 'RAJNI ENTERPRISES PRO. RAJNI DEVI AGARWAL', + limitSanctioned: 5250, + pctTotalAdvances: 1.28, + outstandingBalance: 5605, + pctTier1Capital: 6.23, + assetClassification: 'Standard' + }, + { + pan: 'ADPPA6372F', + name: 'LAXMINARAYANRAJESH KUMAR', + limitSanctioned: 5010, + pctTotalAdvances: 1.22, + outstandingBalance: 4134, + pctTier1Capital: 4.6, + assetClassification: 'Standard' + }, + { + pan: 'CFSPR8020J', + name: 'M/S KRIT TRADERS PRO.RANI RAGHUWANSHI', + limitSanctioned: 5000, + pctTotalAdvances: 1.22, + outstandingBalance: 5004, + pctTier1Capital: 5.56, + assetClassification: 'Standard' + }, + { + pan: 'AATPA9975H', + name: 'M/S SUDARSHAN ENTERPRISES PRO.BHARTI AGRAWAL', + limitSanctioned: 5000, + pctTotalAdvances: 1.22, + outstandingBalance: 3357, + pctTier1Capital: 3.73, + assetClassification: 'Standard' + }, + { + pan: 'ATEPB5535A', + name: 'ANSHLEKHA BHAWSAR', + limitSanctioned: 5000, + pctTotalAdvances: 1.22, + outstandingBalance: 4909, + pctTier1Capital: 5.46, + assetClassification: 'Standard' + }, + { + pan: 'AXNPA7126R', + name: 'IFTI KHAR AHMAD', + limitSanctioned: 4700, + pctTotalAdvances: 1.14, + outstandingBalance: 3590, + pctTier1Capital: 3.99, + assetClassification: 'Standard' + }, + { + pan: 'ABBPP3524A', + name: 'RAM NARAYAN PALOD', + limitSanctioned: 4625, + pctTotalAdvances: 1.12, + outstandingBalance: 4667, + pctTier1Capital: 5.19, + assetClassification: 'Standard' + } ]; export const RAQ_EXPOSURE_NOTES: string[] = [ @@ -733,31 +1474,157 @@ export const RAQ_INVESTMENT_QUALITY: MatrixTable = { 'Market Value — Held for Trading' ], rows: [ - { label: 'A. SLR Investments - Total (i+ii+iii+iv)', level: 0, total: true, values: [151734, 60000, 0, 0, 0] }, - { label: '(i) GOI Treasury bills', level: 1, values: [null, null, null, null, null] }, - { label: '(ii) GOI Securities(Dated)', level: 1, values: [63838, null, null, null, null] }, - { label: '(iii) State Govt. Securities(Dated)', level: 1, values: [87896, 60000, null, null, null] }, - { label: '(iv) Other approved Securities', level: 1, values: [null, null, null, null, null] }, - { label: 'B. Debt Securities - Total (Ba + Bb)', level: 0, total: true, values: [131583, 0, 0, 0, 0] }, - { label: 'Ba. Debt Securities - Quoted - Total(i+ii+iii+iv)', level: 1, total: true, values: [24234, 0, 0, 0, 0] }, - { label: '(i) PSU bonds', level: 2, values: [null, null, null, null, null] }, - { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [24234, null, null, null, null] }, - { label: '(iii) Bank Certificate of Deposits', level: 2, values: [null, null, null, null, null] }, - { label: '(iv) All others excluding equities', level: 2, values: [null, null, null, null, null] }, - { label: 'Bb. Debt Securities - Unquoted-Total(i+ii+iii+iv)', level: 1, total: true, values: [107349, 0, 0, 0, 0] }, - { label: '(i) PSU bonds', level: 2, values: [null, null, null, null, null] }, - { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [null, null, null, null, null] }, - { label: '(iii) Bank Certificate of Deposits', level: 2, values: [107287, null, null, null, null] }, - { label: '(iv) All others excluding equities', level: 2, values: [62, null, null, null, null] }, - { label: 'C. Equities - Total (i+ii)', level: 0, total: true, values: [0, 0, 0, 0, 0] }, - { label: '(i) Quoted', level: 1, values: [null, null, null, null, null] }, - { label: '(ii) Unquoted', level: 1, values: [null, null, null, null, null] }, + { label: 'A. SLR Investments - Total (i+ii+iii+iv)', level: 0, total: true, values: [ + 151734, + 60000, + 0, + 0, + 0 + ] }, + { label: '(i) GOI Treasury bills', level: 1, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(ii) GOI Securities(Dated)', level: 1, values: [ + 63838, + null, + null, + null, + null + ] }, + { label: '(iii) State Govt. Securities(Dated)', level: 1, values: [ + 87896, + 60000, + null, + null, + null + ] }, + { label: '(iv) Other approved Securities', level: 1, values: [ + null, + null, + null, + null, + null + ] }, + { label: 'B. Debt Securities - Total (Ba + Bb)', level: 0, total: true, values: [ + 131583, + 0, + 0, + 0, + 0 + ] }, + { label: 'Ba. Debt Securities - Quoted - Total(i+ii+iii+iv)', level: 1, total: true, values: [ + 24234, + 0, + 0, + 0, + 0 + ] }, + { label: '(i) PSU bonds', level: 2, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [ + 24234, + null, + null, + null, + null + ] }, + { label: '(iii) Bank Certificate of Deposits', level: 2, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(iv) All others excluding equities', level: 2, values: [ + null, + null, + null, + null, + null + ] }, + { label: 'Bb. Debt Securities - Unquoted-Total(i+ii+iii+iv)', level: 1, total: true, values: [ + 107349, + 0, + 0, + 0, + 0 + ] }, + { label: '(i) PSU bonds', level: 2, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(ii) Mutual funds (debt oriented)', level: 2, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(iii) Bank Certificate of Deposits', level: 2, values: [ + 107287, + null, + null, + null, + null + ] }, + { label: '(iv) All others excluding equities', level: 2, values: [ + 62, + null, + null, + null, + null + ] }, + { label: 'C. Equities - Total (i+ii)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0 + ] }, + { label: '(i) Quoted', level: 1, values: [ + null, + null, + null, + null, + null + ] }, + { label: '(ii) Unquoted', level: 1, values: [ + null, + null, + null, + null, + null + ] }, { label: 'Out of C Equities -Total - investments in equity - oriented mutual funds', level: 1, - values: [null, null, null, null, null] + values: [ + null, + null, + null, + null, + null + ] }, - { label: 'Total Investments (A+B+C)', level: 0, total: true, values: [283317, 60000, 0, 0, 0] } + { label: 'Total Investments (A+B+C)', level: 0, total: true, values: [ + 283317, + 60000, + 0, + 0, + 0 + ] } ] }; @@ -776,17 +1643,116 @@ export const RAQ_NON_SLR_SECURITIES: MatrixTable = { 'Total Provisions' ], rows: [ - { label: 'Total Quoted - I', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, - { label: '1. PSU Bonds', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: '2. Mutual funds (Including Units)', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: '3. Others', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: 'Total Unquoted - II', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, - { label: '1. Mutual funds (Including Units)', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: '2. Others', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: 'Equities of AIFIs: - III', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] }, - { label: '1. Quoted', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: '2. Unquoted', level: 1, values: [null, null, null, null, null, null, null, null] }, - { label: 'Grand Total (I+II+III)', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0] } + { label: 'Total Quoted - I', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: '1. PSU Bonds', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: '2. Mutual funds (Including Units)', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: '3. Others', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: 'Total Unquoted - II', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: '1. Mutual funds (Including Units)', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: '2. Others', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: 'Equities of AIFIs: - III', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: '1. Quoted', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: '2. Unquoted', level: 1, values: [ + null, + null, + null, + null, + null, + null, + null, + null + ] }, + { label: 'Grand Total (I+II+III)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] } ] }; @@ -800,60 +1766,260 @@ export const RAQ_SECTORAL_CREDIT: MatrixTable = { 'Provisions made' ], rows: [ - { label: 'I. Gross Advances (II + III)', level: 0, total: true, values: [411381, 23138, 39999] }, - { label: 'II. Food Credit', level: 0, values: [null, null, null] }, - { label: 'III. Non-Food Credit ( 1 to 5)', level: 0, total: true, values: [411381, 23138, 39999] }, - { label: '1. Agriculture and Allied Activities', level: 1, values: [4001, 227, 227] }, - { label: '2. Industry (2.1 to 2.4)', level: 1, total: true, values: [0, 0, 0] }, - { label: '2.1 Micro', level: 2, values: [null, null, null] }, - { label: '2.2 Small', level: 2, values: [null, null, null] }, - { label: '2.3 Medium', level: 2, values: [null, null, null] }, - { label: '2.4 Large', level: 2, values: [null, null, null] }, - { label: '3. Services (3.1 to 3.9 or 3.a to 3.d)', level: 1, total: true, values: [256126, 4417, 3163] }, - { label: '3.1 Transport Operators', level: 2, values: [null, null, null] }, - { label: '3.2 Post and Telecommunication Services', level: 2, values: [null, null, null] }, - { label: '3.3 Tourism, Hotel and Restaurants', level: 2, values: [null, null, null] }, - { label: '3.4 Trade', level: 2, total: true, values: [256126, 4417, 3163] }, - { label: '3.4.1 Wholesale Trade (other than Food Procurement)', level: 3, values: [222886, 4417, 3163] }, - { label: '3.4.2 Retail Trade', level: 3, values: [33240, null, null] }, - { label: '3.5 Financial Intermediation', level: 2, total: true, values: [0, 0, 0] }, - { label: '3.5.1 Banking & Finance other than NBFCs and DFIs', level: 3, total: true, values: [0, 0, 0] }, - { label: '3.5.1.1 Scheduled Commercial Banks', level: 3, values: [null, null, null] }, - { label: '3.5.1.2 Co-operative Banks', level: 3, total: true, values: [0, 0, 0] }, - { label: '3.5.1.2.1 Urban Cooperative Banks', level: 3, values: [null, null, null] }, - { label: '3.5.1.2.2 District Central Cooperative Banks', level: 3, values: [null, null, null] }, - { label: '3.5.1.2.3 Other Cooperative Banks', level: 3, values: [null, null, null] }, - { label: '3.5.2 MFIs, HFCs and NBFCs other than DFIs', level: 3, total: true, values: [0, 0, 0] }, - { label: '3.5.2.1 Micro Financial Institutions (MFIs)', level: 3, values: [null, null, null] }, - { label: '3.5.2.2 Housing Finance Companies (HFCs)', level: 3, values: [null, null, null] }, - { label: '3.5.2.3 Other NBFCs', level: 3, values: [null, null, null] }, - { label: '3.5.3 Other Financial Intermediation (incl DFIs)', level: 3, values: [null, null, null] }, - { label: '3.6 Real Estate Activities', level: 2, total: true, values: [0, 0, 0] }, - { label: '3.6.1 Commercial Real Estate (CRE)', level: 3, values: [null, null, null] }, + { label: 'I. Gross Advances (II + III)', level: 0, total: true, values: [ + 411381, + 23138, + 39999 + ] }, + { label: 'II. Food Credit', level: 0, values: [ + null, + null, + null + ] }, + { label: 'III. Non-Food Credit ( 1 to 5)', level: 0, total: true, values: [ + 411381, + 23138, + 39999 + ] }, + { label: '1. Agriculture and Allied Activities', level: 1, values: [ + 4001, + 227, + 227 + ] }, + { label: '2. Industry (2.1 to 2.4)', level: 1, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '2.1 Micro', level: 2, values: [ + null, + null, + null + ] }, + { label: '2.2 Small', level: 2, values: [ + null, + null, + null + ] }, + { label: '2.3 Medium', level: 2, values: [ + null, + null, + null + ] }, + { label: '2.4 Large', level: 2, values: [ + null, + null, + null + ] }, + { label: '3. Services (3.1 to 3.9 or 3.a to 3.d)', level: 1, total: true, values: [ + 256126, + 4417, + 3163 + ] }, + { label: '3.1 Transport Operators', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.2 Post and Telecommunication Services', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.3 Tourism, Hotel and Restaurants', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.4 Trade', level: 2, total: true, values: [ + 256126, + 4417, + 3163 + ] }, + { label: '3.4.1 Wholesale Trade (other than Food Procurement)', level: 3, values: [ + 222886, + 4417, + 3163 + ] }, + { label: '3.4.2 Retail Trade', level: 3, values: [ + 33240, + null, + null + ] }, + { label: '3.5 Financial Intermediation', level: 2, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '3.5.1 Banking & Finance other than NBFCs and DFIs', level: 3, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '3.5.1.1 Scheduled Commercial Banks', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.1.2 Co-operative Banks', level: 3, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '3.5.1.2.1 Urban Cooperative Banks', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.1.2.2 District Central Cooperative Banks', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.1.2.3 Other Cooperative Banks', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.2 MFIs, HFCs and NBFCs other than DFIs', level: 3, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '3.5.2.1 Micro Financial Institutions (MFIs)', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.2.2 Housing Finance Companies (HFCs)', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.2.3 Other NBFCs', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.5.3 Other Financial Intermediation (incl DFIs)', level: 3, values: [ + null, + null, + null + ] }, + { label: '3.6 Real Estate Activities', level: 2, total: true, values: [ + 0, + 0, + 0 + ] }, + { label: '3.6.1 Commercial Real Estate (CRE)', level: 3, values: [ + null, + null, + null + ] }, { label: '3.6.2 Other Real Estate Activites, other than Housing Loans (Reported in Sec 4.1)', level: 3, - values: [null, null, null] + values: [ + null, + null, + null + ] }, - { label: '3.7 Computer and Related Activities', level: 2, values: [null, null, null] }, - { label: '3.8 Professional Services', level: 2, values: [null, null, null] }, - { label: '3.9 Other Services', level: 2, values: [null, null, null] }, - { label: '3.a Micro', level: 2, values: [222886, 4417, 3163] }, - { label: '3.b Small', level: 2, values: [33240, null, null] }, - { label: '3.c Medium', level: 2, values: [null, null, null] }, - { label: '3.d Large', level: 2, values: [null, null, null] }, - { label: '4. Retail Loans (4.1 to 4.8 )', level: 1, total: true, values: [151254, 18494, 36609] }, - { label: '4.1 Housing Loans (incl. priority sector Housing)', level: 2, total: true, values: [10241, 1559, 1559] }, - { label: '4.1.1 Loans > 30 Lakhs', level: 3, values: [null, null, null] }, - { label: '4.1.2 Loans <= 30 Lakhs', level: 3, values: [10241, 1559, 1559] }, - { label: '4.2 Consumer Durables', level: 2, values: [null, null, null] }, - { label: '4.3 Credit Card Receivables', level: 2, values: [null, null, null] }, - { label: '4.4 Vehicle/Auto Loans', level: 2, values: [null, null, null] }, - { label: '4.5 Education Loans', level: 2, values: [2403, null, null] }, - { label: '4.6 Advances against Fixed Deposits (incl. FCNR(B), etc.)', level: 2, values: [13201, null, null] }, - { label: '4.7 Advances to Individuals against Shares, Bonds, etc.', level: 2, values: [null, null, null] }, - { label: '4.8 Other Retail Loans', level: 2, values: [125409, 16935, 35050] }, - { label: '5. Other Non-food Credit, if any, please specify', level: 1, total: true, values: [0, 0, 0] } + { label: '3.7 Computer and Related Activities', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.8 Professional Services', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.9 Other Services', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.a Micro', level: 2, values: [ + 222886, + 4417, + 3163 + ] }, + { label: '3.b Small', level: 2, values: [ + 33240, + null, + null + ] }, + { label: '3.c Medium', level: 2, values: [ + null, + null, + null + ] }, + { label: '3.d Large', level: 2, values: [ + null, + null, + null + ] }, + { label: '4. Retail Loans (4.1 to 4.8 )', level: 1, total: true, values: [ + 151254, + 18494, + 36609 + ] }, + { label: '4.1 Housing Loans (incl. priority sector Housing)', level: 2, total: true, values: [ + 10241, + 1559, + 1559 + ] }, + { label: '4.1.1 Loans > 30 Lakhs', level: 3, values: [ + null, + null, + null + ] }, + { label: '4.1.2 Loans <= 30 Lakhs', level: 3, values: [ + 10241, + 1559, + 1559 + ] }, + { label: '4.2 Consumer Durables', level: 2, values: [ + null, + null, + null + ] }, + { label: '4.3 Credit Card Receivables', level: 2, values: [ + null, + null, + null + ] }, + { label: '4.4 Vehicle/Auto Loans', level: 2, values: [ + null, + null, + null + ] }, + { label: '4.5 Education Loans', level: 2, values: [ + 2403, + null, + null + ] }, + { label: '4.6 Advances against Fixed Deposits (incl. FCNR(B), etc.)', level: 2, values: [ + 13201, + null, + null + ] }, + { label: '4.7 Advances to Individuals against Shares, Bonds, etc.', level: 2, values: [ + null, + null, + null + ] }, + { label: '4.8 Other Retail Loans', level: 2, values: [ + 125409, + 16935, + 35050 + ] }, + { label: '5. Other Non-food Credit, if any, please specify', level: 1, total: true, values: [ + 0, + 0, + 0 + ] } ] }; @@ -867,9 +2033,21 @@ export const RAQ_SECTORAL_MEMO: MatrixTable = { 'Provisions made' ], rows: [ - { label: 'Total advances against pledge of gold/ silver ornamnets', level: 0, values: [null, null, null] }, - { label: 'Gold loans under bullet repayment scheme', level: 0, values: [null, null, null] }, - { label: 'SME Loans', level: 0, total: true, values: [256126, 4417, 3163] } + { label: 'Total advances against pledge of gold/ silver ornamnets', level: 0, values: [ + null, + null, + null + ] }, + { label: 'Gold loans under bullet repayment scheme', level: 0, values: [ + null, + null, + null + ] }, + { label: 'SME Loans', level: 0, total: true, values: [ + 256126, + 4417, + 3163 + ] } ] }; @@ -885,33 +2063,138 @@ export const RAQ_SEGMENT_ANALYSIS: MatrixTable = { ], rows: [ { label: 'PRIORITY SECTOR', level: 0, header: true, values: [] }, - { label: 'i. Retail Trade', level: 1, values: [163, 222886, 4417, 3163] }, - { label: 'ii. Small Business', level: 1, values: [5, 15421, null, null] }, - { label: 'iii. Small scale industries', level: 1, values: [5, 17819, null, null] }, + { label: 'i. Retail Trade', level: 1, values: [ + 163, + 222886, + 4417, + 3163 + ] }, + { label: 'ii. Small Business', level: 1, values: [ + 5, + 15421, + null, + null + ] }, + { label: 'iii. Small scale industries', level: 1, values: [ + 5, + 17819, + null, + null + ] }, { label: 'iv. Housing Loans (Purchase / Construction of dwelling units and / or repairs to the damaged dwelling units)', level: 1, - values: [31, 10241, 1559, 1559] + values: [ + 31, + 10241, + 1559, + 1559 + ] }, - { label: 'v. Agricultural & Allied activities', level: 1, values: [8, 4001, 227, 227] }, - { label: 'vi. Small Road Transport Operators', level: 1, values: [null, null, null, null] }, - { label: 'vii. Professional and Self Employed artisans and craftsmen', level: 1, values: [null, null, null, null] }, - { label: 'viii. Education', level: 1, values: [32, 2403, null, null] }, - { label: 'ix. Other Priority Sectors', level: 1, values: [2, 40, 40, 40] }, - { label: 'TOTAL PRIORITY SECTOR', level: 0, total: true, values: [246, 272811, 6243, 4989] }, + { label: 'v. Agricultural & Allied activities', level: 1, values: [ + 8, + 4001, + 227, + 227 + ] }, + { label: 'vi. Small Road Transport Operators', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'vii. Professional and Self Employed artisans and craftsmen', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'viii. Education', level: 1, values: [ + 32, + 2403, + null, + null + ] }, + { label: 'ix. Other Priority Sectors', level: 1, values: [ + 2, + 40, + 40, + 40 + ] }, + { label: 'TOTAL PRIORITY SECTOR', level: 0, total: true, values: [ + 246, + 272811, + 6243, + 4989 + ] }, { label: 'NON PRIORITY SECTOR', level: 0, header: true, values: [] }, - { label: 'i. Medium and Large Industries', level: 1, values: [null, null, null, null] }, - { label: 'ii. Export Trade', level: 1, values: [null, null, null, null] }, - { label: 'iii. Banks', level: 1, values: [null, null, null, null] }, - { label: 'iv. Non-Bank Financial Institution', level: 1, values: [null, null, null, null] }, - { label: 'v. Governments (Central / States)', level: 1, values: [null, null, null, null] }, - { label: 'vi. Food Credit (FCI consortium)', level: 1, values: [null, null, null, null] }, - { label: 'vii. Real Estate', level: 1, values: [null, null, null, null] }, - { label: 'viii. Other PSUs', level: 1, values: [null, null, null, null] }, - { label: 'ix. All Other', level: 1, values: [126, 138570, 16895, 35010] }, - { label: 'TOTAL NON-PRIORITY SECTOR', level: 0, total: true, values: [126, 138570, 16895, 35010] }, - { label: 'TOTAL (Priority Sector + Non-Priority Sector)', level: 0, total: true, values: [372, 411381, 23138, 39999] } + { label: 'i. Medium and Large Industries', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'ii. Export Trade', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'iii. Banks', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'iv. Non-Bank Financial Institution', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'v. Governments (Central / States)', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'vi. Food Credit (FCI consortium)', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'vii. Real Estate', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'viii. Other PSUs', level: 1, values: [ + null, + null, + null, + null + ] }, + { label: 'ix. All Other', level: 1, values: [ + 126, + 138570, + 16895, + 35010 + ] }, + { label: 'TOTAL NON-PRIORITY SECTOR', level: 0, total: true, values: [ + 126, + 138570, + 16895, + 35010 + ] }, + { label: 'TOTAL (Priority Sector + Non-Priority Sector)', level: 0, total: true, values: [ + 372, + 411381, + 23138, + 39999 + ] } ], notes: [ '1. In case of overlapping in classification of accounts in various Sectors/Industries, the classification may be done from top to bottom.' @@ -934,58 +2217,368 @@ export const RAQ_INDUSTRY_BREAKUP: MatrixTable = { 'Provisions — Total' ], rows: [ - { label: 'A. Mining and Quarrying', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'B. Food Processing', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, - { label: 'B.1 Sugar', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'B.2 Edible Oils and Vanaspati', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'B.3 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'A. Mining and Quarrying', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'B. Food Processing', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'B.1 Sugar', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'B.2 Edible Oils and Vanaspati', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'B.3 Others', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, { label: 'C. Beverages (excluding Tea & Coffee) and Tobacco', level: 0, total: true, - values: [0, 0, 0, 0, 0, 0, 0, 0, 0] + values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, - { label: 'C.1 Tobacco and tobacco products', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'C.2 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'D. Textiles', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, - { label: 'D.1 Cotton', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'D.2 Jute', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'D.3 Man-made', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'D.4 Others', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'E. Leather and Leather products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'F. Wood and Wood Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'G. Paper and Paper Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'C.1 Tobacco and tobacco products', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'C.2 Others', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'D. Textiles', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'D.1 Cotton', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'D.2 Jute', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'D.3 Man-made', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'D.4 Others', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'E. Leather and Leather products', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'F. Wood and Wood Products', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'G. Paper and Paper Products', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, { label: 'H. Petroleum (non-infra), Coal Products (non-mining) and Nuclear Fuels', level: 0, - values: [null, null, null, null, 0, null, null, null, 0] + values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, { label: 'I. Chemicals and Chemical Products (Dyes, Paints, etc.)', level: 0, - values: [null, null, null, null, 0, null, null, null, 0] + values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, - { label: 'J. Rubber, Plastic and their Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'J. Rubber, Plastic and their Products', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, { label: 'K. Glass, Glassware and other non-metallic mineral products (Except Cement and Cement products)', level: 0, - values: [null, null, null, null, 0, null, null, null, 0] + values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, - { label: 'L. Cement and Cement Products', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'M. Basic Metal and Metal Products', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, - { label: 'M.1 Iron and Steel', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'M.2 Other Metal and Metal Products', level: 1, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'N. All Engineering', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, + { label: 'L. Cement and Cement Products', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'M. Basic Metal and Metal Products', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'M.1 Iron and Steel', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'M.2 Other Metal and Metal Products', level: 1, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'N. All Engineering', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, { label: 'O. Vehicles, Vehicle Parts and Transport Equipments', level: 0, - values: [null, null, null, null, 0, null, null, null, 0] + values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, - { label: 'P. Gems and Jewellery', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'Q. Construction', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'R. Infrastructure', level: 0, values: [null, null, null, null, 0, null, null, null, 0] }, - { label: 'S. Other Industries, pl. specify', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] }, - { label: 'All Industries (A to S)', level: 0, total: true, values: [0, 0, 0, 0, 0, 0, 0, 0, 0] } + { label: 'P. Gems and Jewellery', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'Q. Construction', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'R. Infrastructure', level: 0, values: [ + null, + null, + null, + null, + 0, + null, + null, + null, + 0 + ] }, + { label: 'S. Other Industries, pl. specify', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] }, + { label: 'All Industries (A to S)', level: 0, total: true, values: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] } ], notes: [ '1. In case of overlapping in classification of accounts in various Sectors/Industries, the classification may be done from top to bottom.' @@ -1002,28 +2595,76 @@ export const RAQ_CAPITAL_MARKET: MatrixTable = { ], rows: [ { label: 'A1. Advances to Capital Market (Fund based)', level: 0, header: true, values: [] }, - { label: '1. Individuals', level: 1, values: [null, null] }, - { label: '2. Market Makers', level: 1, values: [null, null] }, - { label: '3. All other borrowers against Security of Shares', level: 1, values: [null, null] }, - { label: '4. Any other (Pl. specify)', level: 1, total: true, values: [0, 0] }, - { label: 'Total Advances to Capital Market (1 to 4)', level: 0, total: true, values: [0, 0] }, + { label: '1. Individuals', level: 1, values: [ + null, + null + ] }, + { label: '2. Market Makers', level: 1, values: [ + null, + null + ] }, + { label: '3. All other borrowers against Security of Shares', level: 1, values: [ + null, + null + ] }, + { label: '4. Any other (Pl. specify)', level: 1, total: true, values: [ + 0, + 0 + ] }, + { label: 'Total Advances to Capital Market (1 to 4)', level: 0, total: true, values: [ + 0, + 0 + ] }, { label: 'A2. Advances to Capital Market (Non-fund based)', level: 0, header: true, values: [] }, { label: '1. Financial Guarantees issued to Stock Exchanges on behalf of Stock Brokers', level: 1, - values: [null, null] + values: [ + null, + null + ] }, - { label: '2. Other Financial Guarantees', level: 1, values: [null, null] }, - { label: '3. Any other (Pl. specify)', level: 1, total: true, values: [0, null] }, - { label: 'Total Non-fund based', level: 0, total: true, values: [0, null] }, + { label: '2. Other Financial Guarantees', level: 1, values: [ + null, + null + ] }, + { label: '3. Any other (Pl. specify)', level: 1, total: true, values: [ + 0, + null + ] }, + { label: 'Total Non-fund based', level: 0, total: true, values: [ + 0, + null + ] }, { label: 'A3. Investments in Capital Market', level: 0, header: true, values: [] }, - { label: '1. Convertible Bonds and Debentures', level: 1, values: [null, null] }, - { label: '2. Equities (of AIFIs)', level: 1, values: [null, null] }, - { label: '3. Equity Oriented Mutual Funds', level: 1, values: [null, null] }, - { label: '4. Any other (to be specified)', level: 1, total: true, values: [24234, 0] }, - { label: 'NON SLR INVESTMENT', level: 2, values: [24234, null] }, - { label: 'Total Investments in Capital Market', level: 0, total: true, values: [24234, 0] }, - { label: 'Total Capital Market Exposure (A1 + A2 + A3)', level: 0, total: true, values: [24234, 0] } + { label: '1. Convertible Bonds and Debentures', level: 1, values: [ + null, + null + ] }, + { label: '2. Equities (of AIFIs)', level: 1, values: [ + null, + null + ] }, + { label: '3. Equity Oriented Mutual Funds', level: 1, values: [ + null, + null + ] }, + { label: '4. Any other (to be specified)', level: 1, total: true, values: [ + 24234, + 0 + ] }, + { label: 'NON SLR INVESTMENT', level: 2, values: [ + 24234, + null + ] }, + { label: 'Total Investments in Capital Market', level: 0, total: true, values: [ + 24234, + 0 + ] }, + { label: 'Total Capital Market Exposure (A1 + A2 + A3)', level: 0, total: true, values: [ + 24234, + 0 + ] } ], notes: [ 'Note: Extant circular on exposure norms may be referred. Non-funded exposure should include derivative contracts, if any.' @@ -1040,83 +2681,161 @@ export const RAQ_REAL_ESTATE: MatrixTable = { ], rows: [ { label: 'I. Exposure to Real Estate:', level: 0, header: true, values: [] }, - { label: 'A. Residential Mortgage *', level: 1, total: true, values: [17229, 2148] }, + { label: 'A. Residential Mortgage *', level: 1, total: true, values: [ + 17229, + 2148 + ] }, { label: 'a. Loans to individuals / cooperative / group housing societies, housing boards undertaking housing projects or schemes for Economically Weaker Sections, Low Income Group and Middle Income Group and to Owners of houses / flats for extension up-gradation major repairs for eligible housing schemes*', level: 2, - values: [17229, 2148] + values: [ + 17229, + 2148 + ] }, { label: 'b. Other loans under finance for housing schemes not covered under (a) above *', level: 2, - values: [null, null] + values: [ + null, + null + ] }, { label: 'c. All other loans for purchase of residential property (including for land)', level: 2, - values: [null, null] + values: [ + null, + null + ] }, { label: 'd. Amount of Refinance obtained from National Housing Bank / funds obtained (for housing loans) from higher financing agencies.', level: 2, - values: [null, null] + values: [ + null, + null + ] }, - { label: 'B. Commercial Real Estate', level: 1, total: true, values: [14445, 0] }, - { label: 'a. Advances for acquiring / developing commercial Real Estate', level: 2, values: [14445, null] }, - { label: 'b. Non Fund Based Limits for above purposes', level: 2, values: [null, null] }, - { label: 'C. Any Other', level: 1, total: true, values: [0, 0] }, + { label: 'B. Commercial Real Estate', level: 1, total: true, values: [ + 14445, + 0 + ] }, + { label: 'a. Advances for acquiring / developing commercial Real Estate', level: 2, values: [ + 14445, + null + ] }, + { label: 'b. Non Fund Based Limits for above purposes', level: 2, values: [ + null, + null + ] }, + { label: 'C. Any Other', level: 1, total: true, values: [ + 0, + 0 + ] }, { label: 'i) Exposures in bonds of National Housing Bank / HUDCO only for financing of housing and included under Priority Sector Lending', level: 2, - values: [null, null] + values: [ + null, + null + ] }, { label: 'ii) Other Fund Based and Non Fund Based Exposures on NHB / HUDCO and Housing Finance Companies, if any', level: 2, - values: [null, null] + values: [ + null, + null + ] }, { label: 'iii) Investments in Mortgage Backed Securities (MBS) and other securitized exposures', level: 2, total: true, - values: [0, 0] + values: [ + 0, + 0 + ] }, - { label: 'a. Residential', level: 3, values: [null, null] }, - { label: 'b. Commercial Real Estate.', level: 3, values: [null, null] }, + { label: 'a. Residential', level: 3, values: [ + null, + null + ] }, + { label: 'b. Commercial Real Estate.', level: 3, values: [ + null, + null + ] }, { label: 'iv) Advances to builders / contractors (other than for commercial Real Estate).', level: 2, - values: [null, null] + values: [ + null, + null + ] }, - { label: 'v) Others', level: 2, values: [null, null] }, - { label: 'D. TOTAL (A+B+C)', level: 0, total: true, values: [31674, 2148] }, + { label: 'v) Others', level: 2, values: [ + null, + null + ] }, + { label: 'D. TOTAL (A+B+C)', level: 0, total: true, values: [ + 31674, + 2148 + ] }, { label: 'Out of above, the total exposure taken after obtaining pari passu (2nd mortgage)', level: 1, - values: [null, null] + values: [ + null, + null + ] }, { label: 'II. Other Exposures to Real Estate', level: 0, header: true, values: [] }, - { label: 'Fund Based', level: 1, values: [null, null] }, - { label: 'Of which loan given after obtaining pari passu (2nd mortgage)', level: 2, values: [null, null] }, - { label: 'Non Fund Based', level: 1, values: [null, null] }, - { label: 'Of which facility given after obtaining pari passu (2nd mortgage)', level: 2, values: [null, null] }, + { label: 'Fund Based', level: 1, values: [ + null, + null + ] }, + { label: 'Of which loan given after obtaining pari passu (2nd mortgage)', level: 2, values: [ + null, + null + ] }, + { label: 'Non Fund Based', level: 1, values: [ + null, + null + ] }, + { label: 'Of which facility given after obtaining pari passu (2nd mortgage)', level: 2, values: [ + null, + null + ] }, { label: 'III. Housing Finance under Priority Sector', level: 0, header: true, values: [] }, { label: '(i) Loan upto Rs.25 lakhs for purchase / construction of houses by Individuals', level: 1, - values: [10241, 1559] + values: [ + 10241, + 1559 + ] }, { label: '(ii) Other Housing Loans to individuals for repairs / additions / alterations etc classified under Priority Sector Lending)', level: 1, - values: [null, null] + values: [ + null, + null + ] }, - { label: '(iii) All other housing related exposures under Priority Sector', level: 1, values: [null, null] }, - { label: 'Total Housing Finance Under Priority Sector', level: 0, total: true, values: [10241, 1559] } + { label: '(iii) All other housing related exposures under Priority Sector', level: 1, values: [ + null, + null + ] }, + { label: 'Total Housing Finance Under Priority Sector', level: 0, total: true, values: [ + 10241, + 1559 + ] } ] }; diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html index da583820f..129a7ccc7 100644 --- a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.html @@ -52,6 +52,46 @@ } + + @if (activeTab !== 'filing' && activeTab !== 'signatory') { +
+
+ + + General Ledger mapping + @if (glMappedCount) { + {{ glMappedCount }} line(s) mapped + } @else { + — pin GL accounts to a line and its first figure comes from the ledger + } + +
+
+ @if (glMapMode) { + @if (canSelectGlBranch) { + + } @else { + + {{ glScopeName }} + + } + @if (glMappedCount) { + + } + } + +
+
+ } + @if (activeTab === 'filing') {
@@ -74,7 +114,9 @@ @if (activeTab === 'portfolio') {
-
PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment)
+
+ PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment) +
@@ -117,7 +159,9 @@
-
Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs — Rs in Thousands
+
+ Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs — Rs in Thousands +
ParticularsMapped GL accountsRs in Thousand
{{ line.label }} - {{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }} + @if (glMapMode) { + +
+ @for (code of glCodesFor(line.label); track code) { + + {{ code }} + + + } + + @if (glDiffers(line.label, line.value)) { + + differs from filed + + } +
+
+ {{ + effectiveValue(line.label, line.value) === null + ? '' + : (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2')) + }}
@@ -240,7 +284,8 @@ Sectoral gross advances ₹{{ sectoralGross | number: '1.2-2' }} and NPAs ₹{{ sectoralNpa | number: '1.2-2' }} do not match the closing position reported by Classification (₹{{ classificationTotal | number: '1.2-2' }} / - ₹{{ grossNpa | number: '1.2-2' }}). + ₹{{ grossNpa | number: '1.2-2' }}). @if (sectoralMatchesOpening) { They match the opening balances in Movement Parts 1 and 2 instead — this statement appears to have been prepared on the previous quarter's book. @@ -326,7 +371,10 @@
- {{ t.title }}@if (t.unit) {{{ t.unit }}} + {{ t.title }} + @if (t.unit) { + {{ t.unit }} + }
@if (t.subtitle) {
{{ t.subtitle }}
@@ -335,6 +383,9 @@
+ @if (glMapMode) { + + } @for (c of t.columns; track $index) { } @@ -344,12 +395,48 @@ @for (row of t.rows; track $index) { + @if (glMapMode) { + + } @if (row.header) { } @else { @for (v of row.values; track $index) { - } } diff --git a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts index 173ae15c1..ffe1ae4c9 100644 --- a/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts +++ b/src/app/rbi-circulars/oss3-raq/oss3-raq.component.ts @@ -12,6 +12,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import * as ExcelJS from 'exceljs'; import { MatrixTable } from '../oss-return.model'; +import { GlMappingHost } from '../gl-mapping.host'; import { addBanner, addMatrixTable, @@ -77,7 +78,14 @@ type Oss3Tab = FormsModule ] }) -export class Oss3RaqComponent { +export class Oss3RaqComponent extends GlMappingHost { + readonly glReturnCode = 'R323'; + + /** RAQ reports the quarter; mapped figures are drawn from the same span. */ + get glRange() { + return { from: '2026-04-01', to: '2026-06-30' }; + } + activeTab: Oss3Tab = 'filing'; /** Tab strip definition — keeps the template free of a dozen near-identical buttons. */ @@ -313,8 +321,7 @@ export class Oss3RaqComponent { { width: 16 }, { width: 18 }, { width: 18 }, - { width: 18 } - ]; + { width: 18 }]; addBanner(sheet, 'TopImpairedCredits - Top Impaired Credits', 'I'); addMatrixTable(sheet, this.capitalMemo); for (const t of this.recoveryTables) { @@ -398,8 +405,7 @@ export class Oss3RaqComponent { { width: 18 }, { width: 20 }, { width: 22 }, - { width: 20 } - ]; + { width: 20 }]; addBanner(sheet, 'ExposureLimits - Single and Group Borrower Exposure Limit', 'G'); addMatrixTable(sheet, this.capitalPreviousMarch); diff --git a/src/app/reports/reports-routing.module.ts b/src/app/reports/reports-routing.module.ts index bdcf03fb0..4d2f7cafc 100644 --- a/src/app/reports/reports-routing.module.ts +++ b/src/app/reports/reports-routing.module.ts @@ -43,6 +43,16 @@ const routes: Routes = [ data: { title: 'Sanctions Logs', breadcrumb: 'Sanctions Logs' }, component: SanctionsLogsComponent }, + // Must sit above ':filter', which would otherwise match this path and + // render an empty report category instead. + { + path: 'form-97-98', + data: { title: 'Form 97 / 98 Transaction Report', breadcrumb: 'Form 97 / 98 Transaction Report' }, + loadComponent: () => + import('../form-declarations/form-declaration-report.component').then( + (m) => m.FormDeclarationReportComponent + ) + }, { path: ':filter', data: { routeParamBreadcrumb: 'filter' }, diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index fa3deecb1..8da43cbc0 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -24,6 +24,7 @@ import { faCalendar, faCalendarAlt, faCaretDown, + faCaretRight, faChartBar, faCheck, faCheckCircle, @@ -261,6 +262,7 @@ export class IconsModule { faCalendarAlt, faCalendarCheck, faCaretDown, + faCaretRight, faChartBar, faCheck, faCheckCircle, diff --git a/src/environments/.env.ts b/src/environments/.env.ts index d3ca875e2..9ef2e50ed 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -10,7 +10,7 @@ export default { 'mifos_x': { 'version': '260801', - 'hash': 'bd753f49' + 'hash': '2a9df828' }, 'allow_switching_backend_instance': true };
ParticularsMapped GL accounts{{ c }}
{{ row.label }} + @if (!row.header) { +
+ @for (code of glCodesFor(t.title + ' / ' + row.label); track code) { + + {{ code }} + + + } + +
+ } +
- {{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }} + + @if ($index === 0 && isGlMapped(t.title + ' / ' + row.label)) { + {{ effectiveValue(t.title + ' / ' + row.label, v) | number: '1.2-2' }}{{ row.percent ? '%' : '' }} + } @else { + {{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }} + }