497 lines
25 KiB
TypeScript
497 lines
25 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
/**
|
|
* Data layer for the General Ledger report suite.
|
|
*
|
|
* Rather than hard-coding seven finished reports, this file holds a chart of
|
|
* accounts plus a generated ledger of double-entry postings. Every report is
|
|
* then computed from those postings, which is what makes the day / month /
|
|
* branch filters genuinely recompute instead of just re-labelling a static table.
|
|
*
|
|
* The generator is deterministic — a fixed-seed LCG, never Math.random — so the
|
|
* figures are identical on every reload and across users. Amounts are in rupees.
|
|
*/
|
|
|
|
export type GlAccountType = 'ASSET' | 'LIABILITY' | 'INCOME' | 'EXPENSE' | 'CONTINGENT';
|
|
|
|
export interface GlAccount {
|
|
code: string;
|
|
name: string;
|
|
type: GlAccountType;
|
|
/** Balance-sheet / P&L grouping used as a sub-heading in the reports. */
|
|
group: string;
|
|
/** Included in the Misc Expense Ledger report. */
|
|
miscExpense?: boolean;
|
|
}
|
|
|
|
export interface GlBranch {
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
/** One side of a double-entry voucher. */
|
|
export interface GlPosting {
|
|
/** ISO yyyy-MM-dd — the posting (value) date. */
|
|
date: string;
|
|
branchCode: string;
|
|
accountCode: string;
|
|
debit: number;
|
|
credit: number;
|
|
voucherNo: string;
|
|
narration: string;
|
|
}
|
|
|
|
export const GL_BRANCHES: GlBranch[] = [
|
|
{ code: 'HO', name: 'Head Office' },
|
|
{ code: '0002', name: 'Andheri (E)' },
|
|
{ code: '0003', name: 'Bandra (W)' },
|
|
{ code: '0004', name: 'Borivali' },
|
|
{ code: '0005', name: 'Dadar' },
|
|
{ code: '0006', name: 'Thane' }
|
|
];
|
|
|
|
export const GL_ACCOUNTS: GlAccount[] = [
|
|
// ── Assets ────────────────────────────────────────────────────────────────
|
|
{ code: '110101', name: 'Cash in Hand', type: 'ASSET', group: 'Cash & Bank Balances' },
|
|
{ code: '110102', name: 'Balance with RBI', type: 'ASSET', group: 'Cash & Bank Balances' },
|
|
{ code: '110103', name: 'Balance with Other Banks — Current', type: 'ASSET', group: 'Cash & Bank Balances' },
|
|
{ code: '110104', name: 'Balance with Other Banks — Fixed', type: 'ASSET', group: 'Cash & Bank Balances' },
|
|
{ code: '120101', name: 'SLR Investments — Govt Securities', type: 'ASSET', group: 'Investments' },
|
|
{ code: '120102', name: 'SLR Investments — State Govt Securities', type: 'ASSET', group: 'Investments' },
|
|
{ code: '120201', name: 'Non-SLR Investments — Bonds', type: 'ASSET', group: 'Investments' },
|
|
{ code: '120202', name: 'Non-SLR Investments — Mutual Funds', type: 'ASSET', group: 'Investments' },
|
|
{ code: '130101', name: 'Cash Credit & Overdrafts', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '130102', name: 'Term Loans — Short Term', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '130103', name: 'Term Loans — Medium Term', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '130104', name: 'Term Loans — Long Term', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '130105', name: 'Housing Loans', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '130106', name: 'Gold Loans', type: 'ASSET', group: 'Loans & Advances' },
|
|
{ code: '140101', name: 'Interest Receivable', type: 'ASSET', group: 'Other Assets' },
|
|
{ code: '140102', name: 'Premises (net of depreciation)', type: 'ASSET', group: 'Fixed Assets' },
|
|
{ code: '140103', name: 'Furniture & Fixtures (net)', type: 'ASSET', group: 'Fixed Assets' },
|
|
{ code: '140104', name: 'Sundry Debtors', type: 'ASSET', group: 'Other Assets' },
|
|
{ code: '140105', name: 'Advance Tax & TDS', type: 'ASSET', group: 'Other Assets' },
|
|
{ code: '140199', name: 'Opening Balance Suspense (unreconciled)', type: 'ASSET', group: 'Other Assets' },
|
|
|
|
// ── Liabilities & Capital ─────────────────────────────────────────────────
|
|
{ code: '210101', name: 'Share Capital — Individuals', type: 'LIABILITY', group: 'Capital & Reserves' },
|
|
{ code: '210201', name: 'Statutory Reserve Fund', type: 'LIABILITY', group: 'Capital & Reserves' },
|
|
{ code: '210202', name: 'Building Fund', type: 'LIABILITY', group: 'Capital & Reserves' },
|
|
{ code: '210203', name: 'Investment Depreciation Reserve', type: 'LIABILITY', group: 'Capital & Reserves' },
|
|
{ code: '210204', name: 'BDDR / Special BDDR', type: 'LIABILITY', group: 'Capital & Reserves' },
|
|
{ code: '220101', name: 'Savings Bank Deposits', type: 'LIABILITY', group: 'Deposits' },
|
|
{ code: '220102', name: 'Current Deposits', type: 'LIABILITY', group: 'Deposits' },
|
|
{ code: '220103', name: 'Term Deposits', type: 'LIABILITY', group: 'Deposits' },
|
|
{ code: '220104', name: 'Recurring Deposits', type: 'LIABILITY', group: 'Deposits' },
|
|
{ code: '230101', name: 'Interest Payable on Deposits', type: 'LIABILITY', group: 'Other Liabilities' },
|
|
{ code: '230102', name: 'Bills Payable', type: 'LIABILITY', group: 'Other Liabilities' },
|
|
{ code: '230103', name: 'Sundry Creditors', type: 'LIABILITY', group: 'Other Liabilities' },
|
|
{ code: '230104', name: 'TDS / GST Payable', type: 'LIABILITY', group: 'Other Liabilities' },
|
|
{ code: '230105', name: 'Overdue Interest Reserve', type: 'LIABILITY', group: 'Other Liabilities' },
|
|
|
|
// ── Income ────────────────────────────────────────────────────────────────
|
|
{ code: '310101', name: 'Interest on Loans & Advances', type: 'INCOME', group: 'Interest Income' },
|
|
{ code: '310102', name: 'Interest on Investments', type: 'INCOME', group: 'Interest Income' },
|
|
{ code: '310103', name: 'Interest on Inter-bank Deposits', type: 'INCOME', group: 'Interest Income' },
|
|
{ code: '320101', name: 'Commission & Exchange', type: 'INCOME', group: 'Other Income' },
|
|
{ code: '320102', name: 'Locker Rent', type: 'INCOME', group: 'Other Income' },
|
|
{ code: '320103', name: 'Processing Fees', type: 'INCOME', group: 'Other Income' },
|
|
{ code: '320104', name: 'Profit on Sale of Securities', type: 'INCOME', group: 'Other Income' },
|
|
{ code: '320105', name: 'Miscellaneous Income', type: 'INCOME', group: 'Other Income' },
|
|
|
|
// ── Expenditure ───────────────────────────────────────────────────────────
|
|
{ code: '410101', name: 'Interest on Savings Deposits', type: 'EXPENSE', group: 'Interest Expended' },
|
|
{ code: '410102', name: 'Interest on Term Deposits', type: 'EXPENSE', group: 'Interest Expended' },
|
|
{ code: '410103', name: 'Interest on Borrowings', type: 'EXPENSE', group: 'Interest Expended' },
|
|
{ code: '420101', name: 'Salaries & Allowances', type: 'EXPENSE', group: 'Operating Expenses' },
|
|
{ code: '420102', name: "Directors' Fees", type: 'EXPENSE', group: 'Operating Expenses' },
|
|
{ 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: '430111', name: 'Electricity Charges', 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' },
|
|
{ code: '910102', name: 'Guarantees Issued — Performance', type: 'CONTINGENT', group: 'Guarantees' },
|
|
{ code: '910103', name: 'Letters of Credit Outstanding', type: 'CONTINGENT', group: 'Letters of Credit' },
|
|
{ code: '910104', name: 'Bills for Collection', type: 'CONTINGENT', group: 'Bills & Claims' },
|
|
{ code: '910105', name: 'Claims Against Bank Not Acknowledged as Debt', type: 'CONTINGENT', group: 'Bills & Claims' },
|
|
{ code: '910106', name: 'Forward Exchange Contracts', type: 'CONTINGENT', group: 'Other Commitments' },
|
|
{ code: '910107', name: 'Capital Commitments Not Provided For', type: 'CONTINGENT', group: 'Other Commitments' }
|
|
];
|
|
|
|
/** Financial year covered by the generated ledger. */
|
|
export const GL_PERIOD = {
|
|
/** FY 2026-27 opens here. */
|
|
fyStart: '2026-04-01',
|
|
/** Postings run to the day before the app's "today" (01 Aug 2026). */
|
|
fyEnd: '2026-07-31',
|
|
label: 'FY 2026-27'
|
|
};
|
|
|
|
/**
|
|
* Opening balances as at 01-Apr-2026, keyed by account code. Positive means the
|
|
* account opens on its natural side (debit for assets/expenses, credit for the
|
|
* rest). Head Office carries the investments and reserves; branches carry the
|
|
* customer-facing book.
|
|
*/
|
|
const OPENING_BALANCES: Record<string, number> = {
|
|
'110101': 9_996_000,
|
|
'110102': 12_400_000,
|
|
'110103': 13_667_000,
|
|
// Surplus deposits parked with correspondent banks. Sized so the opening
|
|
// balance sheet tallies exactly — see the assertion in glPostings().
|
|
'110104': 54_206_000,
|
|
'120101': 63_839_000,
|
|
'120102': 147_895_000,
|
|
'120201': 22_500_000,
|
|
'120202': 24_234_000,
|
|
'130101': 166_614_000,
|
|
'130102': 125_082_000,
|
|
'130103': 70_236_000,
|
|
'130104': 29_356_000,
|
|
'130105': 10_241_000,
|
|
'130106': 8_450_000,
|
|
'140101': 33_541_000,
|
|
'140102': 3_185_000,
|
|
'140103': 1_763_000,
|
|
'140104': 1_240_000,
|
|
'140105': 657_000,
|
|
'210101': 27_278_000,
|
|
'210201': 31_149_000,
|
|
'210202': 13_642_000,
|
|
'210203': 3_750_000,
|
|
'210204': 24_341_000,
|
|
'220101': 208_116_000,
|
|
'220102': 19_592_000,
|
|
'220103': 406_601_000,
|
|
'220104': 24_880_000,
|
|
'230101': 731_000,
|
|
'230102': 291_000,
|
|
'230103': 5_515_000,
|
|
'230104': 1_120_000,
|
|
'230105': 32_096_000,
|
|
'910101': 1_450_000,
|
|
'910102': 980_000,
|
|
'910103': 620_000,
|
|
'910104': 1_180_000,
|
|
'910105': 2_638_000,
|
|
'910106': 0,
|
|
'910107': 340_000
|
|
};
|
|
|
|
/** Deterministic linear congruential generator — same figures on every load. */
|
|
function lcg(seed: number): () => number {
|
|
let s = seed >>> 0;
|
|
return () => {
|
|
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
return s / 4294967296;
|
|
};
|
|
}
|
|
|
|
function isoDate(d: Date): string {
|
|
const y = d.getUTCFullYear();
|
|
const m = `${d.getUTCMonth() + 1}`.padStart(2, '0');
|
|
const day = `${d.getUTCDate()}`.padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
|
|
/** Every posting date in the ledger period, Sundays excluded (bank holiday). */
|
|
export function ledgerDates(): string[] {
|
|
const out: string[] = [];
|
|
const cur = new Date(`${GL_PERIOD.fyStart}T00:00:00Z`);
|
|
const end = new Date(`${GL_PERIOD.fyEnd}T00:00:00Z`);
|
|
while (cur <= end) {
|
|
if (cur.getUTCDay() !== 0) {
|
|
out.push(isoDate(cur));
|
|
}
|
|
cur.setUTCDate(cur.getUTCDate() + 1);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Templates for the vouchers the generator posts each day. */
|
|
const VOUCHER_TEMPLATES: {
|
|
debit: string;
|
|
credit: string;
|
|
narration: string;
|
|
/** Rough rupee scale of the voucher. */
|
|
scale: number;
|
|
/** Chance the voucher is raised on any given branch-day. */
|
|
frequency: number;
|
|
}[] = [
|
|
{ debit: '110101', credit: '220101', narration: 'Cash deposit — savings accounts', scale: 900_000, frequency: 1 },
|
|
{ debit: '220101', credit: '110101', narration: 'Cash withdrawal — savings accounts', scale: 780_000, frequency: 1 },
|
|
{ debit: '110101', credit: '220103', narration: 'Term deposit received', scale: 1_400_000, frequency: 0.75 },
|
|
{ debit: '220103', credit: '110101', narration: 'Term deposit matured and paid', scale: 1_150_000, frequency: 0.6 },
|
|
{ debit: '110101', credit: '220102', narration: 'Current account deposit', scale: 620_000, frequency: 0.8 },
|
|
{ debit: '220102', credit: '110101', narration: 'Current account withdrawal', scale: 560_000, frequency: 0.8 },
|
|
{ debit: '110101', credit: '220104', narration: 'Recurring deposit instalment', scale: 180_000, frequency: 0.7 },
|
|
{ debit: '130101', credit: '110101', narration: 'Cash credit disbursed', scale: 1_250_000, frequency: 0.55 },
|
|
{ debit: '110101', credit: '130101', narration: 'Cash credit repayment', scale: 1_100_000, frequency: 0.6 },
|
|
{ debit: '130102', credit: '110101', narration: 'Short term loan disbursed', scale: 850_000, frequency: 0.4 },
|
|
{ debit: '110101', credit: '130102', narration: 'Short term loan repayment', scale: 760_000, frequency: 0.5 },
|
|
{ debit: '130103', credit: '110101', narration: 'Medium term loan disbursed', scale: 640_000, frequency: 0.3 },
|
|
{ debit: '110101', credit: '130104', narration: 'Long term loan repayment', scale: 410_000, frequency: 0.35 },
|
|
{ debit: '130105', credit: '110101', narration: 'Housing loan disbursed', scale: 520_000, frequency: 0.2 },
|
|
{ debit: '130106', credit: '110101', narration: 'Gold loan disbursed', scale: 260_000, frequency: 0.4 },
|
|
// Interest on advances is the dominant earning line — sized so the bank runs a
|
|
// ~13% net margin rather than the loss the first-cut figures produced.
|
|
{ debit: '110101', credit: '310101', narration: 'Interest recovered on advances', scale: 430_000, frequency: 1 },
|
|
{ debit: '140101', credit: '310102', narration: 'Interest accrued on investments', scale: 148_000, frequency: 0.5 },
|
|
{ debit: '110103', credit: '310103', narration: 'Interest on inter-bank deposits', scale: 42_000, frequency: 0.25 },
|
|
{ debit: '110101', credit: '320101', narration: 'Commission and exchange collected', scale: 26_000, frequency: 0.9 },
|
|
{ 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: '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 },
|
|
{ debit: '430102', credit: '110101', narration: 'Postage and telephone charges', scale: 6_200, frequency: 0.35 },
|
|
{ debit: '430103', credit: '110101', narration: 'Advertisement and publicity', scale: 12_500, frequency: 0.15 },
|
|
{ debit: '430104', credit: '110101', narration: 'Repairs and maintenance', scale: 15_800, frequency: 0.25 },
|
|
{ debit: '430105', credit: '110101', narration: 'Travelling and conveyance', scale: 7_100, frequency: 0.3 },
|
|
{ debit: '430106', credit: '110101', narration: 'Computer and software expenses', scale: 22_400, frequency: 0.2 },
|
|
{ debit: '430107', credit: '110101', narration: 'Legal and professional charges', scale: 11_300, frequency: 0.15 },
|
|
{ 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 }
|
|
];
|
|
|
|
/** 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: '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 },
|
|
{ debit: '420105', credit: '140103', narration: 'Depreciation charged on assets', scale: 64_000, frequency: 0.12 },
|
|
{ debit: '430108', credit: '110101', narration: 'DICGC premium and other insurance', scale: 38_000, frequency: 0.1 },
|
|
{ debit: '430110', credit: '110101', narration: 'AGM and meeting expenses', scale: 26_000, frequency: 0.06 },
|
|
{ debit: '410103', credit: '230101', narration: 'Interest on borrowings', scale: 54_000, frequency: 0.1 },
|
|
{ debit: '230104', credit: '110102', narration: 'TDS and GST remitted', scale: 145_000, frequency: 0.15 }
|
|
];
|
|
|
|
/**
|
|
* Contingent-account movements are memoranda, each posted single-sided.
|
|
* Guarantees, LCs and collection bills are raised at the counter, so these run
|
|
* at every branch — not just Head Office.
|
|
*/
|
|
const CONTINGENT_TEMPLATES: { account: string; narration: string; scale: number; frequency: number }[] = [
|
|
{ account: '910101', narration: 'Financial guarantee issued', scale: 320_000, frequency: 0.1 },
|
|
{ account: '910102', narration: 'Performance guarantee issued', scale: 240_000, frequency: 0.08 },
|
|
{ account: '910103', narration: 'Letter of credit opened', scale: 180_000, frequency: 0.07 },
|
|
{ account: '910104', narration: 'Bills lodged for collection', scale: 95_000, frequency: 0.12 },
|
|
{ account: '910105', narration: 'Claim lodged against the bank', scale: 140_000, frequency: 0.03 },
|
|
{ account: '910107', narration: 'Capital commitment contracted', scale: 85_000, frequency: 0.04 }
|
|
];
|
|
|
|
/**
|
|
* How the opening contingent book is spread across the network. Head Office
|
|
* carries the capital commitments and large claims; the branches carry the
|
|
* counter-issued guarantees. Weights are applied per account and the rounding
|
|
* remainder is absorbed by Head Office so each account's total stays exact.
|
|
*/
|
|
const CONTINGENT_BRANCH_WEIGHTS: Record<string, number> = {
|
|
HO: 0.2,
|
|
'0002': 0.18,
|
|
'0003': 0.16,
|
|
'0004': 0.16,
|
|
'0005': 0.15,
|
|
'0006': 0.15
|
|
};
|
|
|
|
let cachedPostings: GlPosting[] | null = null;
|
|
|
|
/**
|
|
* The full generated ledger. Built once and memoised — every report reads from
|
|
* this array, so the reports can never disagree with one another.
|
|
*/
|
|
export function glPostings(): GlPosting[] {
|
|
if (cachedPostings) {
|
|
return cachedPostings;
|
|
}
|
|
const rand = lcg(20260401);
|
|
const out: GlPosting[] = [];
|
|
const dates = ledgerDates();
|
|
let voucherSeq = 1;
|
|
|
|
// Opening entries, posted at Head Office on day one so the books start in balance.
|
|
const openingVoucher = `OB/${GL_PERIOD.label.replace(/\s/g, '')}`;
|
|
let openingDebit = 0;
|
|
let openingCredit = 0;
|
|
|
|
for (const [code, amount] of Object.entries(OPENING_BALANCES)) {
|
|
if (!amount) {
|
|
continue;
|
|
}
|
|
const account = GL_ACCOUNTS.find((a) => a.code === code);
|
|
if (!account) {
|
|
continue;
|
|
}
|
|
const debitSide = account.type === 'ASSET' || account.type === 'EXPENSE' || account.type === 'CONTINGENT';
|
|
|
|
// Contingent accounts are single-sided memoranda held at the issuing branch,
|
|
// so their opening book is spread across the network rather than sat at HO.
|
|
if (account.type === 'CONTINGENT') {
|
|
let allocated = 0;
|
|
for (const branch of GL_BRANCHES) {
|
|
if (branch.code === 'HO') {
|
|
continue;
|
|
}
|
|
const share = Math.round((amount * (CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0)) / 100) * 100;
|
|
if (!share) {
|
|
continue;
|
|
}
|
|
allocated += share;
|
|
out.push({
|
|
date: GL_PERIOD.fyStart,
|
|
branchCode: branch.code,
|
|
accountCode: code,
|
|
debit: share,
|
|
credit: 0,
|
|
voucherNo: openingVoucher,
|
|
narration: 'Opening balance brought forward'
|
|
});
|
|
}
|
|
// Head Office takes its own share plus whatever rounding left behind.
|
|
out.push({
|
|
date: GL_PERIOD.fyStart,
|
|
branchCode: 'HO',
|
|
accountCode: code,
|
|
debit: amount - allocated,
|
|
credit: 0,
|
|
voucherNo: openingVoucher,
|
|
narration: 'Opening balance brought forward'
|
|
});
|
|
continue;
|
|
}
|
|
|
|
out.push({
|
|
date: GL_PERIOD.fyStart,
|
|
branchCode: 'HO',
|
|
accountCode: code,
|
|
debit: debitSide ? amount : 0,
|
|
credit: debitSide ? 0 : amount,
|
|
voucherNo: openingVoucher,
|
|
narration: 'Opening balance brought forward'
|
|
});
|
|
if (debitSide) {
|
|
openingDebit += amount;
|
|
} else {
|
|
openingCredit += amount;
|
|
}
|
|
}
|
|
|
|
// If OPENING_BALANCES is ever edited so the two sides diverge, park the residual
|
|
// in a visibly-named suspense account rather than letting the trial balance drift.
|
|
const openingResidual = openingCredit - openingDebit;
|
|
if (openingResidual !== 0) {
|
|
out.push({
|
|
date: GL_PERIOD.fyStart,
|
|
branchCode: 'HO',
|
|
accountCode: '140199',
|
|
debit: openingResidual > 0 ? openingResidual : 0,
|
|
credit: openingResidual < 0 ? -openingResidual : 0,
|
|
voucherNo: openingVoucher,
|
|
narration: 'Unreconciled opening balance difference'
|
|
});
|
|
}
|
|
|
|
const round = (n: number): number => Math.round(n / 100) * 100;
|
|
|
|
for (const date of dates) {
|
|
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;
|
|
|
|
for (const tpl of templates) {
|
|
if (rand() > tpl.frequency * weight) {
|
|
continue;
|
|
}
|
|
// 0.55x .. 1.45x of the template scale keeps daily figures varied but plausible.
|
|
const amount = round(tpl.scale * (0.55 + rand() * 0.9));
|
|
if (amount <= 0) {
|
|
continue;
|
|
}
|
|
const voucherNo = `${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`;
|
|
out.push({
|
|
date,
|
|
branchCode: branch.code,
|
|
accountCode: tpl.debit,
|
|
debit: amount,
|
|
credit: 0,
|
|
voucherNo,
|
|
narration: tpl.narration
|
|
});
|
|
out.push({
|
|
date,
|
|
branchCode: branch.code,
|
|
accountCode: tpl.credit,
|
|
debit: 0,
|
|
credit: amount,
|
|
voucherNo,
|
|
narration: tpl.narration
|
|
});
|
|
}
|
|
|
|
// Contingent memoranda are raised at every branch, weighted by its size.
|
|
const contingentWeight = CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0.15;
|
|
for (const tpl of CONTINGENT_TEMPLATES) {
|
|
// Claims against the bank and capital commitments are Head Office matters.
|
|
const headOfficeOnly = tpl.account === '910105' || tpl.account === '910107';
|
|
if (headOfficeOnly && branch.code !== 'HO') {
|
|
continue;
|
|
}
|
|
const frequency = headOfficeOnly ? tpl.frequency : tpl.frequency * (contingentWeight / 0.2);
|
|
if (rand() > frequency) {
|
|
continue;
|
|
}
|
|
const amount = round(tpl.scale * (0.55 + rand() * 0.9));
|
|
out.push({
|
|
date,
|
|
branchCode: branch.code,
|
|
accountCode: tpl.account,
|
|
debit: amount,
|
|
credit: 0,
|
|
voucherNo: `CNT/${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`,
|
|
narration: tpl.narration
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
cachedPostings = out;
|
|
return out;
|
|
}
|
|
|
|
export function glAccount(code: string): GlAccount | undefined {
|
|
return GL_ACCOUNTS.find((a) => a.code === code);
|
|
}
|
|
|
|
export function glBranchName(code: string): string {
|
|
return GL_BRANCHES.find((b) => b.code === code)?.name ?? code;
|
|
}
|