form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -79,17 +79,33 @@
|
||||
|
||||
<div class="glr-filter">
|
||||
<label>Branch</label>
|
||||
<select class="glr-inp" [(ngModel)]="branchCode">
|
||||
<option value="">All Branches (Consolidated)</option>
|
||||
@for (b of branches; track b.code) {
|
||||
<option [value]="b.code">{{ b.code }} — {{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
@if (canSelectBranch) {
|
||||
<select class="glr-inp" [(ngModel)]="branchCode">
|
||||
<option value="">All Branches (Consolidated)</option>
|
||||
@for (b of branches; track b.code) {
|
||||
<option [value]="b.code">{{ b.code }} — {{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<span class="glr-chip" title="Your login is restricted to this branch">
|
||||
<fa-icon icon="university"></fa-icon> {{ scopeName }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="glr-btn" (click)="resetFilters()">Reset</button>
|
||||
</div>
|
||||
|
||||
@if (scopeIsUnresolved) {
|
||||
<div class="glr-balance glr-balance--bad">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>
|
||||
Your office <strong>{{ scopeName }}</strong> 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.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- REPORT SELECTOR -->
|
||||
<div class="glr-tabs">
|
||||
@for (r of reports; track r.code) {
|
||||
@@ -193,7 +209,9 @@
|
||||
@for (r of pnlExpense; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
|
||||
<td>
|
||||
{{ r.name }}<span class="muted"> · {{ r.group }}</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
@@ -229,7 +247,9 @@
|
||||
@for (r of pnlIncome; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
|
||||
<td>
|
||||
{{ r.name }}<span class="muted"> · {{ r.group }}</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
@@ -281,7 +301,9 @@
|
||||
@for (r of bsLiabilities; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
|
||||
<td>
|
||||
{{ r.name }}<span class="muted"> · {{ r.group }}</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -312,7 +334,9 @@
|
||||
@for (r of bsAssets; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
|
||||
<td>
|
||||
{{ r.name }}<span class="muted"> · {{ r.group }}</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -331,11 +355,17 @@
|
||||
@if (activeReport === 'comparative-balance-sheet') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Compares the closing position as at <strong>{{ range.to }}</strong> against the position immediately before
|
||||
this period began. Change the filter above to compare a different span.
|
||||
Compares the closing position as at <strong>{{ range.to }}</strong> against the position immediately before this
|
||||
period began. Change the filter above to compare a different span.
|
||||
</p>
|
||||
|
||||
@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
|
||||
) {
|
||||
<div class="glr-tbl-wrap">
|
||||
<div class="glr-tbl-h">{{ block.title }}</div>
|
||||
<table class="glr-tbl">
|
||||
@@ -353,7 +383,9 @@
|
||||
@for (r of block.rows; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
|
||||
<td>
|
||||
{{ r.name }}<span class="muted"> · {{ r.group }}</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.current | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">{{ r.previous | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono" [class.var-up]="r.variance > 0" [class.var-down]="r.variance < 0">
|
||||
@@ -379,8 +411,8 @@
|
||||
@if (activeReport === 'contingent-liabilities') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Off-balance-sheet commitments outstanding as at <strong>{{ range.to }}</strong>. These are memoranda accounts
|
||||
and are deliberately excluded from the Trial Balance and Balance Sheet totals.
|
||||
Off-balance-sheet commitments outstanding as at <strong>{{ range.to }}</strong
|
||||
>. These are memoranda accounts and are deliberately excluded from the Trial Balance and Balance Sheet totals.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap">
|
||||
<table class="glr-tbl">
|
||||
@@ -419,8 +451,8 @@
|
||||
@if (activeReport === 'gl-wise-dr-cr') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Debit and credit turnover posted between <strong>{{ rangeLabel }}</strong> for {{ branchLabel }}. Every
|
||||
voucher is double-entry, so the debit and credit totals below must match.
|
||||
Debit and credit turnover posted between <strong>{{ rangeLabel }}</strong> for {{ branchLabel }}. Every voucher
|
||||
is double-entry, so the debit and credit totals below must match.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap">
|
||||
<table class="glr-tbl">
|
||||
@@ -474,8 +506,8 @@
|
||||
@if (activeReport === 'misc-expense-ledger') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Every miscellaneous-expense voucher posted between <strong>{{ rangeLabel }}</strong> for {{ branchLabel }}
|
||||
— {{ miscRows.length | number }} entries.
|
||||
Every miscellaneous-expense voucher posted between <strong>{{ rangeLabel }}</strong> for
|
||||
{{ branchLabel }} — {{ miscRows.length | number }} entries.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
@@ -518,6 +550,506 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ GL AVERAGE BALANCE ══════════════ -->
|
||||
@if (activeReport === 'gl-average-balance') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Average of each account's closing balance across every day in
|
||||
<strong>{{ rangeLabel }}</strong> 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.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>GL Code</th>
|
||||
<th>Account</th>
|
||||
<th>Group</th>
|
||||
<th class="ta-r">Opening</th>
|
||||
<th class="ta-r">Closing</th>
|
||||
<th class="ta-r">Average Daily</th>
|
||||
<th class="ta-r">Peak</th>
|
||||
<th class="ta-r">Lowest</th>
|
||||
<th class="ta-c">Active Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of averageRows; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td class="muted">{{ r.group }}</td>
|
||||
<td class="ta-r mono">{{ r.opening | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">{{ r.closing | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">
|
||||
<strong>{{ r.average | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.peak | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">{{ r.lowest | number: '1.2-2' }}</td>
|
||||
<td class="ta-c mono">{{ r.activeDays }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="9" class="glr-empty">No ledger activity for this period and branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="5">TOTAL ({{ averageRows.length }} accounts)</td>
|
||||
<td class="ta-r mono">{{ avgTotalAverage | number: '1.2-2' }}</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ GL MAPPING FOR REPORT ══════════════ -->
|
||||
@if (activeReport === 'gl-mapping') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Which General Ledger accounts feed each RBI return line. Mappings are created on the OSS 1, OSS 2 and OSS 3
|
||||
screens using <strong>Add / Remove GLs</strong>; this is the consolidated view of them, with the value each
|
||||
mapping currently produces from the ledger.
|
||||
</p>
|
||||
|
||||
<div class="glr-tbl-wrap">
|
||||
<div class="glr-tbl-h">Mapped return lines ({{ mappingRows.length }})</div>
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Return</th>
|
||||
<th>Return line</th>
|
||||
<th>Mapped GL accounts</th>
|
||||
<th class="ta-r">Ledger value (₹ thousands)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of mappingRows; track r.returnCode + r.lineKey) {
|
||||
<tr>
|
||||
<td>{{ r.returnLabel }}</td>
|
||||
<td class="mono">{{ r.lineKey }}</td>
|
||||
<td>
|
||||
@for (c of r.codes; track c) {
|
||||
<span class="glr-gl-chip mono">{{ c }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.value | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="4" class="glr-empty">
|
||||
No return lines mapped yet. Open OSS 1, 2 or 3 and use “Add / Remove GLs” to pin accounts to a line.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<div class="glr-tbl-h">Unmapped GL accounts ({{ unmappedAccounts.length }}) — not feeding any return line</div>
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>GL Code</th>
|
||||
<th>Account</th>
|
||||
<th>Group</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of unmappedAccounts; track a.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ a.code }}</td>
|
||||
<td>{{ a.name }}</td>
|
||||
<td class="muted">{{ a.group }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="3" class="glr-empty">Every GL account feeds at least one return line.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ ACCOUNT WISE BALANCE LIST ══════════════ -->
|
||||
@if (activeReport === 'account-wise-balance') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Closing balance of every account as at <strong>{{ range.to }}</strong
|
||||
>, broken out by branch — which branch is actually carrying each balance.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>GL Code</th>
|
||||
<th>Account</th>
|
||||
<th>Group</th>
|
||||
@for (c of branchColumns; track c) {
|
||||
<th class="ta-r">{{ branchName(c) }}</th>
|
||||
}
|
||||
<th class="ta-r">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of accountWiseRows; track r.code) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td class="muted">{{ r.group }}</td>
|
||||
@for (c of branchColumns; track c) {
|
||||
<td class="ta-r mono">{{ r.byBranch[c] ? (r.byBranch[c] | number: '1.2-2') : '' }}</td>
|
||||
}
|
||||
<td class="ta-r mono">
|
||||
<strong>{{ r.total | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="branchColumns.length + 4" class="glr-empty">
|
||||
No balances for this period and branch.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="3">TOTAL ({{ accountWiseRows.length }} accounts)</td>
|
||||
@for (c of branchColumns; track c) {
|
||||
<td class="ta-r mono">{{ branchColumnTotal(c) | number: '1.2-2' }}</td>
|
||||
}
|
||||
<td class="ta-r mono">{{ accountWiseGrandTotal | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ DISTRIBUTION OF BALANCE ══════════════ -->
|
||||
@if (activeReport === 'distribution-of-balance') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Composition of the book as at <strong>{{ range.to }}</strong> for {{ branchLabel }} — how concentrated each side
|
||||
of the balance sheet is by account group.
|
||||
</p>
|
||||
|
||||
<div class="glr-two-col">
|
||||
@for (
|
||||
block of [
|
||||
{ title: 'Capital & Liabilities', rows: liabilityDistribution, total: liabilityDistTotal },
|
||||
{ title: 'Assets', rows: assetDistribution, total: assetDistTotal }
|
||||
];
|
||||
track block.title
|
||||
) {
|
||||
<div class="glr-tbl-wrap">
|
||||
<div class="glr-tbl-h">{{ block.title }}</div>
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<th class="ta-c">A/cs</th>
|
||||
<th class="ta-r">Amount (₹)</th>
|
||||
<th class="ta-r">Share</th>
|
||||
<th class="glr-bar-col">Distribution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of block.rows; track r.group) {
|
||||
<tr>
|
||||
<td>{{ r.group }}</td>
|
||||
<td class="ta-c mono">{{ r.accounts }}</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">
|
||||
<strong>{{ r.share | number: '1.2-2' }}%</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="glr-bar">
|
||||
<div class="glr-bar-fill" [style.width.%]="r.share"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="glr-empty">No balances on this side.</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="total-row">
|
||||
<td colspan="2">Total</td>
|
||||
<td class="ta-r mono">{{ block.total | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">100.00%</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ CLIENT WISE BALANCE ══════════════ -->
|
||||
@if (activeReport === 'client-wise-balance') {
|
||||
<div class="glr-section">
|
||||
<div class="glr-kpis">
|
||||
<div class="glr-kpi glr-kpi--teal">
|
||||
<div class="glr-kpi-n">₹{{ clientDeposits | number: '1.2-2' }}</div>
|
||||
<div class="glr-kpi-l">Customer Deposits</div>
|
||||
</div>
|
||||
<div class="glr-kpi glr-kpi--amber">
|
||||
<div class="glr-kpi-n">₹{{ clientAdvances | number: '1.2-2' }}</div>
|
||||
<div class="glr-kpi-l">Customer Advances</div>
|
||||
</div>
|
||||
<div class="glr-kpi glr-kpi--green">
|
||||
<div class="glr-kpi-n">{{ clientRows.length | number }}</div>
|
||||
<div class="glr-kpi-l">Relationships</div>
|
||||
<div class="glr-kpi-sub">as at {{ range.to }} · {{ branchLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="glr-note">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CIF</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Account No</th>
|
||||
<th>Product</th>
|
||||
<th class="ta-c">Side</th>
|
||||
<th class="ta-r">Balance (₹)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of clientRows; track r.cif) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.cif }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td>{{ branchName(r.branchCode) }}</td>
|
||||
<td class="mono">{{ r.accountNo }}</td>
|
||||
<td class="muted">{{ r.product }}</td>
|
||||
<td class="ta-c">
|
||||
<span
|
||||
class="type-pill"
|
||||
[class.type-liability]="r.side === 'DEPOSIT'"
|
||||
[class.type-asset]="r.side === 'ADVANCE'"
|
||||
>
|
||||
{{ r.side }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="ta-r mono">{{ r.balance | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="glr-empty">No customer relationships for this period and branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ BANKER HEAD REPORT ══════════════ -->
|
||||
@if (activeReport === 'banker-head') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Head-wise abstract for <strong>{{ rangeLabel }}</strong> — the trial balance rolled up to principal heads, with
|
||||
opening, turnover and closing for each. Debit and credit turnover must agree.
|
||||
</p>
|
||||
<div class="glr-tbl-wrap">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Principal Head</th>
|
||||
<th class="ta-c">Accounts</th>
|
||||
<th class="ta-r">Opening</th>
|
||||
<th class="ta-r">Debit</th>
|
||||
<th class="ta-r">Credit</th>
|
||||
<th class="ta-r">Closing</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of bankerHeadRows; track r.head) {
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ r.head }}</strong>
|
||||
</td>
|
||||
<td class="ta-c mono">{{ r.accounts }}</td>
|
||||
<td class="ta-r mono">{{ r.opening | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">{{ r.debit ? (r.debit | number: '1.2-2') : '' }}</td>
|
||||
<td class="ta-r mono">{{ r.credit ? (r.credit | number: '1.2-2') : '' }}</td>
|
||||
<td class="ta-r mono">
|
||||
<strong>{{ r.closing | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="glr-empty">No activity for this period and branch.</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="total-row">
|
||||
<td colspan="2">TOTAL ({{ bankerHeadRows.length }} heads)</td>
|
||||
<td></td>
|
||||
<td class="ta-r mono">{{ headDebitTotal | number: '1.2-2' }}</td>
|
||||
<td class="ta-r mono">{{ headCreditTotal | number: '1.2-2' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ SUNDRY REGISTER ══════════════ -->
|
||||
@if (activeReport === 'sundry-register') {
|
||||
<div class="glr-section">
|
||||
<p class="glr-note">
|
||||
Outstanding items in the sundry, suspense and bills-payable heads as at <strong>{{ range.to }}</strong
|
||||
>, aged from their posting date. These heads are where unreconciled entries collect, so ageing is the point of
|
||||
the register.
|
||||
</p>
|
||||
|
||||
@if (sundryBuckets.length) {
|
||||
<div class="glr-kpis">
|
||||
@for (b of sundryBuckets; track b.bucket) {
|
||||
<div
|
||||
class="glr-kpi"
|
||||
[class.glr-kpi--green]="b.bucket === 'Up to 30 days'"
|
||||
[class.glr-kpi--amber]="b.bucket === '31 to 90 days'"
|
||||
[class.glr-kpi--red]="b.bucket === 'Over 180 days'"
|
||||
>
|
||||
<div class="glr-kpi-n">₹{{ b.amount | number: '1.2-2' }}</div>
|
||||
<div class="glr-kpi-l">{{ b.bucket }}</div>
|
||||
<div class="glr-kpi-sub">{{ b.count }} item(s)</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Branch</th>
|
||||
<th>GL Code</th>
|
||||
<th>Account</th>
|
||||
<th>Narration</th>
|
||||
<th class="ta-r">Amount (₹)</th>
|
||||
<th class="ta-c">Age</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of sundryRows; track r.voucherNo + r.code + r.date) {
|
||||
<tr>
|
||||
<td class="mono">{{ r.date }}</td>
|
||||
<td class="mono">{{ r.voucherNo }}</td>
|
||||
<td>{{ branchName(r.branchCode) }}</td>
|
||||
<td class="mono">{{ r.code }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td class="muted">{{ r.narration }}</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td class="ta-c">
|
||||
<span
|
||||
class="type-pill"
|
||||
[class.type-income]="r.bucket === 'Up to 30 days'"
|
||||
[class.type-expense]="r.bucket === '31 to 90 days'"
|
||||
[class.type-contingent]="r.bucket === 'Over 180 days'"
|
||||
>
|
||||
{{ r.ageDays }}d
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="8" class="glr-empty">No sundry or suspense items outstanding for this selection.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="6">Total outstanding ({{ sundryRows.length }} items)</td>
|
||||
<td class="ta-r mono">{{ sundryTotal | number: '1.2-2' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══════════════ VOUCHER REGISTER ══════════════ -->
|
||||
@if (activeReport === 'voucher-register') {
|
||||
<div class="glr-section">
|
||||
<div class="glr-balance" [class.glr-balance--bad]="unbalancedVouchers > 0">
|
||||
<fa-icon [icon]="unbalancedVouchers ? 'exclamation-triangle' : 'check-circle'"></fa-icon>
|
||||
<span>
|
||||
{{ 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'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
|
||||
<table class="glr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Branch</th>
|
||||
<th>Narration</th>
|
||||
<th>Debit account(s)</th>
|
||||
<th>Credit account(s)</th>
|
||||
<th class="ta-r">Amount (₹)</th>
|
||||
<th class="ta-c">Legs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of voucherRows; track r.voucherNo) {
|
||||
<tr [class.glr-unbalanced]="!r.balanced">
|
||||
<td class="mono">{{ r.date }}</td>
|
||||
<td class="mono">{{ r.voucherNo }}</td>
|
||||
<td>{{ branchName(r.branchCode) }}</td>
|
||||
<td class="muted">{{ r.narration }}</td>
|
||||
<td class="mono">{{ r.debitAccounts }}</td>
|
||||
<td class="mono">{{ r.creditAccounts }}</td>
|
||||
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td class="ta-c mono">{{ r.legs }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="8" class="glr-empty">No vouchers raised in this period for this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="6">Total ({{ voucherRows.length }} vouchers)</td>
|
||||
<td class="ta-r mono">{{ voucherTotal | number: '1.2-2' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="glr-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string, Map<string, number>>();
|
||||
const turnoverCache = new Map<string, Map<string, { debit: number; credit: number; vouchers: Set<string> }>>();
|
||||
|
||||
/** Signed balance on the account's natural side, for postings up to and including `to`. */
|
||||
function cumulativeBalances(to: string, branchCode: string): Map<string, number> {
|
||||
const key = `${to}|${branchCode}`;
|
||||
const hit = balanceCache.get(key);
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
const out = new Map<string, number>();
|
||||
for (const p of glPostings()) {
|
||||
if (p.date > to || !matchesBranch(p, branchCode)) {
|
||||
@@ -110,11 +117,20 @@ function cumulativeBalances(to: string, branchCode: string): Map<string, number>
|
||||
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<string, { debit: number; credit: number; vouchers: Set<string> }> {
|
||||
function periodTurnover(
|
||||
range: GlDateRange,
|
||||
branchCode: string
|
||||
): Map<string, { debit: number; credit: number; vouchers: Set<string> }> {
|
||||
const key = `${range.from}|${range.to}|${branchCode}`;
|
||||
const hit = turnoverCache.get(key);
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
const out = new Map<string, { debit: number; credit: number; vouchers: Set<string> }>();
|
||||
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<string, { d
|
||||
entry.credit += p.credit;
|
||||
entry.vouchers.add(p.voucherNo);
|
||||
}
|
||||
turnoverCache.set(key, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -190,8 +207,7 @@ export function profitAndLoss(range: GlDateRange, branchCode: string): { income:
|
||||
if (!move) {
|
||||
continue;
|
||||
}
|
||||
const amount =
|
||||
account.type === 'INCOME' ? move.credit - move.debit : move.debit - move.credit;
|
||||
const amount = account.type === 'INCOME' ? move.credit - move.debit : move.debit - move.credit;
|
||||
if (!amount) {
|
||||
continue;
|
||||
}
|
||||
@@ -205,7 +221,10 @@ export function profitAndLoss(range: GlDateRange, branchCode: string): { income:
|
||||
* Balance sheet as at the period end. The current-period surplus is folded in as
|
||||
* a liability line so the two sides tally the way the printed statement does.
|
||||
*/
|
||||
export function balanceSheet(range: GlDateRange, branchCode: string): {
|
||||
export function balanceSheet(
|
||||
range: GlDateRange,
|
||||
branchCode: string
|
||||
): {
|
||||
assets: BalanceSheetRow[];
|
||||
liabilities: BalanceSheetRow[];
|
||||
surplus: number;
|
||||
@@ -315,7 +334,417 @@ export function miscExpenseLedger(range: GlDateRange, branchCode: string): MiscE
|
||||
amount: p.debit
|
||||
});
|
||||
}
|
||||
return rows.sort((a, b) => (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<string, number>;
|
||||
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<string, Map<string, number>>();
|
||||
const movedOn = new Map<string, Set<string>>();
|
||||
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<string, number>();
|
||||
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<string>();
|
||||
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<string, Map<string, number>>();
|
||||
for (const b of branchCodes) {
|
||||
perBranch.set(b, cumulativeBalances(range.to, b));
|
||||
}
|
||||
const rows: AccountBranchRow[] = [];
|
||||
for (const account of GL_ACCOUNTS) {
|
||||
const byBranch: Record<string, number> = {};
|
||||
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<string, { amount: number; accounts: number }>();
|
||||
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<string, Map<string, number>>();
|
||||
|
||||
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<string, BankerHeadRow>();
|
||||
|
||||
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<string, GlPosting[]>();
|
||||
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<T>(rows: T[], pick: (row: T) => number): number {
|
||||
return rows.reduce((acc, r) => acc + pick(r), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user