# 21 · GENERAL LEDGER — Frontend (Ledgers section) > **Navigation:** you arrived from `00-CORE.md`. The transport this section calls through is > `docs/12-GENERAL-LEDGER-INTEGRATION.md` (ERPCore's `/api/v1/gl/*` proxy into the external General > Ledger service — that doc's new §7 has a curated endpoint reference for everything this page uses). > General frontend architecture/validation rules are `20-FRONTEND.md` — this doc only adds what's > specific to the Ledgers screens. Record work in `Frontend/PROGRESS.md`. > **GL's own endpoint contract** (exact request/response shapes, error cases) lives in the General > Ledger service's own repo (`04_API_Reference_And_Scenarios.md`) — not duplicated here in full, per > `01-DOC-GUIDE.md §5`'s single-source-of-truth rule; `docs/12` §7 is a summary for quick reference, > that doc is the canonical detail. --- ## 0. Major update, 2026-07-22 — the GL backend changed substantially since this section was last built **Nothing in this pass has been implemented against the frontend codebase yet.** The 2026-07-20 build (§6's history) reflects an earlier, now-superseded GL contract. This revision describes the *target* design against the confirmed, live-verified-on-GL's-side contract as of 2026-07-22 — every section below should be read as "what to build," not "what exists," until §6 says otherwise. Three things changed on GL's side, in order of how much they affect this doc: 1. **Five reports got real structural rework** (not just field renames) — Trial Balance flattened, Profit & Loss restructured into named sections, Cash Flow's display simplified, Tax Report **completely redesigned** into an Income Tax Computation (previously a VAT/WHT/NBT summary), plus a new `outputFormat=Csv` on all seven reports. 2. **Cash and Bank accounts are two separate GL tables/endpoints now**, not one table with a type field — `POST /bank-accounts` (unchanged) and a new `POST /cash-accounts`, unified for reading via `GET /bank-accounts?accountType=Cash|Bank|Both`. 3. **`CLAUDE.md` Rule 8.2 renamed numeric ID fields to business codes across most of GL's API** — the one that reaches this page: `GeneralLedger`'s `accountId` param is now `accountCode` (still optional, same two-mode behavior as before), and the Bank/Cash create forms' `glAccountId` is now `glAccountCode`. --- ## 1. What this is A new **Ledgers** sidebar section (`app/dashboard/ledgers/*`) giving the frontend statutory-format financial reports and cash/bank-account management, sourced entirely from the external General Ledger service via ERPCore's generic proxy (`docs/12-GENERAL-LEDGER-INTEGRATION.md`). Every screen calls `GET /api/v1/gl/reports` or `/gl/bank-accounts`/`/gl/cash-accounts` — there is no ERPCore business logic behind any of it yet (that internal wiring, e.g. posting real journal entries from GRN/adjustments, is tracked separately and deliberately deferred, see `docs/12` §6). **Sidebar structure** (`components/Layouts/AppSidebar.tsx`, mirrored server-side in `Backend/ERPCore/Infra/Persistence/Configurations/{NavItem,SubNavItem,Permission}Configuration.cs` per the existing RBAC-nav convention, docs/10 C.8/C.9) — **gains one entry, Tax Report:** ``` Ledgers ├── Trial Balance /dashboard/ledgers/trial-balance ├── Balance Sheet /dashboard/ledgers/balance-sheet ├── General Ledger /dashboard/ledgers/general-ledger ├── Profit & Loss /dashboard/ledgers/profit-and-loss ├── Cash Flow /dashboard/ledgers/cash-flow ├── Budget vs Actual /dashboard/ledgers/budget-vs-actual ├── Tax Report /dashboard/ledgers/tax-report (new) └── Cash / Bank Accounts /dashboard/ledgers/bank-accounts (+ /new) ``` `/dashboard/ledgers` itself is a card-grid hub, same pattern as `/dashboard/stock` — gains an 8th card. --- ## 2. Transport: a dedicated GL client, not `lib/api-client.ts` `lib/api/general-ledger.ts` is a **separate** fetch client from the one every other screen in this app uses (`lib/api-client.ts`'s `apiRequest`/`apiRequestWithETag`). Reason: those assume ERPCore's own RFC 7807 `ProblemDetails` error shape and a bare-DTO success body. The GL service wraps **every** response — success and error alike — in its own `{ statusCode, success, message, data }` envelope, and (a documented GL quirk, carried through the proxy unchanged per `docs/12` §4) success bodies are camelCase while error bodies are PascalCase. `glRequest()` unwraps both casings itself and throws a `GlApiError` shaped `{ status, detail }` — duck-type compatible with `lib/error-map.ts`'s `ApiErrorLike`, so `errorMessage()`/toasts work unchanged across both API surfaces. All calls are relative to `/api/v1/gl/*` — same-origin through the existing Next `rewrites()` proxy (`next.config.ts`) → ERPCore → the GL service. No new proxy config was needed, and **still isn't** for any of this revision's changes — ERPCore's `GeneralLedgerController` (docs/12) is a byte-for-byte passthrough that never inspects GL's own field names or report contents, so every rename/restructure described in this doc requires zero ERPCore-side change, only frontend-side (`docs/12` §4's new note on this). **No ETag/If-Match anywhere in this module** — the GL service's own reference documents no concurrency tokens on any of the endpoints this UI calls. --- ## 3. Reports: "Sri Lankan Standard" report UI Every report screen (`app/dashboard/ledgers/{trial-balance,balance-sheet,general-ledger,profit-and-loss,cash-flow,budget-vs-actual,tax-report}/page.tsx`) shares three building blocks: - **`components/reports/ReportHeader.tsx`** — a centered statutory header block: small-caps "General Ledger" eyebrow, the report's **LKAS-aligned statement name** (not always GL's own `reportType` value — see the mapping below), the as-at/for-period line, and a currency note. This is the "Sri Lankan Standard report UI" requested: the on-screen equivalent of the formal company-financial-statement layout (title block, period line, right-aligned money columns, bold/indented subtotal rows), not a bespoke ad-hoc table per screen. **The Tax Report screen does not use this component** — see its own entry below, it needs a fuller identity block GL's own PDF gives that report alone. - **`components/reports/DownloadPdfButton.tsx`** — re-issues the *exact same* report call with `outputFormat=Pdf` instead of `Json`, base64-decodes `data.contentBase64` client-side into a `Blob`, and triggers a browser download. - **`components/reports/DownloadCsvButton.tsx`** *(new)* — identical mechanism, `outputFormat=Csv` instead, `Blob` typed `text/csv`, triggers a `.csv` download. Sits next to the PDF button on every screen — two download buttons now, not one. No proxy/transport concern either (`docs/12` §7) — same passthrough as `Pdf` always was. | Screen | `reportType` | On-screen title | Required params | Response shape | |---|---|---|---|---| | Trial Balance | `TrialBalance` | Trial Balance | `asOfDate` | **Flat** array `{accountCode, accountName, debit, credit}` — no `depth` anymore | | Balance Sheet | `BalanceSheet` | Statement of Financial Position | `asOfDate` | Unchanged — hierarchical array `{depth, lineItem, accountType, balance}`, always ends with a synthetic `"Current Year Earnings"` row under Equity | | General Ledger | `GeneralLedger` | General Ledger | `periodStart`, `periodEnd` (`accountCode` optional — renamed from `accountId`, see below) | Unchanged shape | | Profit & Loss | `ProfitAndLoss` | Statement of Profit or Loss | `periodStart`, `periodEnd` | **Nested object**, not a flat array — named sections, see below | | Cash Flow | `CashFlow` | Statement of Cash Flows | `periodStart`, `periodEnd` | Nested object; `workingCapitalChanges[]` gained `direction`; `openingCashBalance`/`closingCashBalance` still returned, not displayed | | Budget vs Actual | `BudgetVsActual` | Budget vs Actual | `budgetId` | Unchanged | | Tax Report *(new)* | `TaxSummary` | Income Tax Computation | `periodStart`, `periodEnd`; optional `allowableDeductions`, `otherTaxableIncome`, `qualifyingPaymentsReliefs`, `surchargeAmount`, `taxRateOverride` | Single flat object, see below | **Defaults, so a screen is never blank on first load:** `asOfDate`/`periodEnd` default to today, `periodStart` to the 1st of the current month (`lib/format.ts`'s `todayIso`/`startOfMonthIso`) — these params are *required* by GL (400 if missing), so the UI always sends something sensible rather than erroring on mount. The Tax Report's five optional parameters get **no forced client-side default** — an untouched field sends nothing, letting GL's own server-side defaulting (`0` for four of them, `system_config`-driven for `qualifyingPaymentsReliefs`/the tax rate) be the single source of truth for what "not supplied" means. **`reportType`/`outputFormat` are TS enums** (`types/general-ledger.ts`'s `ReportType`, `ReportOutputFormat`) — `ReportOutputFormat` gains a `Csv` member. `GlAccount.accountTypeId` stays a `GlAccountTypeId` enum (`Asset=1`…`Expense=5`). **General Ledger: `accountId` renamed to `accountCode` (GL's `CLAUDE.md` Rule 8.2), behavior unchanged from the 2026-07-20 correction.** GL's own reference still documents two modes: `accountCode` supplied → "Account Ledger" (one account + its descendants, one running balance); `accountCode` omitted → the **true General Ledger**, every `is_postable` account's own transactions together, each with its own running balance that resets whenever the account changes, sorted by `accountCode` then `entryDate`. This screen still always calls the second mode — `reportsApi.generalLedger(periodStart, periodEnd)` never sends `accountCode` — and has no account picker/input, same as before; only the underlying param name the client would use *if* a single-account mode were ever added changed, not this screen's own behavior. **Amount formatting** (`lib/format.ts`): comma-grouped thousands + fixed 2 decimals + parentheses for negatives (`formatAmount`) — standard financial-statement convention. **CSV export does not reuse this formatter** — GL's own CSV cells are plain decimals with a leading minus sign, no thousands separator, no parentheses (spreadsheet-numeric-parsing convention, not human-display convention). `DownloadCsvButton` downloads GL's bytes unmodified, same "don't reshape what GL sent" posture as the PDF button. **Hierarchical rows:** Balance Sheet still carries `depth`, rendered with `depth`-proportional left padding. **Trial Balance dropped this entirely** — now a plain flat list, same row-component style as Budget vs Actual. **Profit & Loss is a nested object, not a flat array.** GL's response has named sections — `sales`, `costOfSales`, `otherIncome`, `distributionExpenses`, `administrationExpenses`, `otherExpenses`, `financialExpenses`, `unclassified` (only present with lines if the COA has untagged Income/Expense accounts — GL flags this as worth watching for during setup) — each `{ lines[], total }`, plus top-level `grossProfit`/`netProfitForPeriod`. The screen renders one bordered section per non-empty group, in this fixed order: Sales, Cost of Sales, **Gross Profit** (its own bold subtotal row, not a section), Other Income, the four expense groups, `unclassified` last if present, then **Net Profit for the Period**. **Cash Flow is a structured statement layout, not four `StatCard`s.** `Net Earnings` as its own line, an "Additions to Cash" bordered section and a "Subtractions From Cash" bordered section — client-side bucketed by sign from the combined `nonCashAdjustments[]` + `workingCapitalChanges[]` (a working-capital line's label uses its `direction` field, e.g. `direction: "Decrease"` + `accountName: "Trade Receivables"` → `"Decrease in Trade Receivables"`; a non-cash-adjustment line just prints its plain `description`, e.g. `"Depreciation"`, no Increase/Decrease prefix) — then `Net Cash From Operations` as a subtotal, Investing/Financing sections (their `lines[]`, no subtotal row when a section has only one line), ending in the final combined total. `openingCashBalance`/`closingCashBalance` are fetched (still in the response) but **not rendered anywhere on screen** — same "computed but not displayed" posture GL's own PDF/CSV takes. **Tax Report (`app/dashboard/ledgers/tax-report/page.tsx`) is an entirely new screen.** Layout: - A `periodStart`/`periodEnd` date-range picker, same component as every other period-based report. - **A collapsible "Adjustments" panel**, collapsed by default, with five optional numeric inputs — `Allowable Deductions`, `Other Taxable Income`, `Qualifying Payments / Reliefs`, `Surcharge / Education Levy`, and `Tax Rate Override` (%). Each maps directly to the report call's optional query params. - Result: a two-column `Description`/`Amount` table in GL's own fixed order (`profitBeforeTax` through `balanceTaxPayable`) — `Add:`/`Less:` prefixes are literal row labels the component renders per a fixed lookup, not derived from the amount's sign. Bold rows for the reconciliation checkpoints (`Adjusted Business Profit`, `Assessable Income`, `Taxable Income`, `Gross Tax Liability`, the final total). - **The final row's label depends on the sign of `balanceTaxPayable`** — GL's own PDF renders a negative value (credits/payments exceeded the gross liability) as **"BALANCE TAX REFUNDABLE"**, not a negative "payable" figure (confirmed live on GL's own seeded loss-making test data, `03_Progress_Tracker.md`). The on-screen table should match this exactly: `balanceTaxPayable >= 0` → label `"BALANCE TAX PAYABLE"`, amount as-is; `balanceTaxPayable < 0` → label `"BALANCE TAX REFUNDABLE"`, amount shown as its absolute value (not in parentheses — GL's own PDF changes the *label*, not the sign display, for this one row specifically). - Uses its own header block, not the shared `ReportHeader` — GL's own PDF gives this one report a fuller identity block (company name, address, TIN, BRN) via `Company:Address`/`Tin`/`Brn` config values (`05_LKAS_Report_PDF_Templates.md` §0/§8, confirmed implemented server-side). **Open question, genuinely unresolved:** GL exposes no endpoint returning these four values as data — they're `appsettings.json` entries, read only by the PDF renderer. The on-screen React version needs the *same* values from *somewhere*, and there's no way to fetch them from GL today. Two options, neither picked here: (a) GL adds a small `GET /company-info`-style endpoint; (b) the frontend duplicates these four values in its own config/env, accepting the risk of drifting out of sync with GL's `appsettings.json` whenever one side changes without the other. Flag this for a decision before building this screen's header — don't silently pick one. --- ## 4. Cash / Bank Accounts `app/dashboard/ledgers/bank-accounts/page.tsx` (list) + `/new/page.tsx` (create). No delete — GL's own reference has no delete endpoint for either concept, consistent with every other master in this system (FR-MD-08's deactivate-not-delete posture, though GL doesn't even expose deactivate here). **Rework — Cash and Bank are separate GL tables/endpoints, not one table with a type field.** An earlier design (drafted, never shipped on GL's side) proposed a single `bank_account.accountType` discriminator; GL's team judged a cash account a *peer* concept to a bank account, not a *kind of* bank account, and built two separate tables instead (`Accounting_System_Design_LKAS.md` §5.8). This still satisfies the original requirement — **the create page only ever offers exactly two choices, Cash or Bank, nothing else** — the two-choice constraint now lives in "which endpoint does the form submit to," not "which value of a discriminator field." ### The three endpoints this page now uses - **`POST /bank-accounts`** — unchanged in shape from the original 2026-07-18 design, except `glAccountId` → `glAccountCode` (Rule 8.2). Fields: `accountName` (required), `bankName`/ `accountNumber` (both optional/nullable — a Bank account can genuinely be created with these blank and filled in later, unlike Cash's stricter rule below), `glAccountCode` (required), `currencyCode` (optional, defaults `"LKR"`). - **`POST /cash-accounts`** *(new)* — `accountName` (required), `cashAccountTypeName` (required — matched case-insensitively against GL's `cash_account_type` reference table; a name with no existing match **creates a new type on the fly**, so a frontend "Other, please specify" free-text option becomes a permanent selectable choice for every future cash account created after it), `accountNumber` (**optional** — if omitted, GL auto-generates a sequential `CASH-000001`-style reference; if supplied, used as entered), `glAccountCode` (required), `currencyCode` (optional, defaults `"LKR"`). - **`GET /cash-account-types`** *(new)* — flat, unfiltered list, seeded with Petty Cash / Till Cash / Safe Cash / Cash in Transit, growing over time. Feeds the create form's Cash Account Type dropdown. ### Create form A **two-choice toggle** at the top — **Cash** or **Bank**, backed by a `CashBankAccountType` TS enum (`Cash`/`Bank`) — decides which of the two fieldsets below shows and which endpoint the form submits to: - **Bank selected:** Bank Name + Account Number fields (both optional on this side — GL doesn't enforce them as required for a Bank row), submits to `POST /bank-accounts`. - **Cash selected:** a Cash Account Type `` picker (bank account form, and Budget vs Actual's budget picker) displayed the selected item's **numeric value** after picking it, even though the correct id/code was genuinely being sent to the server. Cause: this app's `` (with `label={...}` set correctly from the start, avoiding the raw-number-display bug from §4's earlier fix) fed by `GET /cash-account-types`, an "Other, please specify" free-text option, `glAccountCode` picker now keyed by the account's code string instead of its numeric id. - Cash/Bank list page — server-side `accountType` filter (`Both`/`Bank`/`Cash`, a `` values, and both dialogs' status-based available-actions logic), not just the visible label. Added five `*_BY_CODE` lookup maps to `types/general-ledger.ts` (`CHEQUE_BOOK_STATUS_BY_CODE`/`CHEQUE_PAGE_ISSUE_STATUS_BY_CODE`/`PAYEE_TYPE_BY_CODE`/`RECEIVED_FROM_TYPE_BY_CODE`/`RECEIVED_CHEQUE_STATUS_BY_CODE`), keyed by the exact integer values `06_Enums_Reference.md` documents. Applied them in `lib/api/general-ledger.ts` via new `Raw*` types (describing what GL's JSON for these fields actually is: `number`/`number | null`) and `mapChequeBook`/`mapChequePage`/`mapReceivedCheque` helpers, wired into every `chequeBooksApi`/`chequePagesApi`/`receivedChequesApi` method that returns one of these shapes — the translation happens once, at the API boundary, so every existing page/dialog/badge map keeps comparing against the same string enum values as before and needed zero changes itself. Checked every other enum in `06_Enums_Reference.md`'s "Persisted enums" table (`JournalEntryStatus`/`PeriodStatus`/`TaxCalculationBasis`/`TaxAppliesTo`/`DepreciationMethod`/`FixedAssetStatus`/`AuditCategory`/`AuditAction`) against this frontend — none are consumed anywhere (no Journal Entry/Fiscal Period/Tax Code/Fixed Asset/Audit Log UI exists here), so Cheque Management is the complete fix, not a partial one. Verified: `tsc --noEmit`/`eslint` clean on both touched files. - [ ] **Not done — internal ERPCore→GL wiring.** Unrelated to this revision, still deferred (`docs/12` §6). - [ ] **Deferred — bank/cash account edit.** Needs GL service changes first (§4); now needs them for *both* tables, not just one. - [ ] **Open — Tax Report's company-identity header fields.** Still genuinely unresolved (§3, end); not picked in this pass either. The screen ships without them rather than guessing. --- *End of 21-GENERAL-LEDGER-FRONTEND.md. Transport: `docs/12-GENERAL-LEDGER-INTEGRATION.md`. General frontend rules: `20-FRONTEND.md`. Record work: `Frontend/PROGRESS.md`.*