alimarchal / laravel-chart-of-accounts
Laravel enterprise accounting package: chart of accounts, double-entry journals, multi-company, AR/AP, payroll, tax, WhatsApp, with React, Blade and API
Package info
github.com/alimarchal/laravel-chart-of-accounts-package
pkg:composer/alimarchal/laravel-chart-of-accounts
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- spatie/laravel-activitylog: ^4.7
- spatie/laravel-permission: ^6.0
- spatie/laravel-query-builder: ^5.0|^6.0
Requires (Dev)
- dompdf/dompdf: ^3.0
- inertiajs/inertia-laravel: ^1.0|^2.0|^3.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.13
- laravel/sanctum: ^4.0
- livewire/livewire: ^3.0|^4.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0|^5.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0|^5.0
Suggests
- dompdf/dompdf: Typeset PDF reports and vouchers with company header, logo and page numbers (^3.0); without it a built-in renderer is used
- inertiajs/inertia-laravel: Required for the Inertia/React frontend (ui_driver=inertia)
- laravel/sanctum: Default guard for the REST API (auth:sanctum)
- livewire/livewire: Required for the Blade/Livewire frontend (ui_driver=blade), ^3.0|^4.0
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.42.1
- v2.42.0
- v2.41.2
- v2.41.1
- v2.41.0
- v2.40.1
- v2.40.0
- v2.39.0
- v2.38.0
- v2.36.0
- v2.35.0
- v2.34.0
- v2.33.0
- v2.32.0
- v2.30.0
- v2.29.0
- v2.28.0
- v2.27.0
- v2.26.0
- v2.25.0
- v2.24.0
- v2.23.0
- v2.22.0
- v2.21.0
- v2.20.0
- v2.19.0
- v2.18.0
- v2.17.0
- v2.16.0
- v2.15.0
- v2.14.0
- v2.13.0
- v2.12.0
- v2.11.0
- v2.10.0
- v2.9.0
- v2.8.0
- v2.7.0
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.0
- v2.2.0
- v2.1.0
- v2.0.0
- v1.4.0
- v1.3.9
- v1.3.8
- v1.3.7
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.1
- v0.0.1
- dev-claude/rollback-self-refs
- dev-claude/tender-goodall-qn4rzv
- dev-claude/opening-balances
- dev-claude/karpathy-plugin
- dev-claude/karpathy-skills
- dev-rel240
- dev-claude/fix-75-restore-fifo
- dev-claude/fix-98-opening-documents
- dev-claude/fix-87-gl-date-filters
- dev-claude/fix-75-gl-aging-fifo
- dev-claude/fix-97-consolidated-key
- dev-claude/fix-59-day-basis
- dev-claude/fix-60-working-hours
- dev-claude/fix-70-asset-opening-depreciation
- dev-claude/fix-71-double-submit
- dev-claude/fix-85-migration-rollback
- dev-claude/fix-52-trial-balance-closing
- dev-claude/fix-99-bank-foreign-currency
- dev-claude/fix-81-token-abilities
- dev-claude/fix-68-rates-per-company
- dev-claude/fix-82-features-super-admin
- dev-claude/fix-83-users-company-scope
- dev-claude/fix-80-stock-backdate
- dev-claude/fix-79-disposal-backdate
- dev-claude/fix-95-voucher-type-collision
- dev-claude/fix-54-account-code-case
- dev-claude/fix-56-reopen-reason
- dev-claude/fix-55-run-now-early
- dev-claude/fix-58-housekeeping
- dev-claude/fix-57-money-boundary
- dev-claude/fix-109-role-account-type
- dev-claude/fix-111-control-offset
- dev-claude/fix-89-fbr-void
- dev-claude/fix-77-zero-price-line
- dev-claude/fix-102-role-in-use
- dev-claude/fix-101-last-super-admin
- dev-claude/fix-104-inactive-costcenter-currency
- dev-claude/fix-65-edit-self-approval
- dev-claude/fix-100-payroll-preparer
- dev-claude/fix-90-settlement-draft-run
- dev-claude/fix-96-loan-void-reopen
- dev-claude/fix-110-allocation-date
- dev-claude/fix-112-account-type-in-use
- dev-claude/fix-105-monthly-reconciliation
- dev-claude/fix-66-voucher-deadlock
- dev-claude/fix-69-rs-prefix-amount
- dev-claude/fix-106-base-currency-all-companies
- dev-claude/fix-84-posted-guard-columns
- dev-claude/fix-74-record-last-day
- dev-claude/fix-53-reversal-approval
- dev-claude/fix-64-module-controls
- dev-claude/fix-94-merge-references
- dev-claude/fix-92-merge-foreign-currency
- dev-claude/fix-93-merge-date
- dev-claude/fix-108-reconciliation-delete
- dev-claude/fix-107-reconciliation-readonly-status
- dev-claude/fix-78-document-overflow
- dev-claude/fix-86-inventory-exact-value
- dev-claude/fix-67-closing-foreign-currency
- dev-claude/fix-73-reverse-inactive-account
- dev-claude/fix-72-closing-inactive-account
- dev-claude/fix-113-tax-code-kind
- dev-claude/fix-63-tax-code-delete
- dev-claude/fix-103-form-money-rules
- dev-claude/fix-91-form-rate-zero
- dev-claude/fix-62-base-rate-one
- dev-claude/fix-61-fx-rate-default
- dev-claude/harden-money-webhook-payroll-errors
- dev-claude/simpler-payroll-and-forms
- dev-claude/menu-and-help
- dev-claude/ui-polish-all-screens
- dev-claude/ui-redesign
- dev-claude/jetstream
- dev-claude/installer
- dev-claude/whatsapp
- dev-claude/final-audit
- dev-claude/guide-ur
- dev-claude/payroll-control
- dev-claude/feature-toggles
- dev-claude/payroll-output
- dev-claude/payroll-operations
- dev-claude/payroll-structure
- dev-claude/security
- dev-claude/i18n-ur
- dev-claude/performance
- dev-claude/fbr
- dev-claude/payroll
- dev-claude/inventory
- dev-claude/fixed-assets
- dev-claude/dashboard
- dev-claude/ar-ap
- dev-claude/tax-engine
- dev-claude/budgets
- dev-claude/bank-statement-import
- dev-claude/fx-revaluation
- dev-claude/recurring-entries
- dev-claude/coa-templates
- dev-claude/report-mapping
- dev-claude/coa-merge-renumber
- dev-claude/coa-import-export
- dev-claude/pdf-professional
- dev-claude/users-roles
- dev-claude/attachments
- dev-claude/control-accounts
- dev-claude/source-documents
- dev-claude/voucher-numbering
- dev-claude/coa-db-guards
- dev-claude/period-close
- dev-claude/multi-company
- dev-claude/release-automation
- dev-claude/enterprise-features
- dev-claude/production-hardening
This package is auto-updated.
Last update: 2026-10-24 10:42:58 UTC
README
Chart of Accounts and double-entry accounting module for Laravel. One-command install. Works with Jetstream (Blade/Livewire) and Breeze (Inertia/React).
Author: Ali Raza Marchal — kh.marchal@gmail.com Package: alimarchal/laravel-chart-of-accounts Source: github.com/alimarchal/laravel-chart-of-accounts-package
Help & guides: Help catalog — every screen and API endpoint with its permission · اردو استعمال گائیڈ (Urdu user guide with screenshots, right-to-left) · OpenAPI · Postman · UAT checklist
Highlights
- Double-entry journal — draft → posted → reversed / void, balanced in exact cents
- Branches — head office, campuses or shops inside one company: a working-branch switcher, entries, invoices and payments booked to a branch, branch-wise reports, and users limited to their branches
- Multi-company — any number of companies in one database, each with its own chart, periods, journal and reports; per-user access, a company switcher, and consolidated group reports
- Tamper-proof ledger — posted entries are immutable at the database layer (triggers on MySQL/MariaDB, PostgreSQL, SQLite), the chart-of-accounts rules (valid tree, locked used accounts, same-company lines, no posting to group accounts) are enforced there too, and every change is written to an audit log
- Maker-checker approvals — entries above a threshold need a second person to approve; nobody approves their own work
- Roles with segregation of duties — super-admin, admin, accountant (maker), approver (checker), auditor, viewer;
accounting:rolesaudits the matrix - Multi-currency — every line keeps its frozen base-currency amount; all reports are in the base currency
- Voucher numbering — JV, CPV, CRV, BPV, BRV (and your own types) with gapless numbers such as
JV-2026-00012, issued at posting, restarting per fiscal year or month, locked in the database - Source documents — every entry can name the invoice, bill or receipt it records (and link to your own
Invoice/Billmodel); a document can be posted only once, enforced by the database - Attachments — scanned bills, receipts and contracts on every entry, stored privately, never removable once posted, duplicate-file warning, optional "evidence required above" amount
- Recurring entries — rent, subscriptions, depreciation and accruals on a daily-to-yearly schedule: generated as drafts or posted automatically, with catch-up, pause/resume, run-now and a full history
- ERPNext standard chart on a fresh install — a new company starts with ERPNext's chart of accounts and its account numbers (
1110 Cash,1310 Debtors,2110 Creditors,4110 Sales…), already wired to the modules through account roles; pick the package's own general or the school chart instead with--chart= - Account roles — which account is "cash", "bank", "salary expense", "receivables in the ageing" … is a setting per company (screen, API,
accounting:account-roles), not a code hard-wired in the program - Filters and search on every list — one filter language (
?filter[search]=…, dates, amounts, status, sorting) on every screen, React and Blade, and every API list, built on Spatie Laravel Query Builder; reports and their exports use the same dates - Industry chart templates — trading, manufacturing, services, school, NGO and healthcare charts: start a company from one, or add an industry's accounts to the chart you have (existing accounts are never changed); add your own in config
- Chart import & export — bring your existing chart in from Excel or CSV with a line-by-line preview (new / updated / errors) before anything is saved; export it, edit it, import it back
- Renumber & merge accounts — give an account (and its sub-accounts) new codes, or fold a duplicate into another account with a posted transfer entry — the ledger history is never rewritten
- Control accounts — receivables, payables, inventory, payroll and tax control accounts that only their module may post to, with a manual-postings exception list
- Professional PDFs — reports on your letterhead (logo, filters, totals, page X of Y) and printable vouchers with amount in words, signature boxes and a DRAFT/VOID watermark; big Excel/PDF exports run in the background
- Month-end & year-end close — a close workspace with a checklist (drafts, approvals, trial balance, bank reconciliation, earlier periods), a closing-entry preview, monthly periods, and audited reopening
- Financial statements by report lines — map accounts to statement lines (Revenue, Cost of sales, Trade receivables, PPE …); balance sheet and income statement with gross / operating / net profit and comparatives; indirect-method cash flow that always reconciles to cash
- 12 reports — trial balance, balance sheet, income statement, cash flow, general ledger, account statement (running balance), bank & cash book, aged AR/AP — export to CSV (streamed, any size), XLSX, PDF
- REST API (OpenAPI 3.1 + Postman), React (Inertia) and Blade/Livewire UIs — or API only
- Events & signed webhooks for every ledger action (posted, reversed, approved, period closed, …)
- Proven at scale — 400,000 journal lines: trial balance 0.77 s, account statement 0.1 s, 46 concurrent postings/s with zero imbalance (performance report)
- Payroll — employees, grades, quantity x rate pay, arrears, attendance and leave, loans, EOBI/PESSI/PF, bank file, payslip PDF/e-mail, settlements, tax certificates, HR-to-finance approval, bonuses
- Feature switches — turn any module or payroll feature off and on from a screen, the API or
php artisan accounting:features; menus and routes follow, data stays - Help catalog and Urdu guide — every screen and endpoint listed, plus a right-to-left Urdu manual with screenshots of every screen
- Tested — 590+ Pest tests in CI on PHP 8.2–8.4, Laravel 11–13 (+ Laravel 14 dev), SQLite/MySQL/MariaDB/PostgreSQL; Larastan level 5
How it works (visual guide)
Every diagram is generated from the Mermaid sources in
docs/diagrams/(SVG + PNG indocs/images/).
1. Architecture — who calls what
2. Installation flow
3. Journal entry lifecycle
4. What happens when an entry is posted
* The control-account check is skipped for reversals and year-end closing entries, which follow their source entry.
5. Month-end and year-end close
6. An API request, step by step
7. Maker-checker approval
8. Multi-company: which company a request works in
Requirements
| Supported | |
|---|---|
| PHP | 8.2, 8.3, 8.4 (Laravel 13 needs ≥ 8.3) |
| Laravel | 11, 12, 13 — Laravel 14 (expected Q1 2027, PHP ≥ 8.4) is tracked in CI against its dev branch and will be supported on release |
| Database | MySQL 8 · MariaDB 10.6+ · PostgreSQL 13+ · SQLite 3.35+ |
| React UI | Laravel React starter kit — Inertia 3 (current kit) or Inertia 2 (small app.tsx addition, see Installation) |
| Blade UI | Livewire 3 or 4 |
| API auth | Laravel Sanctum (or any guard via ACCOUNTING_API_MIDDLEWARE) |
Installation (5 minutes)
1. Install the package
composer require alimarchal/laravel-chart-of-accounts
php artisan install:api # Sanctum, for the REST API (skip if already installed)
2. Add the Spatie trait to app/Models/User.php (and HasApiTokens if you use the REST API)
use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { use HasFactory, HasRoles, Notifiable; }
3. Run the installer, and answer its questions
php artisan accounting:install
The installer asks what it needs, so there is nothing to edit by hand afterwards:
| Question | What it does |
|---|---|
| Which screens do you want? | Livewire starter kit (Flux), Blade (Jetstream / Breeze), React (Inertia) or REST API only (it suggests the one that matches your starter kit) → ACCOUNTING_UI_DRIVER and ACCOUNTING_BLADE_KIT |
| Base currency | ACCOUNTING_BASE_CURRENCY (PKR, USD, …) |
| Chart of accounts | ERPNext standard chart, general business chart or school chart → ACCOUNTING_CHART_PRESET |
| Several companies? | ACCOUNTING_MULTI_COMPANY and the short code of your first company → ACCOUNTING_DEFAULT_COMPANY |
| Approval by a second person? | maker-checker, its threshold and whether one may approve their own entry → ACCOUNTING_APPROVALS_ENABLED, ACCOUNTING_APPROVAL_THRESHOLD, ACCOUNTING_ALLOW_SELF_APPROVAL |
| URL of the screens | default accounting; the starter kit's own URLs (settings, dashboard, login, …) are refused → ACCOUNTING_ROUTE_PREFIX |
| Email, name and password of the administrator | the user is created when it does not exist, verified, and given super-admin |
Then one yes/no question per topic, and a "yes" asks that topic's settings (each shows the current or default value; an empty answer
leaves the package default): REST API (prefix, rate limit, page size), WhatsApp (sandbox or live, token, phone number ID, business account ID, app secret,
verify token, country code, language, receipt template, queue), signed
webhooks (URLs, secret, attempts, timeout), payroll (days per month, approval, automatic run, overtime, gratuity, tax year), budgets and bank statements,
PDF, exports and attachments, language and modules you will not use (switched off through ACCOUNTING_FEATURES_DISABLED). Secrets (verify token,
webhook secret) can be generated for you. --advanced asks every topic without the yes/no. Every key the package reads is covered by these questions
or the ones above; .env keys that are not asked keep their documented defaults.
Your answers are written to .env. Then the installer publishes the config and migrations, migrates, seeds the chart of accounts,
currencies, the current fiscal year, roles and tax codes, creates the database views and triggers, and verifies everything. It adds
the Accounting menu (one entry that opens into sections, settings and help included) next to Dashboard in the menu of your starter kit (Livewire/Flux: resources/views/layouts/app/sidebar.blade.php;
Jetstream: resources/views/navigation-menu.blade.php; Breeze: resources/views/layouts/navigation.blade.php; React: resources/js/components/app-sidebar.tsx
and app-header.tsx) and, for Blade, adds the package's screens to the Tailwind sources (resources/css/app.css for Tailwind 4, tailwind.config.js
for Tailwind 3 as in Jetstream and Breeze; run npm run build once). It is safe to re-run.
Everything can be given as an option instead (no questions): --ui=livewire|blade|inertia|api, --currency=PKR, --chart=erpnext|general|school,
--prefix=accounting, --admin-email=… --admin-name=… --admin-password=…; --no-menu keeps your sidebar untouched, --no-env leaves
.env alone, and --setup-only asks the questions, writes .env, creates the administrator and adds the menu without publishing, migrating or seeding.
The starter kits of Vue and Svelte are supported through the Blade screens (the installer tells you which plain link to add to your sidebar).
Log in with the administrator you entered (the login page is your starter kit's) and open Accounting in the sidebar, or /accounting.
Your UI (the installer sets it in .env):
ACCOUNTING_UI_DRIVER |
You get | Notes |
|---|---|---|
inertia (default) |
React pages in resources/js/pages/accounting |
React starter kit; run npm run build afterwards |
blade |
Blade pages inside your layout | Livewire starter kit, Jetstream, Breeze (also Vue and Svelte kits) |
api |
REST API only | Lightest: no web routes, views or Livewire |
The installer offers three kinds of screens: React, Blade for Jetstream / Breeze and Livewire for the Laravel Livewire starter kit (Flux).
The two Blade kinds are the same pages (ACCOUNTING_UI_DRIVER=blade); ACCOUNTING_BLADE_KIT tells them which shell to look native in:
ACCOUNTING_BLADE_KIT |
For | Look |
|---|---|---|
auto (default) |
whatever your application has | Jetstream, else Flux (Livewire kit), else Breeze |
flux |
Livewire starter kit | Flux style: flat bordered cards, zinc colours, buttons as in Flux, dark mode with the kit's .dark switch |
jetstream |
Laravel Jetstream | Jetstream cards, dark mode follows the system setting |
breeze |
Laravel Breeze | Breeze cards, dark mode with the .dark class |
plain |
none | the package's own page layout |
Matches the official starter kits (laravel.com/docs/starter-kits): Livewire (Flux, sidebar or header layout, also
with teams or WorkOS) gets the Flux look; React gets the React pages and the sidebar/header link; Vue and Svelte have no Blade layout, so the
Blade screens use the package's own page layout and you add one plain <a href="/accounting"> link (not an Inertia Link); Jetstream and Breeze (no longer
developed by Laravel, still widely used) are supported as described above. In the Flux sidebar layout the pages use the full width next to the sidebar.
The screens are rendered in your application's layout: Jetstream and Breeze <x-app-layout>, the Livewire kit its livewire.component_layout
(layouts::app), so the sidebar stays; anything else gets the package's own layout. Scripts and styles that the screens need (the filter button,
the select boxes) are printed for you when your layout has no @stack('scripts'), @stack('styles') or @stack('modals'). To name your own layout
component: ACCOUNTING_BLADE_LAYOUT=layouts::app.
What it looks like in each kit (a real accounting:install on a fresh Laravel Livewire starter kit and on Jetstream; the screenshots are of the Blade screens):
| Livewire starter kit (Flux) | Jetstream |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| Filters: type a value and press Apply filters (Livewire 4 kit) | The list after the filter |
|---|---|
![]() |
![]() |
| A confirm dialog instead of the browser's own (Jetstream) | The same in React |
|---|---|
![]() |
![]() |
Checked with a browser in three real apps (the Laravel Livewire starter kit of Livewire 3 and the current one of Livewire 4, and Jetstream), each after a real accounting:install: every one of the 89 list, report and form pages opens without errors, every filter button opens its filters and
applies them, the "add" forms open and refuse an empty submit, a journal entry can be created from the form, a branch can be created, edited and filtered, and the branch switcher limits lists and reports. The React screens of the branches were checked the same way.
React: the menu link by hand and (older starter kits) the layout
The installer adds this itself; to do it by hand add Accounting to resources/js/components/app-sidebar.tsx:
import { Calculator } from 'lucide-react'; const mainNavItems: NavItem[] = [ { title: 'Dashboard', href: dashboard(), icon: LayoutGrid }, { title: 'Accounting', href: '/accounting', icon: Calculator }, ];
The pages declare their breadcrumbs with Page.layout = { breadcrumbs }. The current starter kit
(Inertia 3) wraps them in the app layout automatically. Starter kits on Inertia 2 need this
resolve in resources/js/app.tsx (tested with the February 2025 kit):
import type { ReactNode } from 'react'; import AppLayout from './layouts/app-layout'; import type { BreadcrumbItem } from './types'; createInertiaApp({ resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')).then((module) => { const page = (module as { default: { layout?: unknown } }).default; const options = page.layout as { breadcrumbs?: BreadcrumbItem[] } | undefined; if (name.startsWith('accounting/') && typeof options !== 'function') { page.layout = (child: ReactNode) => <AppLayout breadcrumbs={options?.breadcrumbs}>{child}</AppLayout>; } return module; }), // ... });
Optional settings
ACCOUNTING_BASE_CURRENCY=USD # default PKR; set before installing ACCOUNTING_CHART_PRESET=erpnext # erpnext (default), general or school ACCOUNTING_ROUTE_PREFIX=accounting # URL prefix for the web UI ACCOUNTING_MULTI_COMPANY=true # several companies (see below) ACCOUNTING_APPROVALS_ENABLED=true # maker-checker (see below) ACCOUNTING_APPROVAL_THRESHOLD=100000 # in base currency; 0 = every entry ACCOUNTING_WEBHOOK_URLS=https://erp.example.com/hooks/accounting ACCOUNTING_WEBHOOK_SECRET=change-me
API quick start
# A token for an existing user (or issue tokens from your own login endpoint) php artisan tinker --execute="echo App\Models\User::first()->createToken('erp')->plainTextToken;"
TOKEN=1|xxxxxxxx API=http://localhost:8000/api/v1/accounting H=(-H "Accept: application/json" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json") # Is everything installed? curl "${H[@]}" $API/health # Easiest possible entry: debit one account, credit another (posts immediately) curl "${H[@]}" -X POST $API/journal-entries/simple \ -H "Idempotency-Key: rent-2026-10" \ -d '{"debit_account_code":"5102","credit_account_code":"1101","amount":2500,"description":"Office rent"}' # Multi-line entry using account codes — no ids needed curl "${H[@]}" -X POST $API/journal-entries -d '{ "entry_date": "2026-10-04", "reference": "INV-1001", "auto_post": true, "lines": [ {"account_code": "1103", "debit": 1500, "credit": 0, "cost_center_code": "ADMIN"}, {"account_code": "4101", "debit": 0, "credit": 1500} ]}' # Balances and reports curl "${H[@]}" "$API/chart-of-accounts/tree" curl "${H[@]}" "$API/reports/trial-balance" curl "${H[@]}" "$API/reports/income-statement?date_from=2026-01-01&date_to=2026-12-31" curl "${H[@]}" "$API/reports/balance-sheet?as_of_date=2026-12-31"
- Full reference:
docs/openapi.yaml(OpenAPI 3.1 — open it in Swagger UI, Redoc, Stoplight, or import into Insomnia). - Postman: import
docs/postman_collection.json, set thehostandtokenvariables, send. - Retries are safe: send an
Idempotency-Keyheader when creating entries — a retry returns the original entry (200,Idempotent-Replayed: true) instead of posting twice. - Errors are predictable:
401no token ·403missing permission ·404not found ·422validation (errors) or accounting rule (message) ·429rate limit.
Upgrading
2.42 — one migration per table. The migrations were rewritten: there is now one create_…_table migration for each table (68 files, in
dependency order, no add_… migrations) and the database objects (views and triggers) are created by the last one. The resulting schema is
the same, but the file names changed, so an installation that already ran the old migrations should be rebuilt (php artisan migrate:fresh
on a development database, or export and re-import the data); this package is still in development, so no upgrade path is provided.
After composer update alimarchal/laravel-chart-of-accounts:
php artisan accounting:update
Refreshes package-owned files (React pages / Select2 assets), runs new migrations (--force), re-syncs the
database views and triggers, and adds roles/permissions introduced by the new version (your changes to
existing roles are kept). It never overwrites your config/accounting.php or customised views
(--views re-publishes Blade views explicitly).
Upgrading to 2.34. The default chart of a new installation is now the ERPNext chart (ACCOUNTING_CHART_PRESET=erpnext); companies that
already have a chart are not touched and keep the account codes they used (cash 1101, receivables 1103 …). The fixed account codes in
config/accounting.php (defaults.*, aging.*) are now optional overrides — if your published config still has them, they keep winning
and nothing changes. Lists accept the shared filters; an unknown filter[...] now answers 400 instead of being ignored by the lists that
used to read their own parameters.
Route Configuration
By default the package uses the /accounting/ URL prefix. To change it:
# .env ACCOUNTING_ROUTE_PREFIX=settings # URLs: /settings/journal-entries, /settings/reports/... ACCOUNTING_ROUTE_NAME_PREFIX=accounting # Route names stay: accounting.dashboard, accounting.journal-entries.index
Route names stay as
accounting.*regardless of the URL prefix, so views and redirects work without changes.
Settings-style routes example (used in our demo):
| URL | Route Name |
|---|---|
/settings |
accounting.dashboard |
/settings/journal-entries |
accounting.journal-entries.index |
/settings/reports/general-ledger |
accounting.reports.general-ledger |
/settings/chart-of-accounts |
accounting.chart-of-accounts.index |
/settings/periods |
accounting.periods.index |
/settings/users |
settings.users.index |
/settings/roles |
settings.roles.index |
/settings/permissions |
settings.permissions.index |
Creating Journal Entries — Three Ways
1. Static Helper — JournalEntry::record()
The fastest way to create a balanced GL entry programmatically:
use Alimarchal\LaravelChartOfAccounts\Models\JournalEntry; // Create a draft entry $entry = JournalEntry::record( description: 'Office rent payment', debitAccountCode: '5102', // Rent Expense creditAccountCode: '1101', // Cash In Hand amount: '150000.00', // strings avoid float rounding post: false, // save as draft ); // Create and post immediately $entry = JournalEntry::record( description: 'Salary payment — June 2026', debitAccountCode: '5101', // Salary Expense creditAccountCode: '1108', // Operating Bank Account amount: 500000, post: true, // post immediately reference: 'SAL-2026-06', ); echo $entry->status; // 'posted' echo $entry->reference; // 'SAL-2026-06' echo $entry->id; // auto-assigned ID
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
description |
string |
Yes | Human-readable transaction description |
debitAccountCode |
string |
Yes | Account code for the debit line (e.g. '5101') |
creditAccountCode |
string |
Yes | Account code for the credit line (e.g. '1101') |
amount |
float|int|string |
Yes | Amount — same value debited AND credited (rounded to cents) |
post |
bool |
No | true = post immediately, false = draft (default) |
reference |
string|null |
No | Optional voucher/invoice reference number |
What it does automatically:
- Resolves account codes to IDs via
ChartOfAccount - Finds active base currency
- Finds currently open accounting period
- Creates entry header + two perfectly balanced lines
- Optionally posts via
JournalEntryService::post() - Wraps in a DB transaction
Returns: Fresh JournalEntry model.
Throws: ModelNotFoundException if an account code or an open period for today is not found;
AccountingException if posting violates a rule (unbalanced, group/inactive account, closed period).
JournalEntry::record()posts as the current user without a permission check — it is a server-side API. Authorize the caller yourself (e.g.Gate::authorize('journal-entries.post')) when exposing it to users.
2. REST API
Create a draft entry:
POST /api/v1/accounting/journal-entries Authorization: Bearer {token} Content-Type: application/json { "entry_date": "2026-06-04", "accounting_period_id": 1, "currency_id": 1, "fx_rate_to_base": 1, "reference": "SAL-2026-06", "description": "Salary payment June 2026", "lines": [ { "chart_of_account_id": 42, "debit": 500000, "credit": 0, "description": "Salaries Expense" }, { "chart_of_account_id": 11, "debit": 0, "credit": 500000, "description": "Cash" } ] }
Post entry:
POST /api/v1/accounting/journal-entries/{id}/post Authorization: Bearer {token}
Reverse entry (reversal_date optional, defaults to today, cannot precede the original):
POST /api/v1/accounting/journal-entries/{id}/reverse Authorization: Bearer {token} Content-Type: application/json { "description": "Reversal of SAL-2026-06", "reversal_date": "2026-06-30" }
auto_post: true on create/update additionally requires the journal-entries.post permission.
Errors: validation failures return 422 with errors; accounting rule violations (unbalanced entry,
closed period, already reversed, account in use, …) return 422 with a message.
Void entry:
POST /api/v1/accounting/journal-entries/{id}/void Authorization: Bearer {token}
List with filters:
GET /api/v1/accounting/journal-entries?filter[status]=posted&filter[entry_date_from]=2026-06-01&sort=-entry_date
3. Web UI (Blade/Livewire)
Visit /settings/journal-entries/create (or /accounting/journal-entries/create).
UI features:
- All account and cost center dropdowns use Select2 with search
- Real-time balance status badge (green = balanced, red = not balanced)
- Save Draft button is disabled until debits = credits — prevents unbalanced saves
- Live debit/credit totals update as you type
Recurring entries
Dashboard → Recurring Entries (React and Blade) or /api/v1/accounting/recurring-entries. A template holds a balanced
set of lines and a schedule; the scheduler turns it into journal entries.
- Schedule: daily, weekly, monthly, quarterly or yearly, every N periods, from a first date, optionally until an end date or after N entries. Monthly dates keep their day and are clamped in short months (the 31st becomes the 28th in February, then returns to the 31st).
- Each entry is created as a draft, or posted automatically. Posting runs as the template's creator: if they may post, it posts; under maker-checker it is submitted for approval; a closed period, a control account or a creator without posting rights leaves it a draft with the reason on the run. The schedule moves on either way.
- Runs are logged (one per scheduled date, enforced by a unique index, so two schedulers cannot duplicate an occurrence). An entry that cannot even be created stops the template at that date and is retried next time.
- Catch-up: if the scheduler was off, the missed occurrences are generated (dated on their scheduled days), up to
ACCOUNTING_RECURRING_MAX_CATCH_UP(12) per run. A paused template skips what it missed when resumed (or generates it). - Generate now makes the next occurrence immediately; pause / resume; templates that have generated entries are paused, not deleted. Amounts are in the base currency.
php artisan accounting:run-recurring # generate what is due (every company) php artisan accounting:run-recurring --dry-run # list what is due php artisan accounting:run-recurring --date=2026-12-31 --company=SUB
The package schedules the command daily at ACCOUNTING_RECURRING_TIME (02:00) — your app needs the Laravel scheduler
(* * * * * php artisan schedule:run); ACCOUNTING_RECURRING_SCHEDULE=false leaves scheduling to you. Permissions
recurring-entries.view / create / update / delete / run (accountant: all; approver, auditor, viewer: view).
Customers, suppliers, invoices and bills
Dashboard → Customers & Suppliers, Invoices & Bills, Receipts & Payments (React and Blade) or
/api/v1/accounting/parties, /party-documents, /party-payments. The receivables and payables sub-ledger: who owes you,
whom you owe, and the ledger moves with it.
- Parties: customers, suppliers or both, with payment terms (the default due date), an optional credit limit (a warning on the account page when the balance passes it), a tax number and, optionally, their own receivable / payable account. Otherwise documents post to the receivables / payables control account (Control accounts), so only this module — not a manual entry — moves those balances.
- Documents: invoice, credit note (customers), bill, debit note (suppliers). Lines carry an account, quantity, unit price
and an output (sales) or input (purchase) tax code, tax-exclusive or inclusive prices; amounts and tax are worked out on
the issue date. A document is a draft until posted; posting gives it a gapless number (
INV-2026-00001, per kind and year; a posting that fails gives the number back) and books one entry: the control account against the lines and their tax, the tax lines marked for the tax report, with the invoice number as the entry's source document. A posted document is never edited: void it (its entry is reversed) and issue a new one. - Receipts and payments are posted when recorded (bank or cash account against the control account) and numbered (
RCT-…,PAY-…). Allocate them to specific invoices (bills), or leave the oldest due first; what is not allocated stays on the account as an unapplied payment. A credit (debit) note is applied to invoices (bills) the same way. Allocations can be undone; a document with allocations cannot be voided until they are. - Statements and open items: per party, the open invoices with days overdue, unapplied credits and payments, and a statement of account for any period with a running balance.
- Ageing by customer or supplier (not due, 1–30, 31–60, 61–90, over 90 days past due, unapplied, total) as of any date, with CSV / XLSX / PDF export — and a check against the control account: the sub-ledger total must equal the ledger, and any difference (a manual posting on the control account) is shown.
- Base currency only in this release.
Permissions parties.view / create / update / delete, party-documents.view / create / update / delete / post / void and
party-payments.view / create / void (accountant: all; approver, auditor, viewer: view). Audited (PARTY_*).
Tax
Dashboard → Tax (React and Blade) or /api/v1/accounting/tax/…. Tax codes and their dated rates (Accounting →
Tax codes) drive a tax ledger and tax returns.
- Tax codes now have a kind — output (collected on sales), input (paid on purchases), withheld (deducted from payments we make) or advance (deducted from payments to us) — the tax account the tax is booked to, and a jurisdiction. A rate is picked by date: the active rate with the latest start on or before the document's date.
- Calculator: base, tax and total for an amount, tax-exclusive or inclusive, rounded to the cent (an inclusive amount always splits exactly: base + tax = amount).
- Taxed documents: Tax → New taxed document books an invoice, bill, credit or debit note, or a payment / receipt with
tax withheld in one go — the tax is worked out and booked to the code's tax account, with a live preview. In the API and
in any journal entry a line may carry
tax_code_id(andtax_inclusive): it is split into the taxable amount and a tax line on the same side. Both lines are marked (tax_rolebase / tax), which is what the reports read; zero-rated and exempt sales keep only the marked base. - Tax report: per tax code the taxable base, the tax and the number of documents for any period, from posted entries in the base currency (credit notes and returns reduce them), with the documents behind it, and CSV / XLSX / PDF export. Totals: output tax, input tax, net payable, tax withheld by us, advance tax.
- Tax return: File a return settles a period in one entry dated at its end — output tax debited and input tax credited
on their tax accounts, the difference on the payable account you pick (a refund debits it). Periods cannot overlap; a
return can be voided (its entry is reversed and the period can be filed again). Audited (
TAX_RETURN_*). - Editing a draft keeps its tax markers (send
tax_code_id,tax_roleandtax_rateback with the lines). Withholding you remit, or advance tax you claim, is an ordinary payment / entry on the withholding account.
Permissions tax-returns.view (accountant, approver, auditor, viewer), tax-returns.file and tax-entries.create
(accountant); the calculator needs tax-codes.view.
Urdu and right-to-left
The screens can be shown in Urdu (اردو), right to left. A language switcher appears on every screen (a selector in the company bar
in Blade, a small selector in the corner in React); the choice is kept for the session, and ?lang=ur selects it directly.
ACCOUNTING_LOCALE=ur makes Urdu the default.
- How it works: phrases are translated one by one: any visible text, placeholder, title or aria-label that equals a key of
resources/lang/ur.jsonexactly is replaced (Blade pages on the server, React pages in the browser). A phrase that is not in the dictionary, or has a name or number inside it, stays in English. About 500 phrases are included: the module names, buttons, statuses, table headings and report names used across the screens. - Your own wording: put a
lang/ur.jsonin your application with the phrases you want to add or change; it wins over the package's.php artisan vendor:publish --tag=accounting-langcopies the package dictionary as a starting point. More languages: add them toaccounting.locales(andrtl_localesif they run right to left) with their ownlang/<code>.json. - Direction:
langanddir="rtl"are set on the page, with a small stylesheet that flips alignment and spacing, uses an Urdu-capable font stack and moves the starter kit's sidebar to the right. Numbers, dates and amounts keep Western digits, as accounts are usually kept. - Not translated: the API (its messages stay as they are for integrations), the PDFs and Excel exports, Livewire updates after the first render, texts that include data (for example "Last 6 months, closing entries excluded"), and the host application's own menus. The Urdu terms were written without review by an accountant: have yours check them and override what they would say differently.
The Accounting menu, the two parts of the area, and the help centre
One entry in your starter kit's menu. Instead of two loose links, the installer adds a single Accounting menu next to Dashboard. It opens into sections, in the same order in every kit, and shows each person only what their role may open (a module you switched off disappears too):
| Section | Items |
|---|---|
| Overview | Overview (the dashboard), All modules |
| Journal & ledger | Journal entries, Recurring entries, Chart of accounts, Account types, Currency revaluation, Balance snapshots |
| Sales & purchases | Customers & suppliers, Invoices & bills, Receipts & payments, Aged receivables, Aged payables, Tax returns, WhatsApp |
| Banking | Bank accounts, Bank statements, Reconciliations, Bank book, Cash book |
| Payroll | Payroll runs, Employees, Attendance & leave, Loans & advances, Payroll reports & tax |
| Assets & stock | Fixed assets, Inventory |
| Reports | Trial balance, General ledger, Balance sheet, Income statement, Cash flow, Financial statements, Account balances, Budgets, Report mapping |
| Settings: Users & access | Users, Roles, Permissions, Audit log |
| Settings: Set-up | Accounting periods, Currencies, Cost centers, Branches, Voucher types, Tax codes, Tax rates, Control accounts, Account roles, Modules on / off |
| Help | Help centre |
| Livewire / Flux sidebar | React sidebar |
|---|---|
![]() |
![]() |
| Jetstream / Breeze top navigation | Flux header layout |
![]() |
![]() |
- The list lives in one place,
Support/AccountingMenu: a new screen is added there once, never in your host files. The host file holds one line: Flux sidebar<x-accounting::menu.sidebar />(variant="sidebar"for Flux 2), top navigation<x-accounting::menu.dropdown />with its mobile twin<x-accounting::menu.responsive />, React sidebar<AccountingNav />below<NavMain />(header layout:<AccountingHeaderMenu />), all from@/components/accounting/accounting-nav. The React menu is shared with every page of the application (it sits in your own sidebar), so it also shows on the starter kit's Dashboard. - An installation with the two old links (Accounting and Accounting settings) is moved to the new menu by
php artisan accounting:install(or--setup-only); nothing else needs doing. - The All modules page (
/accounting) and its Settings tab remain, as cards grouped like the menu, each with a one-line description.
Help on every screen
The Help centre (/accounting/help, a Help centre entry in the menu) has a short article for every item of the menu, written in plain English: what it is, when to use it,
how to, good to know, related screens. There is also Getting started, a Glossary of accounting words and Shortcuts. Every screen has a ? button (a Help button at the bottom
right of the React screens) that opens the article of the screen you are on. The index shows the sections of the menu the person can see and has a search box.
| Blade (Livewire, Jetstream, Breeze) | React |
|---|---|
![]() |
![]() |
The articles are markdown files in resources/help/en/{topic}.md (a title: and a summary: front matter, then text; [[topic]] links to another article). To change or add one,
put a file of the same name in resources/views/vendor/accounting/help/en/; it wins over the package's.
Accounting and Settings (the two parts of the area)
The accounting area still has two parts, with tabs at the top of the All modules pages:
| Part | URL / route | What is in it |
|---|---|---|
| Accounting | /accounting · accounting.dashboard |
the daily work: journal entries, chart of accounts, recurring entries, reports (overview, ledger, trial balance, balance sheet, income statement, cash flow, statements, ageing), customers and suppliers, invoices/bills, receipts/payments, tax, banking, payroll, inventory, fixed assets, budgets, WhatsApp |
| Settings | /accounting/settings · accounting.settings |
Users & access (users with their roles, direct permissions and branches; roles and what each may do; permissions; features; account roles; branches; companies), Setup (currencies, periods, account types, cost centers, tax codes and rates, voucher types, control accounts, report mapping) and Audit |
Only the cards and links the signed-in person's permissions allow are shown, and a route without the permission answers 403. Roles are created and changed in Settings → Users & access → Roles
(React /accounting/roles, Blade /settings/roles): tick the permissions a role has; give a user extra permissions on the user's own screen; remove them the same way.
| Accounting | Settings |
|---|---|
![]() |
![]() |
Screens, design and dropdowns
All three front ends (React/Inertia, Blade with Livewire, the REST API) share the same screens; the Blade screens run inside the layout of your starter kit (Livewire/Flux, Jetstream, Breeze) and follow its light/dark mode.
| Livewire (Flux) | Livewire 4 |
|---|---|
![]() |
![]() |
| Jetstream | React |
![]() |
![]() |
- Every dropdown is searchable. Blade screens use Select2 (add
class="no-select2"to a<select>to keep it native); React screens usecomponents/accounting/dropdown.tsx, which takes the same<option>children andonChangeas a native<select>. An<option data-code="1101" data-name="Cash" data-tag="Asset">is drawn as number + name + type. - The journal entry form lists every account (number and name) as soon as the dropdown opens; type a number or part of a name to narrow it. Debit and credit are typed once, a line's balance button fills in what is missing, the bar at the bottom shows Balanced or the difference, Ctrl + S saves.
- Filters: the Filters button shows how many are active, the panel opens by itself when a filter is applied, and what is filtered is shown as chips with Clear all.
| Journal entries, filters open | Account dropdown with search |
|---|---|
![]() |
![]() |
- Payroll is five tabs on every payroll screen (Runs, People & pay, Attendance & leave, Loans & extras, Reports & tax); the choices inside a tab show once you are in it, and hidden entries follow the feature switches and permissions. Long filter lists keep the rarer fields under More filters, and a screen with many actions keeps the less common ones under More. Status words are coloured pills in every list.
| Payroll (Livewire / Flux) | Payroll, dark mode |
|---|---|
![]() |
![]() |
| Payroll, Jetstream | Payroll, React |
![]() |
![]() |
| General ledger | Journal entry, React |
![]() |
![]() |
The look lives in one file, resources/views/accounting/components/ui-styles.blade.php (CSS variables such as --acc-accent, scoped to .acc-kit); publish the views (php artisan vendor:publish --tag=accounting-views) and change the variables to match your brand.
Dashboard
/accounting/overview (and GET /api/v1/accounting/dashboard?as_of=&months=) shows what an accountant opens the system for:
cash and bank, receivables and payables with ageing, income vs expense by month, top expenses, recent entries and a list of things
needing attention, each linking to the page that deals with it. Sections are filtered by the user's permissions. The existing
/accounting page remains the module hub.
WhatsApp (Cloud API)
Send a WhatsApp message from anywhere in your application with one call, and see on /accounting/whatsapp (and /api/v1/accounting/whatsapp)
what was sent and how far each message got: sent → delivered → read, or failed with the reason in plain words. Everything you set is in .env.
use Alimarchal\LaravelChartOfAccounts\Support\WhatsApp; // A school fee has been received: tell the parent (an approved template, which is how a business starts a conversation) WhatsApp::template('0300-1234567', 'fee_received', [ 'student' => 'Ali Raza', 'amount' => 'Rs 5,000', 'receipt' => 'RV-2026-00012', 'date' => '10 Oct 2026', ], related: $payment); WhatsApp::text('+92 300 1234567', 'Your receipt is ready.'); // free text: only within 24 hours of the person's last message WhatsApp::queueTemplate(...); // the same, sent by a queue worker
template() and text() return the saved WhatsAppMessage (status sent: Meta accepted it) and throw an AccountingException with a
message you can show when WhatsApp refuses (a wrong number, the 24-hour window, parameters that do not match the template). Numbers may be written
as people write them (+92 300 1234567, 0300-1234567, 0092 300 …); they are turned into digits with the country code.
The school-fee example, end to end
- Add the parent as a customer with a phone number (
/accounting/parties), and the fee template to Meta (below). - In
.env:ACCOUNTING_WHATSAPP_ENABLED=trueandWHATSAPP_NOTIFY_RECEIPT=fee_received. - Record the fee as a receipt from that customer (
/accounting/party-payments, orPOST /api/v1/accounting/party-payments). The moment it is saved the parent gets "Dear parent, we received Rs 5,000 for Ali Raza. Receipt RCT-2026-00002 dated 10 Oct 2026. Thank you." The template parameters are filled from the receipt (student/party/name= the customer,amount,receipt,date,reference,method; the order is the one inaccounting.whatsapp.templates.fee_received.parameters). - A problem with WhatsApp never stops the receipt: it is saved, and the message shows as failed on the WhatsApp screen with a Retry button.
To send from your own code (for example a fee voucher in your school app), call WhatsApp::template() as shown above.
Set-up in .env
| Key | What it is |
|---|---|
ACCOUNTING_WHATSAPP_ENABLED |
true switches the module on (default false) |
ACCOUNTING_WHATSAPP_MODE |
fake (default, the sandbox: nothing leaves your server) or live (Meta's Graph API) |
WHATSAPP_TOKEN |
Access token of a system user (a permanent token) with whatsapp_business_messaging |
WHATSAPP_PHONE_NUMBER_ID |
The ID of the sending number (WhatsApp Manager → API Setup); not the phone number itself |
WHATSAPP_BUSINESS_ACCOUNT_ID |
WhatsApp Business Account ID (optional: lists the approved templates) |
WHATSAPP_VERIFY_TOKEN |
A word you choose; you type the same word into Meta's webhook settings |
WHATSAPP_APP_SECRET |
Meta app → Settings → Basic; proves a webhook call really comes from Meta |
WHATSAPP_API_VERSION |
Graph API version, default v25.0 (a version lives about two years: check Meta's changelog) |
WHATSAPP_DEFAULT_COUNTRY_CODE |
92: 0300 1234567 becomes 92 300 1234567 (empty = always require +country code) |
WHATSAPP_DEFAULT_LANGUAGE |
Language code of your templates (en, en_US, ur …) |
WHATSAPP_USE_QUEUE / WHATSAPP_QUEUE |
Send the automatic messages through the queue |
WHATSAPP_NOTIFY_RECEIPT |
Template key sent to the customer when a receipt is recorded (empty = none) |
WHATSAPP_TIMEOUT / WHATSAPP_RETRIES / WHATSAPP_BASE_URL |
Timeout (15 s), quick retries of a dropped connection or a 5xx answer (2), base URL (https:// only) |
Your templates are listed in config/accounting.php → whatsapp.templates (key, name as in WhatsApp Manager, language, and the order of the parameters
{{1}}, {{2}} …; set 'format' => 'named' for templates with named parameters). A template you did not list is sent under its own name with the parameters you pass.
php artisan accounting:whatsapp status shows what is set and what is missing; accounting:whatsapp test +923001234567 sends Meta's hello_world
(--template=fee_received --param=Ali --param=…, or --text="…"); accounting:whatsapp templates lists the templates Meta has approved (live mode).
Try it first: the sandbox (mode=fake)
With ACCOUNTING_WHATSAPP_MODE=fake nothing is sent and no Meta account is needed, but the rules of the real thing apply: a template is accepted,
free text to someone who has not written to you in the last 24 hours is refused (error 131047), and parameters are checked. On the WhatsApp screen use
Mark delivered / Mark read (what Meta's webhook does later) and Customer replies (which opens the 24-hour window), or the API:
POST /whatsapp/messages/{id}/simulate {"status": "delivered|read|failed"}, POST /whatsapp/simulate-reply {"from": "0300…"}. These answer 404 in live mode.
Going live, step by step
- Meta: create a developer account and an app of type Business, add the WhatsApp product. Meta gives a free test number and the
hello_worldtemplate; a test number only sends to the recipients you add in WhatsApp Manager → API Setup (otherwise error 131030). - Token: create a system user in Business settings, give it the app and
whatsapp_business_messaging, generate a token that does not expire. Put it, the Phone number ID, the WhatsApp Business Account ID and the app secret into.envand runphp artisan accounting:whatsapp status. - Test:
php artisan accounting:whatsapp test +92XXXXXXXXXX(setACCOUNTING_WHATSAPP_MODE=livefirst). The message should reach the phone. - Webhook: in WhatsApp → Configuration enter the callback URL shown on the WhatsApp screen
(
https://your-domain/api/v1/accounting/whatsapp/webhook, it must be public https) and yourWHATSAPP_VERIFY_TOKEN, then subscribe to messages. Without the webhook messages stay sent and never turn delivered or read. - Templates: a business starts a conversation only with an approved template (WhatsApp Manager → Message templates, category Utility for
receipts; approval usually takes minutes to a day). Create
fee_receivedwith four body parameters in the order listed in the config, using the same name and language. - Go live: a real business number needs a verified business and a payment method; then set
WHATSAPP_NOTIFY_RECEIPT=fee_received.
What the integration does for you
- Request (
POST {base}/{version}/{phone-number-id}/messages,Authorization: Bearer, JSONmessaging_product: whatsapp,to,type: template|text).httpsonly; the token is never sent over plain http. - Parameters are cleaned the way WhatsApp requires (no line breaks, tabs or runs of spaces, never empty, at most 1024 characters) and checked against the template (count, names) before anything is sent.
- Errors are kept on the record with Meta's code and a sentence you can act on (131047 the 24-hour window, 131026 not on WhatsApp, 131030 not on the test list, 132000/132001 template mismatch, 132018 bad parameter, 190 token expired, rate limits …). Only a dropped connection, HTTP 5xx and "too many requests" are retried (quickly in the request, and with 30 s / 2 min / 10 min pauses on the queue); a 4xx never is.
- Webhook (
GETverification,POSTevents, no login): verified by the verify token and by theX-Hub-Signature-256HMAC of the raw body with your app secret (in live mode a call without a valid signature is refused). A status never moves backwards, a reply from a customer is stored once however often Meta repeats it, and events of another phone number of the same app are ignored. - Scope: every message belongs to the company that sent it and is listed with its filters (
search,status,direction,template,number,date_from,date_to). - Permissions
whatsapp.viewandwhatsapp.send; the feature switchwhatsapp. To use another provider, bind your ownAlimarchal\LaravelChartOfAccounts\Contracts\WhatsAppGateway. - Check before going live: this was written from Meta's published WhatsApp Business Platform (Cloud API) documentation as understood when written (Meta's own pages could not
be read from the build environment, so details come from secondary summaries of them); the package has not been exercised against Meta's servers (no credentials were
available), only against the request and response shapes. Meta changes versions, fees and template rules: check the current documentation and changelog at
developers.facebook.com/docs/whatsapp and run the
testcommand before relying on it.
Payroll
/accounting/payroll (and /api/v1/accounting/payroll) runs the monthly salaries:
- Employees with a monthly basic salary, join and leave dates, cost center, bank details and a "withhold income tax" flag; allowances and deductions (a fixed amount or a percent of basic, per employee overridable), where earnings are booked to an expense account and deductions to the liability they are owed to.
- A payroll run for a month works out every payslip: new joiners and leavers are paid for the days employed, percent
components follow the prorated basic, and flagged employees have income tax withheld from the slabs in
accounting.payroll.tax_slabs(annualised taxable pay; the defaults are illustrative Pakistani salaried-individual slabs, so check them against the current rules). A negative net salary is refused (the message lists every overdrawn employee at once). How a day's pay is worked out (joiners, leavers, unpaid absence, leave encashment) followsACCOUNTING_PAYROLL_DAY_BASIS:calendar(the real 28–31 days, the default),fixed(gratuity.days_per_monthevery month) orworking(the month's days less the weekly off days inACCOUNTING_PAYROLL_WEEKLY_OFF,0= Sunday …6= Saturday). One run per month; a draft can be recalculated or deleted. - Post books one entry in the payroll module: basic pay and earnings as expense (by account and cost center), deductions and
tax as liabilities, and the net pay owed to employees (
ACCOUNTING_PAYROLL_NET_PAYABLE, default 2103). Pay clears that liability against a bank or cash account. Void reverses the payment and the salary entry; the month can then be run again. - Quantity x rate components: an allowance can be a quantity at a rate (fuel: 75 litres x 280). The rate lives on the component, the quantity per employee (or default), so changing the price once changes everybody on the next calculation.
- Salary grades (
/accounting/payroll/grades): a basic salary with its allowances and deductions. Employees on a grade follow it: change an allowance of the grade and everybody on it is paid the new amount next time; an employee's own components and values override the grade's. Leaving the salary blank on an employee takes the grade's. - Bulk changes (
/accounting/payroll/bulk): give or take away a component for many employees, move a group to a grade (optionally moving their salary), or raise salaries (by percent, by an amount, or set to an amount, with rounding) with a preview before it is applied. Who is meant: the employees picked, else everybody on a grade, else every active employee. - Salary history: every change of an employee's salary is a revision with the date it applies from and a reason. A revision in the middle of a month pays the old salary up to the day before and the new one from that day.
- Arrears (
/accounting/payroll/arrears): when a raise is applied late (say it is October and the raise was from January), the difference between what the posted payslips paid and what the salary history says was due is worked out month by month (allowances that are a percent of the basic follow). Preview, create (drafts), approve (the approver,payroll.post), and the payroll run of the payment month pays them as an Arrears line, booked toaccounting.payroll.arrears_account(default: the salary expense account). Income tax on arrears is worked out as if they had been paid in the months they belong to, so one big month does not push the employee into a higher slab; it appears as its own "Income tax on arrears" line. Months already claimed are never claimed twice; a month in which an employee was overpaid is not recovered; a voided or deleted run puts its arrears back in the approved queue. Arrears of an employee who has already left are not paid by a run (they are not in it). A register shows every arrears record with totals by status and by month. - Attendance and leave: a monthly attendance sheet (
/accounting/payroll/attendance) with absent days and overtime hours per employee, and leave types (paid or unpaid, with a yearly entitlement), leaves and yearly balances (/accounting/payroll/leaves). Absent days and unpaid leave come off the basic and the fixed allowances by the day (percent allowances follow the basic); half days are allowed; leave that overlaps other leave, or exceeds a paid type's balance, is refused. A month with a posted payroll cannot be changed. - Overtime: employees flagged "paid overtime" get hours x (salary /
accounting.payroll.overtime.hours_per_month) x the multiplier (a separate one for holidays), as a taxable Overtime line on the payslip. - Loans and advances (
/accounting/payroll/loans): record the amount, the number of instalments and the first salary it comes out of (equal instalments, the last takes the rounding), then pay it out (the approver: debitaccounting.payroll.employee_loans_account, default 1105, credit the bank). Each payroll run takes the due instalments as a deduction ("Loan instalment 2/12"; the credit goes to the loan account), a voided, deleted or recalculated run gives them back, skip moves the next instalment to the end, settle takes the rest in cash, and a loan closes when it has been recovered. - Contributions (EOBI, PESSI/SESSI, provident fund;
/accounting/payroll/schemes): a rate of the basic or the gross up to a ceiling, or a fixed amount; the employee's share is a deduction owed to a liability account, the employer's share is an expense owed to another liability account and is not part of the net pay (it is booked with the run and shown on the payslip). A scheme applies to everybody or to the employees it is given to, and can also be taken on arrears. The rates are yours to set: check them against the current rules. - Bank salary file: from a posted run, CSV or Excel in a layout of
accounting.payroll.bank_file.layouts(standard, IBAN, or your bank's); employees without a bank account are left out and listed on the screen. Needspayroll.post. - Payslip files (print, PDF, e-mail): every payslip opens as a printable page and a PDF, and goes by e-mail to the employee's address
(one, or the whole posted run:
accounting.payroll.payslip_mail). Employees without an address are listed as skipped. - Automatic run:
php artisan accounting:payroll-run [--month=2026-10] [--company=1]creates the draft run (default: last month; skips a month that already has one). Turn onaccounting.payroll.auto_runto have the scheduler do it every month. - Final settlement (
/accounting/payroll/settlements): when an employee leaves, preview and save the gratuity (days per year x basic/30 x years served, after a minimum service), pay for unused leave of one leave type, a signed adjustment and the loans still owed; post it (the approver), pay it, or void it (the loans open again). Rules inaccounting.payroll.gratuity. - Tax (
/accounting/payroll/tax): the salary tax statement of a tax year for every employee (CSV, Excel, PDF) and a printable certificate per employee, from the posted payroll. The tax year starts inaccounting.payroll.tax_year_start_month(7 = July). - Payroll reports (
/accounting/payroll/reports): month by month with the change, cost by cost center, and headcount; CSV, Excel, PDF. - Run approval: with
accounting.payroll.require_approval(or the Run approval switch on the Features screen) HR (payroll.run) submits a draft run, which is locked, and finance (payroll.approve, never the person who submitted it) approves it or sends it back with a reason; only an approved run can be posted. Submitted, approved and returned runs are audited. - Bonuses and one-off pay (
/accounting/payroll/adjustments): an earning (bonus, extra allowance) or a deduction (fine) for an employee and month, or for every active employee at once as a fixed amount or a percent of each basic salary; taxable or not, with or without a pay component (a plain bonus goes toaccounting.payroll.bonus_account, default the salary expense account). The month's run takes the open ones up; a recalculated, deleted or voided run gives them back; a month whose run is already submitted or posted is closed to new ones. - Printable payslips. Permissions
payroll.view/manage/run/post/void: the accountant prepares (manage,run), the approver posts, pays and voids, so one person cannot do both. Audited (PAYROLL_RUN_*,EMPLOYEE_SAVED).
Feature switches
Everything beyond the core (chart of accounts, journal entries, periods, reports, users and roles) is optional. An administrator
(accounting.manage-settings) turns modules and payroll features off and on from Features (/accounting/features; /settings/features with
Blade), the API (GET /api/v1/accounting/features, PUT with {"features": {"inventory": false}}) or the command line:
php artisan accounting:features # the list, with state and source php artisan accounting:features disable inventory fixed_assets php artisan accounting:features enable payroll_loans php artisan accounting:features reset budgets # back to the config default
A feature that is off disappears from the menus and its web and API routes answer 404; nothing is deleted, and turning it on again brings it
back. A sub-feature (payroll_loans) is off whenever its parent (payroll) is off, the payroll engine skips what is off (loans, attendance,
contributions, arrears, bonuses), and the schedulers of recurring entries and payroll do nothing while their module is off. The defaults come from
accounting.features.disabled (ACCOUNTING_FEATURES_DISABLED="inventory,fixed_assets"); what is saved from the screen, API or command wins. Every
change is in the audit trail (FEATURE_ENABLED, FEATURE_DISABLED). Keys: parties, tax, banking, budgets, fixed_assets, inventory,
recurring_entries, fx_revaluation, whatsapp, attachments, report_mapping, chart_templates, control_accounts, snapshots, audit_log,
payroll, payroll_grades, payroll_arrears, payroll_attendance, payroll_loans, payroll_contributions, payroll_bank_file,
payroll_payslip_mail, payroll_adjustments, payroll_approval, payroll_settlements, payroll_reports. Multi-company and maker-checker
approvals of journal entries keep their own switches (accounting.multi_company.enabled, accounting.approvals.enabled).
Filtering and searching lists
Every list, in the React screens, the Blade screens and the REST API, filters the same way (Spatie Laravel Query Builder underneath). On a screen the filter bar sits above the table; the same names work in the address and in an API call:
GET /api/v1/accounting/party-documents?filter[search]=rent&filter[status]=posted&filter[date_from]=2026-01-01&sort=-total
GET /api/v1/accounting/party-documents?search=rent&status=posted&date_from=2026-01-01&sort=-total # the short form is the same
filter[name]=valueis the standard form; the short?name=valueform is read too, andfilter[...]wins when both are sent. A link to a filtered screen and an API call therefore look alike.- Several values, separated by commas, match any of them (
filter[status]=draft,posted). - Text (
search,name,email…) matches part of the text, ignoring case;%and_in what you type match themselves, so searching100%finds "100% Organic" and nothing else. - Dates are
YYYY-MM-DD(date_from/date_toor a name that says what it is, e.g.due_from,joined_to), amountsmin_total/max_total(or_amount,_cost,_salary,_net), yes/no filters take1,0,trueorfalse. ?sort=nameand?sort=-name(descending) sort where a list offers it; the allowed columns are in the API reference. Each list has its own default order (newest first for documents and payments, by code for accounts).- Strict where it matters: an unknown filter or sort answers
400, a malformed date or number422, so a typo never silently returns everything. - Filters combine with AND; a result that is capped (for example the 300 newest documents) is capped after filtering, so the record you are looking for is found by its status or date, not only when it is recent.
- Reports keep their filters when exported: the CSV / Excel / PDF of the trial balance, balance sheet, income statement and the aged
reports contain exactly the date and filters on the screen (the Blade screens pick a period instead of a date; its last day is used).
The trial balance takes
as_of_date,date_from(the movement of a period),search,account_type,report_groupandhide_zero.
| List | Filters (besides search) |
|---|---|
| Customers and suppliers | type, email, tax_number, active |
| Invoices and bills | kind, status, party_id, date_from/date_to (issued), due_from/due_to, min_total/max_total |
| Receipts and payments | kind, status, party_id, method, date_from/date_to, min_amount/max_amount |
| Fixed assets | category, method, status, as_of, acquired_from/acquired_to, min_cost/max_cost |
| Inventory, movements, warehouses | category, unit, low_stock, warehouse_id, as_of; item_id, type, dates; active |
| Budgets, bank statements | status, dates; bank_account_id, unmatched, dates |
| Recurring entries | frequency, mode, active, due_by |
| Payroll: runs, employees | status, period_from/period_to; active, withhold_tax, cost_center_id, joined_from/joined_to, min_salary/max_salary |
| Payroll: loans, arrears, adjustments, settlements | employee_id and a search by the employee's code or name, status, kind, dates, amounts |
| Payroll: components, grades, schemes | kind, method, taxable, base, applies_to_all, active, min_salary/max_salary |
| Users, roles, voucher types, companies, control accounts | role, name, email; guard_name; active, system, reset; is_active; control_type |
| Tax returns, exchange rates, revaluations, periods | return_from/return_to, min_payable; currency_id, source, rate_from/rate_to; status, kind, dates; status, dates |
| Journal entries, chart of accounts, audit log, small master data | reference, status, entry_date_from/entry_date_to, … (see the OpenAPI document) |
Inventory
/accounting/inventory (and /api/v1/accounting/inventory) keeps stock and its value at moving average cost: items with the
inventory and cost-of-goods-sold accounts they post to, warehouses, and a stock ledger of movements.
- Receive (quantity, unit cost, the account that paid or owes): debits inventory. Issue: leaves at the current average and debits cost of goods sold (or the account you choose); the last unit out takes whatever value is left, so no cent is lost. Adjust a count difference: gains come in at the average (or a cost you give), losses go to a gain/loss account. Transfer between warehouses moves quantity only.
- Each movement and its journal entry are made in one transaction (origin
inventory, so it may post to inventory control accounts); a closed period stops the stock change too. Stock never goes negative, in total or in a warehouse. - The valuation report shows quantity, average cost and value per item and warehouse at any date, flags items at or below their reorder level, checks the stock ledger against the inventory accounts, and exports to CSV, Excel and PDF. Each item has a stock card with the running balance.
- An item's accounts are locked once it has movements; items and warehouses with movements cannot be deleted, only deactivated.
Permissions
inventory.view/manage/move; audited (INVENTORY_ITEM_*,STOCK_*).
Fixed assets
/accounting/fixed-assets (and /api/v1/accounting/fixed-assets) is the asset register: code, cost, salvage value, useful life,
straight-line or declining-balance depreciation, and the three accounts an asset posts to (cost, accumulated depreciation,
depreciation expense). Giving an "offset" account when registering books the purchase (debit the asset account, credit cash,
bank or the supplier).
- Depreciation run (
/fixed-assets/depreciation): previews and books everything due up to a month end. One journal entry per month, dated on the month's last day, one line per expense/accumulated account. Depreciation starts in the month the asset enters service, never takes an asset below its salvage value, keeps every cent (the last month takes the remainder), and a month is booked once per asset, so running it again does nothing. Months missed are caught up in order. - Disposal: sell or scrap an asset. Depreciation is brought up to the month before the disposal date, cost and accumulated depreciation are removed, the proceeds are received in the account you choose, and the gain or loss goes to the account you choose. Disposed assets stay in the register history.
- Cost, life, method, dates and accounts are locked once an asset has an acquisition or depreciation entry; the register shows the difference between the ledger and the register (should be zero) and exports to CSV, Excel and PDF.
- Entries use the
fixed_assetsorigin, so they may post to accounts marked as fixed-asset control accounts. Permissionsfixed-assets.view/create/update/delete/depreciate/dispose; audited (FIXED_ASSET_*,ASSET_DEPRECIATION_POSTED).
Budgets
Dashboard → Budgets (React and Blade) or /api/v1/accounting/budgets. A budget plans income and expenses by
account (optionally cost center) and month, in the base currency and the account's natural direction (income
earned, expense spent), and is then followed against the ledger.
- Plan: a first and last month (up to 24 months) and one line per account — an annual figure that is spread evenly (the odd cents land in the first months) or amounts by month. Build from last year: add every account that had income or expenses in the same months a year earlier, raised or lowered by a percentage. Copy a budget to a later year with a percentage change.
- Workflow: draft (editable) → approved (needs
budgets.approve— the accountant plans, the approver approves) → closed; reopen to revise. Only one approved budget may cover a month. - Budget against actual: for the whole budget or any range of months, and for one cost center. Actuals are posted
entries in base currency (closing entries excluded). Per account: budget, actual, variance (favourable when
positive: income above plan, expenses below it), share used and a status — ok, warning (an expense at
ACCOUNTING_BUDGET_WARN_PERCENT, 90%, of its budget), over, behind (income below plan), unbudgeted (spending with no budget line); click a row for the months. Totals for income, expenses and net. Export to CSV, XLSX and PDF. - Optional control:
ACCOUNTING_BUDGET_CONTROL=blockrefuses to post an entry that takes a budgeted expense account past its cumulative budget (the sum of its monthly budgets up to the entry's month) in the approved budget covering the date. Unbudgeted accounts, income, drafts and system entries are never blocked; users withbudgets.overridemay post anyway. The default isoff.
Permissions budgets.view (accountant, approver, auditor, viewer), budgets.create / update / delete (accountant),
budgets.approve (approver) and budgets.override (administrators). Audited (BUDGET_*).
Bank statements
Dashboard → Bank Statements (React and Blade) or /api/v1/accounting/bank-statements. Import the statement your bank
gives you, match it to the ledger and reconcile — no retyping.
- Import a CSV or Excel file for a bank account (one linked to a chart of accounts account). Columns are recognised
by name: Date (Transaction / Value / Posting Date), Description (Narration, Particulars, Details …), Reference
(Cheque No, Ref No …), Withdrawal / Debit and Deposit / Credit — or one signed Amount — and Balance. Dates
as
2026-10-05,05/10/2026(ACCOUNTING_BANK_DATE_ORDER=mdyfor US order),5-Oct-2026or Excel serials; amounts as1,234.50,(120.00),-45,500.00 DR,1.234,56. A preview lists every row first; a file with an error imports nothing. - No duplicates: each transaction is fingerprinted (account, date, amount, reference, description and its n-th occurrence in the file), so importing an overlapping statement again brings in only what is new, while two identical charges on one day both come in. The fingerprint is unique in the database.
- Matching: a transaction matches a posted, unreconciled ledger line on the bank's account with the same amount, dated
within
ACCOUNTING_BANK_MATCH_DAYS(5) days. Auto-match takes every transaction with exactly one candidate (and whose candidate nobody else wants); the rest you match by hand from the list of candidates. A ledger line serves one transaction. Matched ledger lines are marked cleared. - Book it: for a transaction not in the books yet (bank charges, interest, a direct deposit) pick the other account: a deposit debits the bank and credits it, a withdrawal the reverse; the entry is posted when possible, otherwise left as a draft (closed period, approval, evidence required) and still linked. Or ignore it.
- Reconcile once every transaction is matched, booked or ignored (and the closing balance — read from the file's last balance, or typed — is known): the matched ledger lines become reconciled in a bank reconciliation dated at the statement's end. A reconciled statement is locked; deleting an unreconciled one releases its matches.
Permissions bank-statements.view (accountant, approver, auditor, viewer), bank-statements.import and
bank-statements.match (accountant). Audited (BANK_STATEMENT_*).
Currency revaluation
Dashboard → Currency Revaluation (React and Blade) or /api/v1/accounting/fx-revaluation. An asset or liability
account whose currency is not the base currency (a USD bank account, a EUR payable, …) holds a foreign balance
(what its lines say in that currency) and a carrying value in the base currency (what the ledger booked at the
rates of the day). At period end the carrying value is restated to foreign balance × closing rate; the difference
is an unrealised exchange gain or loss.
- Closing rates: type them in, or keep a dated rate history (
/fx-revaluation/rates): the latest rate on or before the date is used, else the currency's own rate. Calculate first — the preview lists every account with its foreign balance, rate, carrying value, revalued amount and adjustment; nothing is posted until you confirm. - One adjusting entry in the base currency (voucher type JV, origin
fx-revaluation): each account is debited or credited by its adjustment and the net goes to the gain/loss account you pick (any active income or expense account;ACCOUNTING_FX_GAIN_LOSS_ACCOUNTpre-selects one by code). Only posted entries up to the date count, and only lines in the account's own currency make up its foreign balance, so earlier adjustments never distort it. - Safe to repeat: the carrying value already includes earlier adjustments, so running again at the same rates adjusts nothing ("Nothing to revalue"); a new rate adjusts only the difference.
- Auto-reverse (optional): a reversal dated after the revaluation (default: the next day, needs an open period) puts
the books back on the original rates for the next period — the usual month-end routine. A revaluation can also be
reversed later from its page. Each run keeps its per-account detail and is audited (
FX_REVALUATION_*).
Permissions fx-revaluation.view (accountant, approver, auditor, viewer), fx-revaluation.run and
fx-revaluation.rates (accountant).
The chart of accounts on a fresh install (ERPNext)
A new installation starts with the ERPNext standard chart of accounts with account numbers, so an accountant who knows ERPNext finds every account where it expects it:
| Number | Account | Number | Account |
|---|---|---|---|
1000 |
Application of Funds (Assets) | 4000 |
Income |
1100 / 1110 |
Cash In Hand / Cash | 4110 |
Sales |
1200 / 1210 |
Bank Accounts / Primary Bank Account | 4120 |
Service |
1300 / 1310 |
Accounts Receivable / Debtors | 5000 |
Expenses |
1400 / 1410 |
Stock Assets / Stock In Hand | 5111 |
Cost of Goods Sold |
1700 |
Fixed Assets (1710–1790, 1780 accumulated depreciation) |
5213 |
Salary |
2000 |
Source of Funds (Liabilities) | 5212 |
Round Off |
2100 / 2110 |
Accounts Payable / Creditors | 5219 |
Exchange Gain / Loss |
2300 |
Duties and Taxes (2310 TDS Payable, 2320 Output Sales Tax Payable) |
3000 / 3400 |
Equity / Retained Earnings |
Choose another chart when you install (or later for a new company):
php artisan accounting:install --chart=erpnext # the default: ERPNext's standard chart php artisan accounting:install --chart=general # the package's own business chart (1101 Cash In Hand, 1103 Accounts Receivable …) php artisan accounting:install --chart=school # the school chart php artisan accounting:seed --chart=erpnext --company=BRANCH2 # seed one company php artisan accounting:create-company BRANCH2 "Branch 2" --template=erpnext # a new company; without --template it uses ACCOUNTING_CHART_PRESET
ACCOUNTING_CHART_PRESET sets the default (erpnext, general or school). Run interactively, the installer asks which chart to use
(ERPNext is the default answer). The chart is chosen per company and kept in the company's settings; the seeder only creates missing
accounts and never renames or removes one.
- Everything is wired up — cash book, bank book, ageing, control accounts, year-end close, rounding, exchange gain/loss, payroll (salary expense, income tax, net salary payable, loans) all find their accounts through account roles, and the financial statements are mapped to every account.
- Mixing is refused — adding an ERPNext account to a company that was seeded with the general chart (or the other way round) is not
allowed, because the two charts use the same numbers for different things. Industry templates (
accounting:chart-templates) fit both: the template's accounts are numbered after the chart you have. - Existing installations are unchanged — a company that already has a chart keeps behaving as the general chart did (the same account
codes), and
accounting:updateadds the roles table without touching your accounts. To switch such a company to roles explicitly, runphp artisan accounting:account-roles seed generalonce, then change roles as you like. - Licence — the chart follows ERPNext's published structure and names (ERPNext is GPL-3.0); see
NOTICE.md. Install with--chart=generalif you prefer the package's own chart.
Account roles
A module never asks for "account 1101". It asks for a role: "which account is the cash book?", "which accounts are the receivables in the
ageing report?". Each company has its own answers, filled in from the chart when it was seeded and changeable from Account roles
(/accounting/account-roles, /settings/account-roles with Blade; needs accounting.manage-settings), the API or the command line.
| Role | Used for |
|---|---|
cash, bank |
Cash book, bank book, close checklist (bank reconciled) |
retained_earnings, rounding, fx_gain_loss |
Year-end close, rounding differences, currency revaluation |
opening_balance_equity |
Carries the difference of the opening balances (see below) |
aging.receivables, aging.payables |
The ageing reports and the party statements |
control.receivables, .payables, .inventory, .payroll, .tax |
"Recommended setup" of the control accounts |
salary_expense, net_salary_payable, income_tax_payable, employee_advances |
Payroll posting, loans, settlements |
salary_arrears, bonus_expense, gratuity_expense, gratuity_payable |
Optional: arrears, bonuses and settlements default to salary expense / net salary payable |
php artisan accounting:account-roles # who plays what, and what the config pins php artisan accounting:account-roles set cash 1110 php artisan accounting:account-roles set aging.receivables 1310 1311 php artisan accounting:account-roles seed general --overwrite # fill the roles of a chart again
GET /api/v1/accounting/account-roles PUT /api/v1/accounting/account-roles {"roles": {"cash": [12], "aging.receivables": [14, 15]}}
- The order of resolution is: the config value (
accounting.defaults.cash_account_code,accounting.aging.*,accounting.payroll.*_account,accounting.fx.gain_loss_account— a config value pins the role and the screen shows it as fixed), then the company's saved roles, then the default of the company's chart. - A role takes active posting accounts of the company; a role that takes one account refuses two. An empty list puts the role back to the
chart's default. Every change is in the audit trail (
ACCOUNT_ROLES_UPDATED). - They follow the money: merging an account into another moves its roles to the account that keeps the balance; an account that plays a role cannot be deleted or deactivated until the role is given to another account; and cash, bank, retained earnings and rounding cannot be renumbered, merged away or deactivated while they play their role.
Opening balances
A company that starts using the books brings its balances over with one entry on the go-live date:
POST /api/v1/accounting/opening-balances {"date": "2026-07-01", "balances": [ {"chart_of_account_id": 12, "debit": "5000.00"}, {"chart_of_account_id": 31, "credit": "1500.00"} ]} GET /api/v1/accounting/opening-balances # the posted entry, or null
- One posted entry (reference
OPENING-BALANCE, voucher number from the normal sequence). Each balance is either a debit or a credit; whatever the two sides do not balance by is booked to theopening_balance_equityrole (account3102, or3300in the ERPNext chart), so the entry always balances. - Needs
journal-entries.post. The normal posting rules apply: open period, active posting accounts, and control accounts (receivables, payables, stock) are opened with their own opening invoices, bills and stock rather than with a plain balance. - Only one opening entry can stand at a time (422 otherwise); reverse it to post the balances again.
- API only for now: there is no screen for it.
The posted entry as it appears on the journal entry page:
Industry chart templates
Chart of Accounts → Templates (React and Blade), GET /chart-templates, or php artisan accounting:chart-templates.
A template is a base chart plus the accounts an industry adds:
| Template | Adds (examples) |
|---|---|
erpnext |
The ERPNext standard chart with account numbers (the default for a new installation) |
general |
The package's own business chart |
trading |
Merchandise and goods in transit, sales returns / discounts (contra income), purchases, purchase returns, freight inward, customs duty, sales commission |
manufacturing |
Work in progress, packing materials, spare parts, factory building, tools and dies, a Manufacturing Overheads group (rent, utilities, depreciation, indirect labour, wastage) and production variances |
services |
Unbilled revenue, retention payable, consulting / retainer / project income, subcontractor costs |
school |
The school chart (student fee receivable, fee incomes, books and uniform stock) plus hostel, annual charges and examination accounts |
ngo |
Restricted / unrestricted / endowment funds, grants and pledges receivable, donations, grant income, programme and fundraising costs |
healthcare |
Patient and insurance receivables, medical and pharmacy stock, consultation / laboratory / pharmacy / procedure income, doctors' fees |
- Preview first: every account is listed as new, already there, named differently (your name is kept) or blocked (missing parent).
- Safe on a chart in use: only missing accounts are added, parents first; nothing existing is renamed, moved or re-typed, so a template can be applied again later or on top of another one. Contra accounts get the opposite side, and every new account is mapped to its statement line (see Report mapping) so the statements stay complete.
- New companies can start from one:
php artisan accounting:create-company SUB "Clinic" --template=healthcare. - Permission
chart-templates.apply(super-admin); audited asCHART_TEMPLATE_APPLIED.
// config/accounting.php — your own template 'chart_templates' => [ 'bakery' => ['name' => 'Bakery', 'description' => 'Bread and cakes', 'base' => 'general', 'extras' => [['5121', '5100', 'EXPENSE', 'Flour and Ingredients', false, ['line' => 'IS-COST-OF-SALES']]]], ],
php artisan accounting:chart-templates # list php artisan accounting:chart-templates ngo --dry-run # what it would add php artisan accounting:chart-templates ngo --company=SUB # add it to a company
Importing and exporting the chart
Bring an existing chart of accounts in from Excel (.xlsx) or CSV — Chart of Accounts → Import
(React and Blade), or POST /api/v1/accounting/chart-of-accounts/import.
- Download the template (or export the current chart — the export is in the same layout).
- Fill in or edit the rows and upload the file.
- The preview lists every line as new, updated (old → new for each field), unchanged or error, with the reason. Nothing is saved yet.
- Import applies the whole file in one transaction. If any row is in error, nothing is imported.
| Column | Required | Notes |
|---|---|---|
account_code |
always | identifies the account; an existing code is updated (or left alone with "Leave them unchanged") |
account_name |
new accounts | |
parent_code |
no | an existing account or another row — parents may come after their children in the file |
account_type |
top-level new accounts | type code or name (ASSET, Expense …); children default to their parent's type |
normal_balance |
no | debit / credit; defaults to the type's |
currency |
no | currency code; defaults to the parent's, then the base currency |
is_group, is_active |
no | yes / no / 1 / 0 / true / false |
control_type |
no | receivables, payables … (needs control-accounts.manage) |
description |
no |
Blank cells keep an existing account's value. Common header names are understood (Code, Name, Parent,
Type …), CSVs may use commas, semicolons or tabs, and Excel / Windows-1252 files are read as UTF-8. Every row
passes the same rules as the screens and the API and the database guards — a used account cannot change
type, a posting account cannot get children, loops are refused. The import is audited (CHART_IMPORTED, with
the codes created and updated), and needs the chart-of-accounts.import permission (super-admin by default).
Limits: ACCOUNTING_CHART_IMPORT_MAX_KB (5 MB) and ACCOUNTING_CHART_IMPORT_MAX_ROWS (5,000).
# Preview over the API, then import curl -H "Authorization: Bearer $TOKEN" -F file=@chart.xlsx -F dry_run=1 $APP/api/v1/accounting/chart-of-accounts/import curl -H "Authorization: Bearer $TOKEN" -F file=@chart.xlsx $APP/api/v1/accounting/chart-of-accounts/import
Renumbering and merging accounts
Chart of Accounts → the ⇄ icon on an account (React and Blade), or the API. Needs chart-of-accounts.restructure
(super-admin by default). Both show a preview first and are audited.
Renumber — give an account a new code, even if it has journal entries (lines point to the account, not its
code, so every report follows). A group can take its sub-accounts along: codes sharing the group's prefix
(the code without trailing zeros) get the new prefix — 5100 → 6100 turns 5101 into 6101 and 5110 into
6110. To take its sub-accounts along, the new code keeps the group's length and trailing zeros (7000 → 8000,
not 7000 → 7700); otherwise renumber the account alone. Codes already in use are refused, and so are accounts named in config('accounting.defaults') (change
the configured code first).
Merge — for duplicates (Stationery and Stationary). Posted entries are immutable, so nothing is rewritten:
- The source's balance moves to the target with a posted transfer entry, one pair of lines per cost center, through the normal rules (open period, maker-checker approval, control accounts). If the transfer needs approval, the merge is refused: post the transfer through approvals first, then merge (the balance is zero).
- Draft lines, sub-accounts and bank accounts that used the source now use the target.
- The source is deactivated with
metadata.merged_into; its history stays in every report.
Only accounts of the same type, normal balance, currency and control type merge; entries waiting for approval on the source must be decided first; configured and system accounts cannot be merged away.
curl -X POST -H "Authorization: Bearer $TOKEN" -d account_code=6100 $APP/api/v1/accounting/chart-of-accounts/59/renumber curl -H "Authorization: Bearer $TOKEN" "$APP/api/v1/accounting/chart-of-accounts/120/merge-preview?target_account_id=67" curl -X POST -H "Authorization: Bearer $TOKEN" -d target_code=5104 $APP/api/v1/accounting/chart-of-accounts/120/merge
Voucher numbering
Every journal entry has a voucher type; when it is posted it gets the next number of that type's series.
| Code | Voucher | Typical use |
|---|---|---|
JV |
Journal Voucher (default) | Adjustments, accruals, transfers; reversals and closing entries of untyped entries |
CPV / CRV |
Cash Payment / Cash Receipt Voucher | Cash paid / received |
BPV / BRV |
Bank Payment / Bank Receipt Voucher | Cheques and transfers out / in |
- Gapless, in posting order. The number is taken inside the posting transaction under a row lock, so two users posting at once never get the same number, and a failed posting gives its number back. Drafts have no number, so a voided draft never leaves a gap (auditors check exactly this).
- Format per type, default
{PREFIX}-{FY}-{SEQ:5}→JV-2026-00012. Tokens:{PREFIX},{FY}(2026, or2025-26for a July–June year),{YYYY},{YY},{MM},{SEQ}/{SEQ:n}. - Restarts every fiscal year (default), every month (format needs
{MM}and a year), or never. - Locked: the database refuses to change a posted entry's voucher number or type. A type with numbered
entries keeps its code and restart rule (name, prefix and format may change; new numbers use them).
The default type
JVcannot be deleted or deactivated; other used types can only be deactivated. - A reversal is numbered in the series of the entry it reverses (
BRV-2026-00002reversesBRV-2026-00001). - Choose the type in the journal form (React and Blade), or send
voucher_type_code/voucher_type_idto the API. Manage types under Accounting → Voucher Types (voucher-types.*; admin manages, everyone else views). - Upgrading numbers the entries already posted as
JV, in posting order, and continues the series from there.
$entry = JournalEntry::query()->find($id); $entry->voucher_number; // "CPV-2026-00007" (null while draft) $entry->voucherType->name; // "Cash Payment Voucher" app(VoucherNumberService::class)->preview($type, '2026-11-01'); // next number, not reserved
Source documents
An entry can say which document it records — a sales invoice, purchase bill, receipt, payment voucher,
credit/debit note, expense claim, payroll sheet, bank statement, contract or other (config('accounting.source_documents.types'))
— with its number and date, and can be linked to the application model it came from.
- No double posting. A document (type + number, case-insensitive) can be posted only once per company. The second
posting is refused: "Purchase bill BILL-778 is already posted as JV-2026-00012. Reverse that entry first, or correct
the document number." A unique database index backs this up, so two users posting the same bill at the same moment
cannot both succeed. Drafts with the same number can still be saved. Turn it off with
ACCOUNTING_PREVENT_DUPLICATE_DOCUMENTS=false. - Reversal frees the document: the reversal records the same document (and link) but never holds it, so a corrected entry can be posted for the same bill.
- Locked after posting: the database refuses to change the document type, number, date or link of a posted entry.
- Shown in the journal form (React and Blade), the entry page, the journal list (filter Document no.) and the general ledger (voucher number and document number on every line, exports included).
- API:
source_document_type,source_document_number,source_document_dateonPOST /journal-entriesand/journal-entries/simple; responses carrysource_document; filtersfilter[source_document_number],filter[source_document_type].
Link entries to your own models:
use Alimarchal\LaravelChartOfAccounts\Concerns\HasJournalEntries; class Invoice extends Model { use HasJournalEntries; } JournalEntry::record("Invoice {$invoice->number}", '1103', '4101', $invoice->total, post: true, source: $invoice, documentType: 'invoice', documentNumber: $invoice->number); $invoice->journalEntries; // every entry for the invoice, reversals included $invoice->postedJournalEntry(); // the posted entry that currently holds it JournalEntry::forSource($invoice)->get();
JournalEntryService::create() takes the same source_document_* keys and 'source' => $invoice.
Control accounts
A control account summarises a sub-ledger — customers (Accounts Receivable), suppliers (Accounts Payable), stock, fixed assets, payroll or tax. Its balance must always equal the sub-ledger, so only entries of that module may post to it. A manual journal entry (UI or API) touching a control account is refused:
Account 1103 Accounts Receivable is the control account for accounts receivable (customers): post through that module, or ask a controller with the "control-accounts.post-manual" permission.
- Setting up: open Accounting → Control Accounts and click Recommended setup (marks 1103 → receivables,
2101 → payables, 1151–1153 → inventory, 2103 → payroll, 1107/2104 → tax;
config('accounting.control_accounts.recommended')), or mark any posting account yourself. Existing installs are not marked on upgrade, so nothing changes until you opt in. Changing control accounts needscontrol-accounts.manage(super-admin and admin). - Modules post with their name:
JournalEntryService::create([... 'origin_module' => 'receivables'])orJournalEntry::record(..., module: 'receivables'). The API never accepts a module, so API clients post as manual. The module of a posted entry is locked in the database. - Controller adjustments: users with
control-accounts.post-manual(super-admin only by default) may post manual entries to control accounts — under maker-checker both the submitter and the approver need it. Every such entry appears in the Manual postings column and list, which is the first thing an auditor reviews. - Reversals and year-end closing entries follow the entry they come from and are never blocked.
- API:
GET /control-accounts(balances and manual postings),POST /control-accounts/recommended,GET /control-accounts/{id}/manual-postings,PUT /chart-of-accounts/{id}/control-type; accounts carrycontrol_type.
Attachments (supporting documents)
Every journal entry can carry its evidence — the scanned bill, receipt, contract or bank advice. Use the Supporting documents panel on the entry page (React and Blade) or the API.
- Private storage: files go to
ACCOUNTING_ATTACHMENTS_DISK(defaultlocal, i.e.storage/app/private) underaccounting/{company}/{yyyy}/{mm}/{uuid}.extand are served only through the authorised download route (attachments.view, company-scoped). Paths never leave the server. - Limits:
ACCOUNTING_ATTACHMENTS_MAX_KB(default 10 MB) andconfig('accounting.attachments.mimes')(pdf, images, Office files, csv, txt, xml, zip). - Evidence is kept: documents of posted or voided entries cannot be removed; drafts can lose them. Every upload
and removal is in the audit trail (
ATTACHMENT_ADDED/ATTACHMENT_REMOVED, with the SHA-256 of the file). - Duplicate warning: a file that is already attached to another entry (same SHA-256) is flagged — the classic "same bill booked twice" check.
- Evidence required above an amount:
ACCOUNTING_ATTACHMENTS_REQUIRED_ABOVE=50000makes entries of that total (base currency) or more need at least one document before they are posted or submitted for approval. - Permissions:
attachments.view(every role that sees the journal),attachments.createandattachments.delete(accountant). API:GET/POST /journal-entries/{id}/attachments(multipartfile,description),GET /attachments/{id}/download,DELETE /attachments/{id}.
app(AttachmentService::class)->attach($entry, $request->file('bill'), 'Supplier bill'); $entry->attachments; // morphMany
Branches
One company, several places of business (head office, campuses, shops). Turn it on once; nothing changes while it is off.
ACCOUNTING_BRANCHES=true # off by default; `php artisan accounting:install` asks for it
Create the branches in Accounting → Branches (React and Blade, permission branches.create) or with POST /api/v1/accounting/branches.
After upgrading an existing installation run php artisan accounting:update once, so the branches.* permissions are created.
| Blade (Livewire kit): the Branch switcher limits the list | React: the branches screen |
|---|---|
![]() |
![]() |
| Booked to a branch | Journal entries, invoices/bills and payments. An invoice, bill or payment gives its branch to the entry it posts; a reversal keeps the branch of the original; the year-end close is for the whole company. Entries with no branch are "head office / not allocated". |
| Working branch | Pick it in the Branch switcher (above every Blade page and on the React dashboard; add <BranchSwitcher /> from @/components/accounting/branch-switcher to your sidebar), or send X-Branch: LHR (code or id) to the API. New records go to it; lists and reports follow it. "All branches" shows the whole company. |
| Reports by branch | Trial balance, general ledger, account statement, income statement, balance sheet, cash flow, ageing, dashboard KPIs, financial statements. The branches add up to the company. |
| Who sees what | Admins and super-admins always work in every branch (accounting.branches.unrestricted_roles). A user given branches (Branches screen → People who work in this branch, or POST /branches/{id}/users) works in those only: other branches' entries, invoices and reports are not even found, and X-Branch of another branch is 403. A user with no branch, and a super-admin, work in all branches. A user with several branches chooses the branch of each entry. |
| Users | Tick the branches when you create or edit a user (React: Users, Blade: Settings → Users, API: branches: [ids] on POST/PATCH /users). The user sees and books only the data of those branches. |
| What belongs to a branch | Journal entries, invoices/bills, payments, customers/suppliers, bank accounts, employees, fixed assets and warehouses. Anything created while a working branch is picked (or by a user with a single branch) belongs to it; a user of several branches must pick the working branch first. Records without a branch (all data from before branches were turned on) are for people without a branch restriction. Chart of accounts, periods, tax codes and currencies stay company-wide. |
| Deleting | A branch with entries, invoices, bills or payments cannot be deleted: deactivate it. Inactive branches cannot be chosen. |
| Permissions | branches.view, branches.create, branches.update, branches.delete (admin: all; accountant and auditor: view). |
Branch reports
GET /api/v1/accounting/reports/branch-comparison (permission reports.income-statement.view) shows revenue, expenses and net income of every branch side by side; the other
reports (trial balance, income statement, balance sheet, general ledger, ageing, cash flow, dashboard) follow the working branch (X-Branch) or, with none picked, the user's branches.
Seeing only what you are allowed to see is automatic. To build your own report by branch:
- Group by the entry's
branch_id:vw_accounting_general_ledgerhas abranch_idcolumn (null= head office / not allocated), or joinaccounting_journal_entries.branch_idasReports/BranchComparisonReportdoes (one grouped query, posted entries only,is_closing_entry = false, amounts inbase_debit/base_credit). - In your own queries limit to what the user may see with
CurrentBranch::constrain($query, 'entry.branch_id')(orCurrentBranch::accessibleIds()for a branch list); models that use theHasBranchtrait are limited automatically. - Company-wide on purpose: budgets, tax returns and the customer/supplier control-account reconciliation (they follow the one chart of accounts), and the year-end close. Entries and documents booked before branches were switched on have no branch: they are visible to admins, super-admins and users without a branch only.
- A posted entry's branch cannot be changed (a database trigger refuses it); reverse and re-book instead.
Period close and the year-end close are company-wide: run them as a user without branch restriction. Branches are a booking dimension of the company's one chart of accounts, periods and numbering; for separate books use Multi-company instead (the two can be combined: branches of each company).
Multi-company
Run several companies (legal entities) from one installation. Each company has its own chart of accounts, periods, journal, cost centers, bank accounts, tax codes, reports and audit trail; account types and currencies are shared, and every company reports in the shared base currency.
ACCOUNTING_MULTI_COMPANY=true # off by default: a single-company install works exactly as before
A new installation starts with the default company (MAIN), created by the companies migration; nothing changes
until you add a second company.
Create a company (chart of accounts, current fiscal year, cost centers and tax codes are created for it):
php artisan accounting:create-company SUB "Subsidiary Ltd" --fiscal-start=7 --user=accountant@example.com
…or in the React UI (Accounting → Companies, permission companies.manage), or with
POST /api/v1/accounting/companies. --fiscal-start=7 gives a July–June fiscal year.
Who sees what
| Access | A user works only in companies they are given access to (Companies screen, POST /companies/{id}/users, or --user). super-admin works in all companies. Roles are the same in every company. |
| Web UI | A company switcher on the accounting dashboard (React) and above every Blade page. Add <CompanySwitcher /> from @/components/accounting/company-switcher to your sidebar to show it everywhere. |
| API | Send X-Company: SUB (code or id). Without it the user's default company is used. Unknown company → 404, no access → 403. |
| Commands | accounting:close-period {id} and friends work in the company that owns the period; --company=SUB on accounting:seed, verify, health-check, rebuild-snapshots. |
| Isolation | Records of another company are never returned (404 by id); ids and codes of another company are rejected in requests; a journal line can never use another company's account; a posted entry cannot be moved to another company (database trigger). |
Consolidated reports (reports.consolidated.view): trial balance, balance sheet and income statement
for several companies side by side with a group total — Accounting → Consolidated Reports, or
GET /api/v1/accounting/reports/consolidated/{trial-balance|balance-sheet|income-statement}?companies=MAIN,SUB.
Intercompany balances are not eliminated automatically; post elimination entries in a separate
consolidation company if you need them.
Maker-checker approvals
Turn on four-eyes control for journal entries:
ACCOUNTING_APPROVALS_ENABLED=true ACCOUNTING_APPROVAL_THRESHOLD=100000 # base currency; entries below post directly. 0 = every entry
| Step | Who | Web UI | API |
|---|---|---|---|
| Record the draft | maker (accountant) |
Save | POST /journal-entries |
| Submit | maker | Submit for approval | POST /journal-entries/{id}/submit |
| Approve → posts it | checker (approver) |
Approve & post | POST /journal-entries/{id}/approve |
| or Reject with a reason | checker | Reject | POST /journal-entries/{id}/reject {"reason": "…"} |
| Fix and resubmit | maker | Edit → Submit | PUT then submit |
- The checker can never be the person who created or submitted the entry (not even
super-admin) unlessACCOUNTING_ALLOW_SELF_APPROVAL=true. - Posting an entry that needs approval directly is refused (
422), from the UI, the API andJournalEntry::record(). - All posting rules (balance, open period, active accounts) are checked at submission, so makers learn about problems before the checker sees the entry.
- Editing a rejected or pending entry sends it back to draft; it must be submitted again.
- The threshold is compared in the base currency (
amount × fx_rate_to_base). - Year-end closing entries are system-generated and are not held for approval. A reversal at or above the threshold can only be
made by a user who may approve (
ACCOUNTING_APPROVAL_INCLUDE_REVERSALS, defaulttrue). - Module postings (
ACCOUNTING_APPROVAL_MODULES, defaultreceivables,payables,inventory,fixed_assets,tax-return) at or above the threshold can only be made by a user who holdsjournal-entries.approve; the budget control applies to every module posting. Payroll has its own run approval. - Whoever last edited an entry cannot approve it either (
edited_by), so editing is not a way around the four-eyes rule. ACCOUNTING_REQUIRE_REVERSAL_REASON=truemakes a reason mandatory when reversing an entry (reopening a closed period always needs one).- Every step is recorded (who + when) and shown as an audit trail on the entry page; the pending queue is
GET /journal-entries?filter[approval_status]=pending.
Events & webhooks
Every ledger action dispatches a Laravel event after the database transaction commits (listeners never see rolled-back work):
| Event class | Webhook name |
|---|---|
JournalEntryPosted |
journal_entry.posted |
JournalEntryReversed |
journal_entry.reversed |
JournalEntryVoided |
journal_entry.voided |
JournalEntrySubmitted / Approved / Rejected |
journal_entry.submitted / .approved / .rejected |
AccountingPeriodClosed / Reopened |
accounting_period.closed / .reopened |
All live in Alimarchal\LaravelChartOfAccounts\Events and implement AccountingEvent, so one listener can catch them all:
Event::listen(AccountingEvent::class, fn (AccountingEvent $e) => logger($e->name(), $e->payload()));
Webhooks — set ACCOUNTING_WEBHOOK_URLS (comma separated) and ACCOUNTING_WEBHOOK_SECRET. Each endpoint
gets its own queued job (retried with backoff 10 s → 1 h, ACCOUNTING_WEBHOOK_TRIES, default 5). Run a queue
worker in production; a receiver being down never fails the request that posted the entry.
POST /hooks/accounting X-Accounting-Event: journal_entry.posted X-Accounting-Delivery: 0b6f… # same on every retry of this delivery X-Accounting-Signature: t=1791100000,v1=5d41… {"id":"9c1e…","event":"journal_entry.posted","occurred_at":"2026-10-04T10:15:00Z","data":{"journal_entry":{"id":42,…}}}
Verify the signature on the receiving side (id is stable across retries — use it to de-duplicate):
[$t, $v1] = array_map(fn ($p) => explode('=', $p, 2)[1], explode(',', $request->header('X-Accounting-Signature'))); $valid = hash_equals(hash_hmac('sha256', $t.'.'.$request->getContent(), config('services.accounting.secret')), $v1) && abs(time() - (int) $t) < 300; // reject replays older than 5 minutes
Multi-currency
Each journal entry has a currency and fx_rate_to_base. When it is posted, every line's base-currency
amount is calculated once and frozen (base_debit / base_credit, cents-exact: any rounding difference goes
to the largest line, so the entry still balances in the base currency). All reports, balances and the
approval threshold use these base amounts, so a later change of the exchange rate never changes history.
Financial statements and report mapping
Report Mapping (dashboard → Report Mapping, report-mapping.manage) decides which statement line each account
reports under. Every company starts with a standard (IFRS-style) layout:
| Statement | Sections and standard lines |
|---|---|
| Balance sheet | Current assets: Cash and cash equivalents, Trade and other receivables, Inventories, Tax receivable · Non-current assets: Property, plant and equipment, Accumulated depreciation, Long-term investments · Current liabilities: Trade and other payables, Tax payable, Deferred income, Short-term borrowings · Non-current liabilities: Long-term borrowings · Equity: Share capital, Reserves, Retained earnings |
| Income statement | Revenue · Cost of sales · Other income · Operating expenses (Administrative, Depreciation, Other) · Finance costs · Income tax |
- Map a group and everything under it follows; an account can override its parent. Lines can be renamed, reordered and added (custom lines can be deleted once unused).
- Apply recommended mapping maps the seeded chart in one click (new installs and new companies are mapped automatically; existing accounts are never re-mapped).
- Each balance sheet line has a cash-flow class — cash, operating (working capital), non-cash adjustment, investing or financing — which an account can override.
- Accounts without a line are reported on an "unmapped" line of their section (and as unclassified cash flow), so the statements always add up; the screens say how many are left.
Financial Statements (reports.financial-statements.view): one screen with three tabs (React and Blade),
GET /reports/statements/{balance-sheet|income-statement|cash-flow}, and CSV / Excel / PDF exports
(/reports/statement-balance-sheet/export/pdf …).
- Balance sheet as of a date, optional comparative date; profit not yet closed shows in equity; totals and a difference check. Every line expands to its accounts.
- Income statement for a period, optional comparative period, with gross profit, operating profit, profit before tax and net profit.
- Cash flow — indirect method: profit for the period, non-cash adjustments (depreciation …), changes in
working capital, investing and financing activities, then opening and closing cash. Year-end closing entries
are left out, and because every entry balances, opening cash + net change = closing cash — any
differencewould point at a data problem.
Reports & exports
| Report | Web | API | Notes |
|---|---|---|---|
| Trial balance | ✓ | reports/trial-balance |
totals must be equal |
| Balance sheet | ✓ | reports/balance-sheet |
as_of_date |
| Income statement | ✓ | reports/income-statement |
date_from, date_to |
| Cash flow | ✓ | reports/cash-flow |
direct method: cash & bank movements, period totals; paginated |
| General ledger | ✓ | reports/general-ledger |
filter by account, dates, status; paginated |
| Account statement | ✓ | reports/account-statement |
opening balance, running balance, closing balance; paginated |
| Bank book / cash book | ✓ | reports/bank-book, cash-book |
|
| Aged receivables / payables | ✓ | reports/aged-receivables, aged-payables |
30/60/90 buckets |
Every report exports to CSV, Excel (XLSX) and PDF (/accounting/reports/{report}/export/{csv|xlsx|pdf}
and the same under the API, with the same filters; buttons on every React and Blade report). The permission is
checked before any data is read.
- CSV is streamed row by row, so it works for millions of lines.
- PDF is typeset with dompdf when installed (
composer require dompdf/dompdf): company letterhead (logo, address, NTN), report title, the filters used, right-aligned amounts, debit/credit totals, "generated by … on …" and Page X of Y. Without dompdf a built-in renderer produces a plain PDF. - Big Excel/PDF exports run in the background. Above
ACCOUNTING_EXPORT_MAX_XLSX_ROWS(50,000) /ACCOUNTING_EXPORT_MAX_PDF_ROWS(2,000) rows the export is queued and shows up under My Exports (/accounting/exports), where only its owner can download it. The job re-checks the permission when it runs. Run a queue worker (php artisan queue:work) in production, and scheduleaccounting:prune-exportsdaily (keeps 7 days,ACCOUNTING_EXPORTS_KEEP_DAYS).ACCOUNTING_QUEUE_LARGE_EXPORTS=falserefuses big exports with a422instead.
Printing vouchers
Every journal entry prints as a voucher — Print (browser) and PDF buttons on the entry page, or
/accounting/journal-entries/{id}/print · /pdf · GET /api/v1/accounting/journal-entries/{id}/pdf:
voucher type and number, dates, source document, lines with cost centers, totals, amount in words
("PKR One Thousand Two Hundred Fifty and 50/100 Only", or Lakh/Crore with ACCOUNTING_NUMBER_SYSTEM=south_asian),
Prepared / Approved / Posted / Received signature boxes, and a DRAFT / VOID / REVERSED watermark.
// config/accounting.php 'pdf' => [ 'engine' => env('ACCOUNTING_PDF_ENGINE', 'auto'), // auto | dompdf | builtin 'paper' => env('ACCOUNTING_PDF_PAPER', 'a4'), 'logo' => env('ACCOUNTING_PDF_LOGO'), // path on ACCOUNTING_PDF_LOGO_DISK; a company's own logo wins 'number_system' => env('ACCOUNTING_NUMBER_SYSTEM', 'international'), // or south_asian (Lakh, Crore) ],
Performance with 400,000 posted lines (PostgreSQL 16, 4 vCPU): trial balance 0.77 s, balance sheet 0.32 s, general ledger page 0.09–0.38 s, account statement 0.11 s, 46 postings/s from 24 concurrent clients with zero imbalance. Full numbers: docs/performance.md.
Full REST API Reference
Base URL: /api/v1/accounting — ACCOUNTING_API_PREFIX; middleware ACCOUNTING_API_MIDDLEWARE (default api,auth:sanctum)
plus throttle:accounting-api (ACCOUNTING_API_RATE_LIMIT, default 120/min per user). Every endpoint checks a permission.
Token abilities: with ACCOUNTING_API_ENFORCE_TOKEN_ABILITIES=true (accounting.api_enforce_token_abilities, off by default) a Sanctum token needs accounting:read to read (GET) and accounting:write for anything else (read/write and * also work), e.g. $user->createToken('reporting', ['accounting:read']). Permissions are always checked as well.
Amounts: a transaction amount may not exceed accounting.max_amount (ACCOUNTING_MAX_AMOUNT, default 999,999,999,999.99; the database columns hold more, DECIMAL(18,2)). Larger values answer 422.
Machine-readable spec: docs/openapi.yaml. Every screen and endpoint with its permission: the help catalog.
328 endpoints, generated from the routes by docs/generate-route-catalog.py. Screens: see the help catalog.
Journal entries
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /journal-entries/{journalEntry}/pdf |
Printable voucher PDF | journal-entries.view |
| GET | /journal-entries/{journalEntry}/attachments |
Supporting documents of an entry | attachments.view |
| POST | /journal-entries/{journalEntry}/attachments |
Attach a document (multipart/form-data) | attachments.create |
| GET | /attachments/{attachment}/download |
Download a document | attachments.view |
| DELETE | /attachments/{attachment} |
Remove a document of a draft | attachments.delete |
| GET | /opening-balances |
The opening-balance entry of the company, if one is posted | journal-entries.view |
| POST | /opening-balances |
Post the balances the company starts with | journal-entries.post |
| GET | /journal-entries |
List entries | journal-entries.view |
| POST | /journal-entries |
Create (and optionally post) an entry | journal-entries.create |
| POST | /journal-entries/simple |
Two-line entry by account codes | journal-entries.create |
| GET | /journal-entries/{journalEntry} |
Show an entry with lines | journal-entries.view |
| PUT | /journal-entries/{journalEntry} |
Update a draft (PATCH also accepted) | journal-entries.update |
| POST | /journal-entries/{journalEntry}/post |
Post a draft | journal-entries.post |
| POST | /journal-entries/{journalEntry}/reverse |
Reverse a posted entry | journal-entries.reverse |
| POST | /journal-entries/{journalEntry}/submit |
Submit a draft for approval (maker) | journal-entries.create |
| POST | /journal-entries/{journalEntry}/approve |
Approve and post (checker) | journal-entries.approve |
| POST | /journal-entries/{journalEntry}/reject |
Reject back to the maker (checker) | journal-entries.approve |
| POST | /journal-entries/{journalEntry}/void |
Void a draft | journal-entries.void |
Chart of accounts
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /chart-of-accounts |
List accounts | chart-of-accounts.view |
| POST | /chart-of-accounts |
Create an account | chart-of-accounts.create |
| GET | /chart-of-accounts/export/{format} |
Export the chart (re-importable) | chart-of-accounts.view |
| GET | /chart-of-accounts/import/template/{format} |
Import template with example rows | chart-of-accounts.import |
| POST | /chart-of-accounts/import |
Import accounts from CSV or Excel (multipart/form-data) | chart-of-accounts.import |
| GET | /chart-of-accounts/{chartOfAccount}/renumber-preview |
Preview a renumbering | chart-of-accounts.restructure |
| POST | /chart-of-accounts/{chartOfAccount}/renumber |
Renumber an account (used accounts too) | chart-of-accounts.restructure |
| GET | /chart-of-accounts/{chartOfAccount}/merge-preview |
Preview merging this account into another | chart-of-accounts.restructure |
| POST | /chart-of-accounts/{chartOfAccount}/merge |
Merge this account into another | chart-of-accounts.restructure |
| GET | /chart-templates |
Industry chart templates | chart-templates.apply |
| GET | /chart-templates/{template} |
What a template would add to this company | chart-templates.apply |
| POST | /chart-templates/{template}/apply |
Add a template's accounts to this company | chart-templates.apply |
| GET | /chart-of-accounts/tree |
Whole chart as a nested tree | chart-of-accounts.view |
| GET | /chart-of-accounts/{chartOfAccount}/balance |
Account balance | chart-of-accounts.view |
| GET | /chart-of-accounts/{chartOfAccount} |
Show an account | chart-of-accounts.view |
| PUT | /chart-of-accounts/{chartOfAccount} |
Update an account (PATCH also accepted) | chart-of-accounts.update |
| DELETE | /chart-of-accounts/{chartOfAccount} |
Delete an account | chart-of-accounts.delete |
| GET | /control-accounts |
Control accounts with balances and manual postings | chart-of-accounts.view |
| POST | /control-accounts/recommended |
Mark the recommended control accounts | control-accounts.manage |
| GET | /control-accounts/{chartOfAccount}/manual-postings |
Manual postings to a control account | chart-of-accounts.view |
| PUT | /chart-of-accounts/{chartOfAccount}/control-type |
Set or clear an account's control type | control-accounts.manage |
Reports
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /report-mapping |
Report lines and the mapping of every account | report-mapping.manage |
| POST | /report-mapping/recommended |
Map the seeded accounts to their standard lines | report-mapping.manage |
| PUT | /chart-of-accounts/{chartOfAccount}/report-mapping |
Map an account (and its sub-accounts) to a line | report-mapping.manage |
| POST | /report-lines |
Add a statement line | report-mapping.manage |
| PUT | /report-lines/{reportLine} |
Rename, move or reclassify a line | report-mapping.manage |
| DELETE | /report-lines/{reportLine} |
Delete a custom line | report-mapping.manage |
| GET | /reports/statements/{type} |
Financial statement by report lines | reports.financial-statements.view |
| GET | /reports/{report}/export/{format} |
Download a report as CSV, Excel or a typeset PDF | accounting.view |
| POST | /reports/{report}/exports/{format} |
Queue a background export | accounting.view |
| GET | /exports |
Your background exports | accounting.view |
| GET | /exports/{export}/download |
Download a finished export | accounting.view |
| DELETE | /exports/{export} |
Delete one of your exports and its file | accounting.view |
| GET | /reports/consolidated/{report} |
Consolidated report | reports.consolidated.view |
| GET | /reports/trial-balance |
Trial balance | reports.trial-balance.view |
| GET | /reports/balance-sheet |
Balance sheet | reports.balance-sheet.view |
| GET | /reports/income-statement |
Income statement | reports.income-statement.view |
| GET | /reports/general-ledger |
General ledger | reports.general-ledger.view |
| GET | /reports/cash-flow |
Cash flow | reports.cash-flow.view |
| GET | /reports/bank-book |
Bank book | reports.bank-book.view |
| GET | /reports/cash-book |
Cash book | reports.cash-book.view |
| GET | /reports/aged-receivables |
Aged receivables | reports.aged-receivables.view |
| GET | /reports/aged-payables |
Aged payables | reports.aged-payables.view |
| GET | /reports/account-statement |
Account statement | reports.account-statement.view |
| GET | /reports/branch-comparison |
Branch comparison | reports.income-statement.view |
Periods
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| POST | /periods/generate-monthly |
Create twelve monthly periods | periods.create |
| GET | /periods |
List periods | periods.view |
| POST | /periods |
Create periods | periods.create |
| GET | /periods/{record} |
Show periods | periods.view |
| PUT | /periods/{record} |
Update periods (PATCH also accepted) | periods.update |
| DELETE | /periods/{record} |
Delete periods | periods.delete |
| GET | /periods/{period}/close-checklist |
Pre-close checklist | periods.view |
| POST | /periods/{period}/close |
Close a period | periods.close |
| POST | /periods/{period}/reopen |
Reopen a closed period | periods.reopen |
| POST | /periods/{period}/close-fiscal-year |
Year-end close | periods.close |
| GET | /account-balance-snapshots |
List balance snapshots | account-balance-snapshots.view |
| GET | /account-balance-snapshots/{record} |
Show a balance snapshot | account-balance-snapshots.view |
Account types
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /account-types |
List account-types | account-types.view |
| POST | /account-types |
Create account-types | account-types.create |
| GET | /account-types/{record} |
Show account-types | account-types.view |
| PUT | /account-types/{record} |
Update account-types (PATCH also accepted) | account-types.update |
| DELETE | /account-types/{record} |
Delete account-types | account-types.delete |
Currencies
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /currencies |
List currencies | currencies.view |
| POST | /currencies |
Create currencies | currencies.create |
| GET | /currencies/{record} |
Show currencies | currencies.view |
| PUT | /currencies/{record} |
Update currencies (PATCH also accepted) | currencies.update |
| DELETE | /currencies/{record} |
Delete currencies | currencies.delete |
Cost centers
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /cost-centers |
List cost-centers | cost-centers.view |
| POST | /cost-centers |
Create cost-centers | cost-centers.create |
| GET | /cost-centers/{record} |
Show cost-centers | cost-centers.view |
| PUT | /cost-centers/{record} |
Update cost-centers (PATCH also accepted) | cost-centers.update |
| DELETE | /cost-centers/{record} |
Delete cost-centers | cost-centers.delete |
Bank accounts
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /bank-accounts |
List bank-accounts | bank-accounts.view |
| POST | /bank-accounts |
Create bank-accounts | bank-accounts.create |
| GET | /bank-accounts/{record} |
Show bank-accounts | bank-accounts.view |
| PUT | /bank-accounts/{record} |
Update bank-accounts (PATCH also accepted) | bank-accounts.update |
| DELETE | /bank-accounts/{record} |
Delete bank-accounts | bank-accounts.delete |
Reconciliations
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /reconciliations |
List reconciliations | reconciliations.view |
| POST | /reconciliations |
Create reconciliations | reconciliations.create |
| GET | /reconciliations/{record} |
Show reconciliations | reconciliations.view |
| PUT | /reconciliations/{record} |
Update reconciliations (PATCH also accepted) | reconciliations.update |
| DELETE | /reconciliations/{record} |
Delete reconciliations | reconciliations.delete |
Tax
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| POST | /tax/calculate |
Tax on an amount | tax-codes.view |
| POST | /tax/entries |
Book a taxed document as a journal entry | tax-entries.create |
| GET | /tax/returns/report |
The tax ledger of a period | tax-returns.view |
| GET | /tax/returns/report/export/{format} |
Export the tax report | tax-returns.view |
| GET | /tax/returns |
Filed tax returns | tax-returns.view |
| POST | /tax/returns |
File a return for a period | tax-returns.file |
| GET | /tax/returns/{taxReturn} |
A filed return | tax-returns.view |
| DELETE | /tax/returns/{taxReturn} |
Void a return (its entry is reversed) | tax-returns.file |
| GET | /tax-codes |
List tax-codes | tax-codes.view |
| POST | /tax-codes |
Create tax-codes | tax-codes.create |
| GET | /tax-codes/{record} |
Show tax-codes | tax-codes.view |
| PUT | /tax-codes/{record} |
Update tax-codes (PATCH also accepted) | tax-codes.update |
| DELETE | /tax-codes/{record} |
Delete tax-codes | tax-codes.delete |
| GET | /tax-rates |
List tax-rates | tax-rates.view |
| POST | /tax-rates |
Create tax-rates | tax-rates.create |
| GET | /tax-rates/{record} |
Show tax-rates | tax-rates.view |
| PUT | /tax-rates/{record} |
Update tax-rates (PATCH also accepted) | tax-rates.update |
| DELETE | /tax-rates/{record} |
Delete tax-rates | tax-rates.delete |
Recurring entries
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /recurring-entries |
Recurring entry templates | recurring-entries.view |
| POST | /recurring-entries |
Create a template | recurring-entries.create |
| GET | /recurring-entries/{recurringEntry} |
A template with its generated entries and upcoming dates | recurring-entries.view |
| PUT | /recurring-entries/{recurringEntry} |
Change a template (PATCH also accepted) | recurring-entries.update |
| DELETE | /recurring-entries/{recurringEntry} |
Delete a template that has not generated anything | recurring-entries.delete |
| POST | /recurring-entries/{recurringEntry}/pause |
Pause a template | recurring-entries.update |
| POST | /recurring-entries/{recurringEntry}/resume |
Resume a paused template | recurring-entries.update |
| POST | /recurring-entries/{recurringEntry}/run |
Generate the next entry now | recurring-entries.run |
Voucher types
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /voucher-types |
List voucher types with their next numbers | voucher-types.view |
| POST | /voucher-types |
Create a voucher type | voucher-types.create |
| GET | /voucher-types/{record} |
Show a voucher type | voucher-types.view |
| PUT | /voucher-types/{record} |
Update a voucher type (PATCH also accepted) | voucher-types.update |
| DELETE | /voucher-types/{record} |
Delete an unused voucher type | voucher-types.delete |
| GET | /voucher-types/{record}/next-number |
Preview the next number | voucher-types.view |
Branches
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /branches |
Branches of the company | branches.view |
| POST | /branches |
Create a branch | branches.create |
| GET | /branches/{branch} |
Show a branch with the people who work in it | branches.view |
| PUT | /branches/{branch} |
Update a branch | branches.update |
| DELETE | /branches/{branch} |
Delete a branch | branches.delete |
| POST | /branches/{branch}/users |
Limit a user to this branch (add) | branches.update |
| DELETE | /branches/{branch}/users/{user} |
Remove a user from this branch | branches.update |
Companies
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /companies |
Companies you can work in | accounting.view |
| POST | /companies |
Create a company | companies.manage |
| GET | /companies/{company} |
Show a company with its members | companies.manage |
| PUT | /companies/{company} |
Update a company | companies.manage |
| POST | /companies/{company}/users |
Give a user access | companies.manage |
| DELETE | /companies/{company}/users/{user} |
Remove a user's access | companies.manage |
Users & roles
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /users |
List users | user.view |
| POST | /users |
Create a user | user.create |
| GET | /users/{user} |
Show a user with all effective permissions | user.view |
| PUT | /users/{user} |
Update a user (PATCH also accepted) | user.update |
| DELETE | /users/{user} |
Delete a user | user.delete |
| PUT | /users/{user}/roles |
Replace a user's roles | user.assign-role |
| PUT | /users/{user}/permissions |
Replace a user's direct permissions | user.assign-permission |
| GET | /roles |
List roles with permission and user counts | accounting.manage-settings |
| POST | /roles |
Create a role | accounting.manage-settings |
| GET | /roles/{role} |
Show a role | accounting.manage-settings |
| PUT | /roles/{role} |
Update a role (PATCH also accepted) | accounting.manage-settings |
| DELETE | /roles/{role} |
Delete a role | accounting.manage-settings |
| GET | /permissions |
All permissions grouped by area | accounting.manage-settings |
System
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /dashboard |
Dashboard overview | accounting.view |
| GET | /health |
Installation health | accounting.view |
Bank statements
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /bank-statements |
Imported statements | bank-statements.view |
| POST | /bank-statements/import |
Import a statement (CSV or XLSX) | bank-statements.import |
| GET | /bank-statements/{bankStatement} |
A statement with its transactions | bank-statements.view |
| PUT | /bank-statements/{bankStatement} |
Set the closing balance (PATCH also accepted) | bank-statements.match |
| DELETE | /bank-statements/{bankStatement} |
Delete a statement and release its matches | bank-statements.match |
| POST | /bank-statements/{bankStatement}/auto-match |
Match every transaction that has exactly one ledger candidate | bank-statements.match |
| POST | /bank-statements/{bankStatement}/reconcile |
Reconcile the statement | bank-statements.match |
| GET | /bank-statement-lines/{bankStatementLine}/candidates |
Ledger lines that could be this transaction | bank-statements.view |
| POST | /bank-statement-lines/{bankStatementLine}/match |
Match a transaction to a ledger line | bank-statements.match |
| POST | /bank-statement-lines/{bankStatementLine}/unmatch |
Release a matched or booked transaction | bank-statements.match |
| POST | /bank-statement-lines/{bankStatementLine}/ignore |
Ignore (or restore) a transaction | bank-statements.match |
| POST | /bank-statement-lines/{bankStatementLine}/create-entry |
Book a transaction as a journal entry | bank-statements.match |
Budgets
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /budgets |
Budgets | budgets.view |
| POST | /budgets |
Create a draft budget | budgets.create |
| GET | /budgets/{budget} |
Budget against actual | budgets.view |
| PUT | /budgets/{budget} |
Change a draft budget (PATCH also accepted) | budgets.update |
| DELETE | /budgets/{budget} |
Delete a budget that is not approved | budgets.delete |
| POST | /budgets/{budget}/approve |
Approve a draft budget | budgets.approve |
| POST | /budgets/{budget}/reopen |
Reopen an approved or closed budget as a draft | budgets.update |
| POST | /budgets/{budget}/close |
Close an approved budget | budgets.approve |
| POST | /budgets/{budget}/copy |
Copy as a draft, optionally shifted and raised | budgets.create |
| GET | /budgets/{budget}/export/{format} |
Export budget against actual | budgets.view |
Currency revaluation
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /fx-revaluation |
Past revaluations and the dated exchange rates | fx-revaluation.view |
| GET | /fx-revaluation/preview |
What a revaluation would adjust (nothing is posted) | fx-revaluation.view |
| POST | /fx-revaluation |
Post a revaluation | fx-revaluation.run |
| POST | /fx-revaluation/rates |
Save a dated exchange rate (replaces the rate of that currency and date) | fx-revaluation.rates |
| DELETE | /fx-revaluation/rates/{exchangeRate} |
Remove a dated rate | fx-revaluation.rates |
| GET | /fx-revaluation/{fxRevaluation} |
One revaluation with its accounts | fx-revaluation.view |
| POST | /fx-revaluation/{fxRevaluation}/reverse |
Reverse a revaluation | fx-revaluation.run |
Fixed assets
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /fixed-assets |
Fixed asset register at a date | fixed-assets.view |
| POST | /fixed-assets |
Register an asset | fixed-assets.create |
| GET | /fixed-assets/depreciation |
Preview the depreciation due | fixed-assets.view |
| POST | /fixed-assets/depreciation |
Book the depreciation due | fixed-assets.depreciate |
| GET | /fixed-assets/export/{format} |
Export the register | fixed-assets.view |
| GET | /fixed-assets/{asset} |
An asset with its depreciation history | fixed-assets.view |
| PUT | /fixed-assets/{asset} |
Change an asset (PATCH also accepted) | fixed-assets.update |
| DELETE | /fixed-assets/{asset} |
Delete an asset without entries | fixed-assets.delete |
| POST | /fixed-assets/{asset}/dispose |
Sell or scrap an asset | fixed-assets.dispose |
Inventory
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /inventory |
Stock valuation at a date | inventory.view |
| GET | /inventory/export/{format} |
Export the stock valuation | inventory.view |
| POST | /inventory/items |
Create an item | inventory.manage |
| GET | /inventory/items/{item} |
An item with its stock card | inventory.view |
| PUT | /inventory/items/{item} |
Change an item (PATCH also accepted) | inventory.manage |
| DELETE | /inventory/items/{item} |
Delete an item without movements | inventory.manage |
| GET | /inventory/warehouses |
Warehouses | inventory.view |
| POST | /inventory/warehouses |
Add a warehouse | inventory.manage |
| PUT | /inventory/warehouses/{warehouse} |
Change a warehouse (PATCH also accepted) | inventory.manage |
| DELETE | /inventory/warehouses/{warehouse} |
Delete a warehouse without movements | inventory.manage |
| GET | /inventory/movements |
The latest 200 stock movements | inventory.view |
| POST | /inventory/movements |
Receive, issue, adjust or transfer stock | inventory.move |
Payroll
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /payroll |
Payroll runs, newest first | payroll.view |
| POST | /payroll/runs |
Start a payroll run for a month | payroll.run |
| GET | /payroll/runs/{run} |
A run with its payslips and their lines | payroll.view |
| POST | /payroll/runs/{run}/recalculate |
Work the payslips out again (draft runs) | payroll.run |
| POST | /payroll/runs/{run}/post |
Post the run to the books | payroll.post |
| POST | /payroll/runs/{run}/pay |
Pay the net salaries | payroll.post |
| POST | /payroll/runs/{run}/void |
Void a posted or paid run | payroll.void |
| DELETE | /payroll/runs/{run} |
Delete a draft run | payroll.run |
| GET | /payroll/runs/{run}/payslips/{payslip} |
One payslip | payroll.view |
| GET | /payroll/employees |
Employees | payroll.view |
| POST | /payroll/employees |
Add an employee | payroll.manage |
| GET | /payroll/employees/{employee} |
An employee with their allowances and deductions | payroll.view |
| PUT | /payroll/employees/{employee} |
Change an employee (PATCH also accepted) | payroll.manage |
| DELETE | /payroll/employees/{employee} |
Delete an employee without payslips | payroll.manage |
| GET | /payroll/components |
Allowances and deductions | payroll.view |
| POST | /payroll/components |
Add an allowance or deduction | payroll.manage |
| PUT | /payroll/components/{component} |
Change a component (PATCH also accepted) | payroll.manage |
| DELETE | /payroll/components/{component} |
Delete a component that is not in use | payroll.manage |
| GET | /payroll/grades |
Salary grades | payroll.view |
| POST | /payroll/grades |
Add a salary grade | payroll.manage |
| GET | /payroll/grades/{grade} |
A grade with its allowances | payroll.view |
| PUT | /payroll/grades/{grade} |
Change a grade (PATCH also accepted) | payroll.manage |
| DELETE | /payroll/grades/{grade} |
Delete a grade nobody is on | payroll.manage |
| POST | /payroll/grades/{grade}/assign |
Put a group of employees on the grade | payroll.manage |
| POST | /payroll/bulk/components |
Give a component to a group of employees, or take it away | payroll.manage |
| GET | /payroll/employees/{employee}/revisions |
The salary history of an employee | payroll.view |
| POST | /payroll/employees/{employee}/revisions |
Revise an employee's salary from a date | payroll.manage |
| POST | /payroll/revisions/preview |
Preview a raise for a group (nothing is saved) | payroll.manage |
| POST | /payroll/revisions |
Apply a raise to a group | payroll.manage |
| GET | /payroll/arrears |
The arrears register with totals | payroll.view |
| POST | /payroll/arrears/preview |
Work arrears out without saving | payroll.run |
| POST | /payroll/arrears |
Create arrears (drafts) from the posted payslips | payroll.run |
| POST | /payroll/arrears/approve-all |
Approve every arrears draft | payroll.post |
| GET | /payroll/arrears/{arrear} |
One arrears record with its months | payroll.view |
| POST | /payroll/arrears/{arrear}/approve |
Approve arrears | payroll.post |
| POST | /payroll/arrears/{arrear}/cancel |
Cancel arrears not yet in a run | payroll.post |
| GET | /payroll/attendance |
The attendance sheet of a month | payroll.view |
| POST | /payroll/attendance |
Save the attendance sheet of a month | payroll.manage |
| GET | /payroll/leave-types |
Leave types | payroll.view |
| POST | /payroll/leave-types |
Add a leave type | payroll.manage |
| PUT | /payroll/leave-types/{leaveType} |
Change a leave type (PATCH also accepted) | payroll.manage |
| DELETE | /payroll/leave-types/{leaveType} |
Delete a leave type nobody took | payroll.manage |
| GET | /payroll/leaves |
Leaves, newest first | payroll.view |
| GET | /payroll/leaves/balances |
Leave balances of a year | payroll.view |
| POST | /payroll/leaves |
Record leave | payroll.manage |
| POST | /payroll/leaves/{leave}/cancel |
Cancel a leave | payroll.manage |
| GET | /payroll/loans |
Loans and advances | payroll.view |
| POST | /payroll/loans |
Record a loan or advance with its instalment schedule | payroll.manage |
| GET | /payroll/loans/{loan} |
A loan with its schedule | payroll.view |
| POST | /payroll/loans/{loan}/disburse |
Pay the loan out | payroll.post |
| POST | /payroll/loans/{loan}/settle |
The employee pays back what is left in cash | payroll.post |
| POST | /payroll/loans/{loan}/skip |
Move the next instalment to the end of the schedule | payroll.manage |
| POST | /payroll/loans/{loan}/cancel |
Cancel a loan not yet paid out | payroll.manage |
| GET | /payroll/schemes |
Contribution schemes (EOBI, PESSI/SESSI, provident fund) | payroll.view |
| POST | /payroll/schemes |
Add a contribution scheme | payroll.manage |
| GET | /payroll/schemes/{scheme} |
A scheme | payroll.view |
| PUT | /payroll/schemes/{scheme} |
Change a scheme (PATCH also accepted) | payroll.manage |
| DELETE | /payroll/schemes/{scheme} |
Delete a scheme | payroll.manage |
| POST | /payroll/schemes/{scheme}/assign |
Give a scheme to employees or take it away | payroll.manage |
| GET | /payroll/runs/{run}/bank-file/{format} |
The bank salary file of a posted run | payroll.post |
| GET | /payroll/runs/{run}/payslips/{payslip}/print |
A payslip as a printable page | payroll.view |
| GET | /payroll/runs/{run}/payslips/{payslip}/pdf |
A payslip as a PDF | payroll.view |
| POST | /payroll/runs/{run}/payslips/{payslip}/email |
E-mail one payslip to the employee | payroll.manage |
| POST | /payroll/runs/{run}/email-payslips |
E-mail every payslip of a posted run | payroll.manage |
| POST | /payroll/runs/{run}/submit |
HR submits a draft run to finance | payroll.run |
| POST | /payroll/runs/{run}/withdraw |
Take a submitted or approved run back to draft | payroll.run |
| POST | /payroll/runs/{run}/approve |
Finance approves a submitted run | payroll.approve |
| POST | /payroll/runs/{run}/reject |
Finance sends a submitted run back to HR | payroll.approve |
| GET | /payroll/adjustments |
Bonuses and other one-off pay of a month | payroll.view |
| POST | /payroll/adjustments |
Add a bonus, extra allowance or fine for one employee and month | payroll.manage |
| POST | /payroll/adjustments/bulk |
The same bonus for every active employee (or the ones named) | payroll.manage |
| POST | /payroll/adjustments/{adjustment}/cancel |
Cancel an adjustment no run has taken up | payroll.manage |
| GET | /payroll/settlements |
Final settlements | payroll.view |
| POST | /payroll/settlements/preview |
Work a settlement out without saving | payroll.manage |
| POST | /payroll/settlements |
Save a settlement as a draft | payroll.manage |
| GET | /payroll/settlements/{settlement} |
One settlement | payroll.view |
| DELETE | /payroll/settlements/{settlement} |
Delete a draft settlement | payroll.manage |
| POST | /payroll/settlements/{settlement}/post |
Post a settlement to the books | payroll.post |
| POST | /payroll/settlements/{settlement}/pay |
Pay a posted settlement | payroll.post |
| POST | /payroll/settlements/{settlement}/void |
Void a settlement | payroll.void |
| GET | /payroll/reports |
Month comparison, cost centers and headcount | payroll.view |
| GET | /payroll/reports/{report}/export/{format} |
Download a payroll report | payroll.view |
| GET | /payroll/tax |
Salary and tax withheld per employee for a tax year | payroll.view |
| GET | /payroll/tax/annual/{format} |
Download the annual salary tax statement | payroll.view |
| GET | /payroll/tax/certificate/{employee} |
An employee's salary tax certificate | payroll.view |
Receivables & payables
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /parties |
Customers and suppliers | parties.view |
| POST | /parties |
Create a customer or supplier | parties.create |
| GET | /parties/{party} |
A party with its balance and open items | parties.view |
| PUT | /parties/{party} |
Change a party (PATCH also accepted) | parties.update |
| DELETE | /parties/{party} |
Delete a party without documents or payments | parties.delete |
| GET | /parties/{party}/statement |
Statement of account with a running balance | parties.view |
| GET | /party-documents |
Invoices, bills, credit and debit notes | party-documents.view |
| POST | /party-documents |
Create a draft document | party-documents.create |
| GET | /party-documents/{partyDocument} |
A document with its lines | party-documents.view |
| PUT | /party-documents/{partyDocument} |
Change a draft (PATCH also accepted) | party-documents.update |
| DELETE | /party-documents/{partyDocument} |
Delete a draft | party-documents.delete |
| POST | /party-documents/{partyDocument}/post |
Number and post a draft | party-documents.post |
| POST | /party-documents/{partyDocument}/void |
Void a posted document (its entry is reversed) | party-documents.void |
| POST | /party-documents/{partyDocument}/apply |
Apply a credit or debit note to invoices or bills | party-documents.post |
| GET | /party-payments |
Receipts and payments | party-payments.view |
| POST | /party-payments |
Record a receipt from a customer or a payment to a supplier | party-payments.create |
| GET | /party-payments/{partyPayment} |
A payment with its allocations | party-payments.view |
| POST | /party-payments/{partyPayment}/allocate |
Allocate (more of) a payment to documents | party-payments.create |
| POST | /party-payments/{partyPayment}/void |
Void a payment (entry reversed, allocations released) | party-payments.void |
| DELETE | /party-allocations/{partyAllocation} |
Undo an allocation | party-payments.create |
| GET | /receivables/aging |
Ageing by customer or supplier, checked against the control account | party-documents.view |
| GET | /receivables/aging/export/{format} |
Export the ageing | party-documents.view |
Settings
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /features |
Every optional module and payroll feature with its switch | accounting.manage-settings |
| PUT | /features |
Switch features on or off | accounting.manage-settings |
| GET | /account-roles |
Which account plays which part for the modules (cash, bank, salary expense, receivables …) | accounting.manage-settings |
| PUT | /account-roles |
Give roles other accounts | accounting.manage-settings |
| Method | Endpoint | What it does | Permission |
|---|---|---|---|
| GET | /whatsapp/webhook |
Meta's webhook verification | |
| POST | /whatsapp/webhook |
Meta's events: message statuses (sent, delivered, read, failed) and customer replies | |
| GET | /whatsapp |
Messages, the set-up checks (what is set in .env and what is missing) and the configured templates | whatsapp.view |
| POST | /whatsapp/messages |
Send a message: an approved template with its parameters, or free text | whatsapp.send |
| POST | /whatsapp/messages/{message}/retry |
Send a failed message again | whatsapp.send |
| POST | /whatsapp/messages/{message}/simulate |
Sandbox only: move a message on as WhatsApp would | whatsapp.send |
| POST | /whatsapp/simulate-reply |
Sandbox only: a customer writes back (opens the 24-hour window for free text) | whatsapp.send |
List endpoints support ?filter[field]=value (or the short ?field=value; see Filtering and searching lists), ?sort=field / -field, ?page=N and ?per_page=N (capped by ACCOUNTING_API_MAX_PER_PAGE, default 100).
Journal entries also filter by filter[approval_status]=pending|approved|rejected, filter[voucher_number] (partial),
filter[source_document_number] (partial), filter[source_document_type],
filter[voucher_type_id] and filter[entry_date_from] / filter[entry_date_to].
Ledger-style reports (general ledger, account statement, cash flow, bank & cash book) are paginated and return totals for the whole filter, not just the page.
Configuration
php artisan vendor:publish --tag=accounting-config
// config/accounting.php (abridged) return [ 'ui_driver' => env('ACCOUNTING_UI_DRIVER', 'inertia'), // 'inertia' | 'blade' | 'api' 'route_prefix' => env('ACCOUNTING_ROUTE_PREFIX', 'accounting'), 'route_name_prefix' => env('ACCOUNTING_ROUTE_NAME_PREFIX', 'accounting'), 'settings_route_prefix' => env('SETTINGS_ROUTE_PREFIX', 'settings'), 'api_prefix' => env('ACCOUNTING_API_PREFIX', 'api/v1/accounting'), 'api_middleware' => env('ACCOUNTING_API_MIDDLEWARE', 'api,auth:sanctum'), // comma separated 'api_enabled' => env('ACCOUNTING_API_ENABLED', true), 'api_rate_limit' => env('ACCOUNTING_API_RATE_LIMIT', 120), // per minute per user; 0 = off 'api_max_per_page' => env('ACCOUNTING_API_MAX_PER_PAGE', 100), 'users_table' => env('ACCOUNTING_USERS_TABLE', 'users'), 'multi_company' => [ 'enabled' => env('ACCOUNTING_MULTI_COMPANY', false), 'header' => env('ACCOUNTING_COMPANY_HEADER', 'X-Company'), 'default_company_code' => env('ACCOUNTING_DEFAULT_COMPANY', 'MAIN'), ], 'export_max_rows' => ['xlsx' => 50000, 'pdf' => 2000], // ACCOUNTING_EXPORT_MAX_*_ROWS; CSV is unlimited 'approvals' => [ 'enabled' => env('ACCOUNTING_APPROVALS_ENABLED', false), 'threshold' => env('ACCOUNTING_APPROVAL_THRESHOLD', '0'), // base currency; 0 = all entries 'allow_self_approval' => env('ACCOUNTING_ALLOW_SELF_APPROVAL', false), ], 'webhooks' => [ 'urls' => env('ACCOUNTING_WEBHOOK_URLS', ''), // comma separated 'secret' => env('ACCOUNTING_WEBHOOK_SECRET'), 'timeout' => env('ACCOUNTING_WEBHOOK_TIMEOUT', 10), 'tries' => env('ACCOUNTING_WEBHOOK_TRIES', 5), ], 'defaults' => [ 'currency_code' => env('ACCOUNTING_BASE_CURRENCY', 'PKR'), 'cash_account_code' => env('ACCOUNTING_CASH_ACCOUNT_CODE'), // null: the account with the "cash" role 'bank_account_code' => env('ACCOUNTING_BANK_ACCOUNT_CODE'), 'retained_earnings_account_code' => env('ACCOUNTING_RETAINED_EARNINGS_ACCOUNT_CODE'), 'rounding_account_code' => env('ACCOUNTING_ROUNDING_ACCOUNT_CODE'), ], 'aging' => [ 'receivable_account_codes' => null, // null: the accounts with the "aging.receivables" role 'payable_account_codes' => null, ], 'chart_preset' => env('ACCOUNTING_CHART_PRESET', 'erpnext'), // 'erpnext', 'general' or 'school' 'permissions' => [/* … */], 'roles' => [/* super-admin, admin, accountant, approver, auditor, viewer */], ];
The defaults.*_account_code and aging.* values are optional overrides: leave them out and the accounts that play each
role in the company's chart (see Account roles) are used. An account that plays cash, bank, retained earnings or rounding
(by role or by one of these overrides) is protected: its code cannot change and it cannot be deactivated, deleted or merged away.
The web routes always use ['web', 'auth', 'verified'] plus a per-route can: permission check.
Artisan Commands
| Command | Description |
|---|---|
accounting:install |
Full setup: publish all assets, migrate, seed, sync, verify (--chart=erpnext|general|school) |
accounting:update |
Re-publish assets + sync DB after package upgrade |
accounting:seed |
Seed account types, currencies, COA, permissions, periods (--chart=, --company=) |
accounting:sync-db-objects |
Sync database views, triggers, stored procedures |
accounting:verify |
Verify accounting data integrity |
accounting:health-check |
Run accounting health checks |
accounting:rebuild-snapshots |
Rebuild account balance snapshots |
accounting:close-fiscal-year |
Close the current fiscal year |
accounting:close-period |
Close an accounting period (snapshots, totals, net income) |
accounting:open-period |
Reopen a closed accounting period |
accounting:roles |
Print the role × permission matrix; fails if a role can both create and approve entries |
accounting:prune-exports |
Delete background exports older than accounting.exports.keep_days with their files |
accounting:run-recurring |
Generate the recurring entries that are due (--date=, --company=, --dry-run); scheduled daily |
accounting:account-roles |
Show which account plays which role (cash, bank, salary expense …) for a company, set a role, or seed the roles of a chart (--company=, --overwrite) |
accounting:chart-templates |
List the industry chart templates, preview (--dry-run) or add one to a company (--company=) |
accounting:create-company |
Create a company with its own chart, fiscal year, cost centers and tax codes (--fiscal-start, --user, --empty, --template=) |
Models & Tables
| Model | Table | Description |
|---|---|---|
Company |
accounting_companies |
Companies; accounting_company_user holds who may use each |
AccountType |
accounting_account_types |
Asset, Liability, Equity, Revenue, Expense (shared) |
ChartOfAccount |
accounting_chart_of_accounts |
Hierarchical account tree |
Currency |
accounting_currencies |
Currencies and exchange rates |
AccountingPeriod |
accounting_periods |
Fiscal periods with open/close state |
JournalEntry |
accounting_journal_entries |
Entry header (draft / posted / void), voucher type and number |
VoucherType |
accounting_voucher_types |
Voucher types (JV, CPV, CRV, BPV, BRV, …) and their number format |
VoucherSequence |
accounting_voucher_sequences |
Next number per voucher type and fiscal year / month |
JournalEntryLine |
accounting_journal_entry_lines |
Debit/credit lines |
BankAccount |
accounting_bank_accounts |
Bank account register |
Reconciliation |
accounting_reconciliations |
Bank reconciliation records |
TaxCode |
accounting_tax_codes |
Tax code definitions |
TaxRate |
accounting_tax_rates |
Tax rates per code |
AccountingAuditLog |
accounting_audit_logs |
Full change audit trail |
AccountBalanceSnapshot |
accounting_account_balance_snapshots |
Period-end snapshots |
CostCenter |
accounting_cost_centers |
Departmental cost centers |
Roles & permissions
Six roles are seeded, designed around segregation of duties (the person who records an entry is not the person who approves it):
| Role | Who | Can | Cannot |
|---|---|---|---|
super-admin |
Owner | Everything | Approve its own entries under maker-checker |
admin |
IT / user admin | Users, roles, permissions, companies and who can use them, voucher types | Record, post, approve or close anything |
accountant |
Maker | Record, edit, post (below the approval threshold), reverse, void drafts, close periods, bank & reconciliations, tax, FX rates | Approve, reopen periods, manage users/roles |
approver |
Checker | Approve or reject entries; read ledger & reports | Create, edit or post entries |
auditor |
Internal / external audit | Read everything incl. the audit trail | Change anything |
viewer |
Management | Read ledger & reports | Change anything |
php artisan accounting:roles # who-can-what matrix from the database + SoD check php artisan accounting:roles --config # the shipped defaults
accounting:roles exits non-zero if any role other than super-admin can both create and approve
journal entries — handy as a deployment check.
Safe to customise: change roles in the Settings → Roles screen. Re-running accounting:seed
(e.g. after an upgrade) never removes permissions you changed — it only adds newly introduced ones.
Privilege-escalation protection: a user can only grant roles/permissions they hold themselves, only a
super-admin can manage super-admin users or the super-admin role, changing a user's roles requires
user.assign-role (direct permissions: user.assign-permission), role management requires
accounting.manage-settings.
Full role × permission matrix (defaults)
| Permission | super-admin | admin | accountant | approver | auditor | viewer |
|---|---|---|---|---|---|---|
accounting.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
accounting.manage-settings |
✔ | ✔ | ||||
companies.manage |
✔ | ✔ | ||||
reports.consolidated.view |
✔ | ✔ | ✔ | ✔ | ||
user.view |
✔ | ✔ | ||||
user.create |
✔ | ✔ | ||||
user.update |
✔ | ✔ | ||||
user.delete |
✔ | |||||
user.assign-role |
✔ | ✔ | ||||
user.assign-permission |
✔ | ✔ | ||||
account-types.view |
✔ | ✔ | ✔ | |||
account-types.create |
✔ | |||||
account-types.update |
✔ | |||||
account-types.delete |
✔ | |||||
currencies.view |
✔ | ✔ | ✔ | |||
currencies.create |
✔ | |||||
currencies.update |
✔ | ✔ | ||||
currencies.delete |
✔ | |||||
periods.view |
✔ | ✔ | ✔ | |||
periods.create |
✔ | |||||
periods.update |
✔ | |||||
periods.delete |
✔ | |||||
periods.close |
✔ | ✔ | ||||
periods.reopen |
✔ | |||||
chart-of-accounts.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
chart-of-accounts.create |
✔ | |||||
chart-of-accounts.update |
✔ | |||||
chart-of-accounts.import |
✔ | |||||
chart-of-accounts.restructure |
✔ | |||||
chart-templates.apply |
✔ | |||||
recurring-entries.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
recurring-entries.create |
✔ | ✔ | ||||
recurring-entries.update |
✔ | ✔ | ||||
recurring-entries.delete |
✔ | ✔ | ||||
recurring-entries.run |
✔ | ✔ | ||||
parties.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
parties.create |
✔ | ✔ | ||||
parties.update |
✔ | ✔ | ||||
parties.delete |
✔ | ✔ | ||||
party-documents.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
party-documents.create / update / delete / post / void |
✔ | ✔ | ||||
party-payments.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
party-payments.create / void |
✔ | ✔ | ||||
tax-returns.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
tax-returns.file |
✔ | ✔ | ||||
tax-entries.create |
✔ | ✔ | ||||
budgets.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
budgets.create |
✔ | ✔ | ||||
budgets.update |
✔ | ✔ | ||||
budgets.delete |
✔ | ✔ | ||||
budgets.approve |
✔ | ✔ | ||||
budgets.override |
✔ | |||||
fixed-assets.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
fixed-assets.create |
✔ | ✔ | ||||
fixed-assets.update |
✔ | ✔ | ||||
fixed-assets.delete |
✔ | ✔ | ||||
fixed-assets.depreciate |
✔ | ✔ | ||||
fixed-assets.dispose |
✔ | ✔ | ||||
inventory.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
inventory.manage |
✔ | ✔ | ||||
inventory.move |
✔ | ✔ | ||||
payroll.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
payroll.manage |
✔ | ✔ | ||||
payroll.run |
✔ | ✔ | ||||
payroll.post |
✔ | ✔ | ||||
payroll.void |
✔ | ✔ | ||||
whatsapp.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
whatsapp.send |
✔ | ✔ | ||||
bank-statements.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
bank-statements.import |
✔ | ✔ | ||||
bank-statements.match |
✔ | ✔ | ||||
fx-revaluation.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
fx-revaluation.run |
✔ | ✔ | ||||
fx-revaluation.rates |
✔ | ✔ | ||||
chart-of-accounts.delete |
✔ | |||||
cost-centers.view |
✔ | ✔ | ✔ | |||
cost-centers.create |
✔ | ✔ | ||||
cost-centers.update |
✔ | ✔ | ||||
cost-centers.delete |
✔ | |||||
journal-entries.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
journal-entries.create |
✔ | ✔ | ||||
journal-entries.update |
✔ | ✔ | ||||
journal-entries.delete |
✔ | |||||
journal-entries.post |
✔ | ✔ | ||||
journal-entries.reverse |
✔ | ✔ | ||||
journal-entries.void |
✔ | ✔ | ||||
journal-entries.approve |
✔ | ✔ | ||||
bank-accounts.view |
✔ | ✔ | ✔ | |||
bank-accounts.create |
✔ | ✔ | ||||
bank-accounts.update |
✔ | ✔ | ||||
bank-accounts.delete |
✔ | |||||
reconciliations.view |
✔ | ✔ | ✔ | |||
reconciliations.create |
✔ | ✔ | ||||
reconciliations.update |
✔ | ✔ | ||||
reconciliations.delete |
✔ | |||||
tax-codes.view |
✔ | ✔ | ✔ | ✔ | ||
tax-codes.create |
✔ | ✔ | ||||
tax-codes.update |
✔ | ✔ | ||||
tax-codes.delete |
✔ | |||||
tax-rates.view |
✔ | ✔ | ✔ | ✔ | ||
tax-rates.create |
✔ | ✔ | ||||
tax-rates.update |
✔ | ✔ | ||||
tax-rates.delete |
✔ | |||||
voucher-types.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
voucher-types.create |
✔ | ✔ | ||||
voucher-types.update |
✔ | ✔ | ||||
voucher-types.delete |
✔ | ✔ | ||||
control-accounts.manage |
✔ | ✔ | ||||
report-mapping.manage |
✔ | ✔ | ||||
control-accounts.post-manual |
✔ | |||||
attachments.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
attachments.create |
✔ | ✔ | ||||
attachments.delete |
✔ | ✔ | ||||
account-balance-snapshots.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.general-ledger.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.trial-balance.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
reports.balance-sheet.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
reports.financial-statements.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
reports.income-statement.view |
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
reports.cash-flow.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.aged-receivables.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.aged-payables.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.account-statement.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.account-balances.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.bank-book.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
reports.cash-book.view |
✔ | ✔ | ✔ | ✔ | ✔ | |
audit-logs.view |
✔ | ✔ | ✔ |
Select2 Integration
All <select> elements use Select2 4.1.0 served from public/vendor/accounting/:
public/vendor/accounting/jquery.min.js — jQuery 3.7.1
public/vendor/accounting/select2.min.js — Select2 4.1.0
public/vendor/accounting/select2.min.css — Select2 CSS
Falls back to CDN if assets not published.
Journal entry line dropdowns use Select2 with full Livewire compatibility:
- Destroyed and re-initialized on every
livewire:updatedevent - Native
changeevent fired after Select2 selection to sync with Livewire state
To re-publish:
php artisan vendor:publish --tag=accounting-assets --force
Double-Entry Workflow
| Status | Description |
|---|---|
| Draft | Editable, not included in balances or reports |
| Posted | Locked (immutable in the database), included in balances; the period must be open |
| Void | Cancelled draft — posted entries cannot be voided, they must be reversed |
Reversal: reversing a posted entry posts a mirror entry (debits ↔ credits). Both stay posted and net to
zero (GAAP); the original is flagged via reversed_by_entry_id — use $entry->isReversed() or
JournalEntry::reversed(). A reversal cannot itself be reversed.
Integrity is enforced in layers:
- UI — Save is disabled until debits equal credits.
- Application —
PostJournalEntryActionchecks, in exact cents: ≥ 2 lines, each line either debit or credit, active posting (non-group) accounts, account currency, debits = credits, open period (locked so a concurrent period close cannot interleave; concurrent postings do not block each other). - Database — triggers reject any change to posted entries/lines (amounts, accounts, dates, status, deletes), posting to group accounts, lines on another company's accounts and invalid account trees (see Chart of accounts rules); PostgreSQL also has CHECK constraints (one-sided lines, positive FX rates, single base currency).
Periods — month-end and year-end close
Open Accounting → Periods (React or Blade). Use yearly periods, or click Monthly periods to create
twelve months for a fiscal year (POST /periods/generate-monthly); the last month of the fiscal year is
marked year end.
Close workspace — Month-end close / Year-end close on a period opens a checklist
(GET /periods/{id}/close-checklist[?year_end=1]):
| Check | Blocks closing? |
|---|---|
| No entries waiting for approval | ✕ yes |
| No draft entries dated in the period | ✕ yes |
| Trial balance is balanced | ✕ yes |
| Earlier periods are closed | ⚠ warns at month end, ✕ blocks at year end |
| Bank lines reconciled | ⚠ warns |
| Retained earnings account is set up (year end) | ✕ yes |
Each item links to the screen that fixes it. The year-end workspace also shows the closing entry preview before anything is posted.
- Month-end close (
periods.close) writes balance snapshots (opening, movement, closing), stores the totals and net income, and locks the period against postings. - Year-end close posts a closing entry that brings every income and expense account to zero and moves the
result to retained earnings (
ACCOUNTING_RETAINED_EARNINGS_ACCOUNT_CODE), then closes the period. It uses each account's balance up to the year end, so it works with monthly or yearly periods and sweeps any earlier unclosed years too. - Reopen (
periods.reopen) needs a reason, which is kept in the audit trail. Periods are reopened newest first. Reopening a year-end period reverses its closing entry automatically, so closing it again recalculates the result from the corrected figures. - Periods may not overlap. A period's dates cannot change, and it cannot be deleted, once it contains entries.
Chart of accounts rules
- No cycles; a parent must be a group account of the same account type.
- Once an account has journal lines its code, type, normal balance and group flag are locked (rename or deactivate instead).
- Accounts with children or journal lines cannot be deleted; system accounts cannot be deleted.
- Omitted
is_active/is_groupon update are left unchanged;normal_balancedefaults to the account type's. - Journal lines can only use posting (non-group) accounts of the entry's own company — rejected when the
entry is saved (
422), when it is posted, and by the database.
Enforced by the database too. These rules hold even for writes that bypass the application (raw SQL, imports, other apps on the same database): triggers on MySQL/MariaDB, PostgreSQL and SQLite reject
| Write | Rejected when |
|---|---|
| Insert / update an account | the parent is not a group account of the same type and company; the move creates a cycle |
| Update an account | it has journal lines and its type, normal balance or group flag changes; it is a group with children and becomes a posting account; its type changes away from its children's; it moves to another company |
| Insert / update a journal line | the account belongs to another company than the entry |
| Post a journal entry | a line uses a group account |
The account code is not locked at the database level (lines reference the account id, so history is
kept); the application still locks it for used accounts. php artisan migrate adds the triggers to existing
installs.
Seeding
accounting:seed only creates missing records — it never renames, re-activates or re-rates existing
accounts/currencies and never reopens closed periods. ACCOUNTING_CHART_PRESET=erpnext (default) seeds the ERPNext standard chart;
general seeds the package's industry-neutral chart; school seeds education-specific names (--chart= overrides it for one run). With a base currency other than PKR only the
base currency is seeded.
Security
The package is reviewed against the OWASP top ten for what it controls; see tests/Feature/Accounting/SecurityRoutesTest.php,
SecurityHardeningTest.php and the cross-company test in ApiAccessTest.php.
- Authorisation: every route carries a
can:ability and authentication; a test fails the build if a new route is added without one, or if a GET route changes data. - Company isolation: a record of another company is never served, changed or deleted by id, on any API resource (tested for every route).
- Uploads: attachments are limited to a whitelist of document types, stored on a private disk and downloaded with
nosniff; Excel imports reject DOCTYPE/entity tricks and oversized (zip-bomb) parts. - Exports: CSV cells starting with
=,+,-or@are prefixed so spreadsheets never run them as formulas. - Audit: postings, voids, role and permission changes are written to the audit trail.
A UAT checklist for accountants is in docs/uat.md.
Testing
composer test # Pest on in-memory SQLite DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_DATABASE=coa_test DB_USERNAME=postgres composer test composer analyse # Larastan level 5 composer format:check # Pint
Production checklist
-
php artisan accounting:install --admin-email=…— confirm who holdssuper-admin. - User model has
HasRoles(andHasApiTokensfor the API); keepverifiedemail enforcement on. -
php artisan accounting:rolespasses (no role can both create and approve). - Maker-checker on for material amounts:
ACCOUNTING_APPROVALS_ENABLED=true,ACCOUNTING_APPROVAL_THRESHOLD=…. - A queue worker is running if you use webhooks (
php artisan queue:work). - API behind Sanctum (or your guard) with the rate limit on (
ACCOUNTING_API_RATE_LIMIT). -
php artisan config:cache route:cachein deploys;php artisan accounting:updateafter every upgrade. - Back up before closing a fiscal year; give
periods.reopento as few people as possible. - Multi-company: give each user access only to their companies; review
accounting:roles(roles apply in every company). - Known limitations: customer refunds are not built yet, and large Excel exports are built in memory (not streamed); all companies share one base currency; consolidation does not eliminate intercompany balances; seeded exchange rates are samples — set your own.
FAQ
Q: How do I change the URL from /accounting/ to /settings/?
A: Add ACCOUNTING_ROUTE_PREFIX=settings to .env. Route names stay accounting.* so no view changes needed.
Q: Does this work with Jetstream (Livewire)?
A: Yes. Set ACCOUNTING_UI_DRIVER=blade in .env before accounting:install.
Q: How do I update after a package upgrade?
A: Run php artisan accounting:update.
Q: Can I create GL entries without the UI?
A: Yes — use JournalEntry::record(description, debitCode, creditCode, amount, post: true).
Q: Why do I get a 422 "Posted journal entries are immutable"? A: Posted entries can't be edited or deleted — reverse them and post a corrected entry.
Q: Why is the Save button disabled? A: Debits and credits must be equal before saving. Enter matching amounts in the debit/credit columns.
Q: What is accounting:update?
A: Refreshes package assets, runs new migrations and re-syncs DB objects without touching your config. Run after every composer update.
Q: I only need the API — how do I keep it light?
A: Set ACCOUNTING_UI_DRIVER=api. No web routes, views or Livewire components are loaded; only the API routes.
Q: How do I require a second person to approve entries?
A: ACCOUNTING_APPROVALS_ENABLED=true and a threshold — see Maker-checker approvals. Give makers the accountant role and checkers approver.
Q: The accounting pages have no sidebar/menu in my React app.
A: Add the menu link, and on an Inertia 2 starter kit the small app.tsx addition — see React: add the menu link under Installation.
Q: My XLSX/PDF export returns 422.
A: The report has more rows than the XLSX/PDF limit. Narrow the dates/account, export CSV (unlimited), or raise ACCOUNTING_EXPORT_MAX_XLSX_ROWS / ACCOUNTING_EXPORT_MAX_PDF_ROWS.
Q: Can I run several companies?
A: Yes — ACCOUNTING_MULTI_COMPANY=true, then php artisan accounting:create-company. See Multi-company.
Q: Is Laravel 14 supported? A: Laravel 14 is expected in Q1 2027 and requires PHP 8.4. CI already runs the suite against Laravel's development branch; the version constraint will be widened as soon as 14.0 and the test tooling (Pest, Testbench) are released.
Changelog
See CHANGELOG.md for full version history and Releases for release notes.
Releasing (maintainers): add a ## [x.y.z] - date section to CHANGELOG.md, merge, then push the tag
(git tag vx.y.z && git push origin vx.y.z) — or run Actions → Release → Run workflow with the tag. The
workflow creates the tag if needed and publishes a GitHub Release with that CHANGELOG section; Packagist
picks the tag up automatically.
License
MIT — see LICENSE.
Contact & Support
- Author: Ali Raza Marchal
- Email: kh.marchal@gmail.com
- Issues: github.com/alimarchal/laravel-chart-of-accounts-package/issues
- Packagist: packagist.org/packages/alimarchal/laravel-chart-of-accounts
Keywords
laravel accounting · laravel chart of accounts · laravel double-entry bookkeeping ·
laravel journal entries · laravel general ledger · laravel trial balance ·
laravel balance sheet · laravel income statement · laravel cash flow ·
laravel bank reconciliation · laravel ERP · laravel GAAP · laravel IFRS ·
double entry bookkeeping php · accounting package for laravel · laravel COA ·
laravel multi-currency · laravel financial reports · laravel accounting module










































