Laravel Chart of Accounts




Professional-grade, production-ready 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
| Dimension |
Score |
Notes |
| Architecture |
9.5/10 |
Clean service-layer, Actions pattern, no God classes |
| Double-Entry Correctness |
10/10 |
Balance enforced at DB and application layer + UI |
| API Coverage |
9/10 |
Full versioned REST API with Eloquent resources |
| Frontend Flexibility |
10/10 |
Dual-stack: one env switch (ACCOUNTING_UI_DRIVER) |
| Reporting Depth |
9/10 |
10 financial reports covering all standard statements |
| Security |
9/10 |
Spatie Permission RBAC, route middleware, @can guards |
| Developer Experience |
9.5/10 |
One-command install, JournalEntry::record() helper, Select2 UI |
| Test Coverage |
8/10 |
Feature tests for posting, reversals, voids, edge cases |
| Documentation |
9.5/10 |
Full README, CHANGELOG, PHPDoc, API examples |
- PHP ^8.2
- Laravel ^10.0 | ^11.0 | ^12.0 | ^13.0
composer require alimarchal/laravel-chart-of-accounts
Blade/Livewire apps (Jetstream): add ACCOUNTING_UI_DRIVER=blade to .env before installing.
Inertia/React apps (Breeze): leave default (inertia).
php artisan accounting:install
accounting:install does automatically (11 steps):
- Publishes accounting migrations
- Publishes accounting config (
config/accounting.php)
- Publishes Blade views (
resources/views/vendor/accounting/)
- Publishes public assets — jQuery 3.5.1 + Select2 4.1.0 →
public/vendor/accounting/
- Publishes
spatie/laravel-permission migrations (if not present)
- Publishes
spatie/laravel-activitylog migrations (if not present)
- Runs
php artisan migrate
- Seeds all master data (account types, currencies, COA, permissions, tax codes, periods)
- Syncs database objects (stored procedures, views, triggers)
- Assigns
super-admin role to first user
- Verifies the installation
After install — add HasRoles to your User model:
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Available roles: super-admin (all), admin, accountant, viewer.
After composer update alimarchal/laravel-chart-of-accounts:
php artisan accounting:update
Re-publishes views, assets, config, JS; runs new migrations; syncs DB objects.
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: '5101', // Rent Expense
creditAccountCode: '1101', // Cash
amount: 150000,
post: false, // save as draft
);
// Create and post immediately
$entry = JournalEntry::record(
description: 'Salary payment — June 2026',
debitAccountCode: '6101', // Salaries Expense
creditAccountCode: '1101', // Cash
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. '6101') |
creditAccountCode |
string |
Yes |
Account code for the credit line (e.g. '1101') |
amount |
float |
Yes |
Amount — same value debited AND credited |
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 account code or open period not found.
Create a draft entry:
POST /api/accounting/v1/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/accounting/v1/journal-entries/{id}/post
Authorization: Bearer {token}
Reverse entry:
POST /api/accounting/v1/journal-entries/{id}/reverse
Authorization: Bearer {token}
Content-Type: application/json
{ "description": "Reversal of SAL-2026-06" }
Void entry:
POST /api/accounting/v1/journal-entries/{id}/void
Authorization: Bearer {token}
List with filters:
GET /api/accounting/v1/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
Base URL: /api/accounting/v1
| Method |
Endpoint |
Description |
| GET/POST |
/account-types |
List / Create |
| GET/PUT/DELETE |
/account-types/{id} |
Show / Update / Delete |
| GET/POST |
/chart-of-accounts |
List / Create |
| GET/PUT/DELETE |
/chart-of-accounts/{id} |
Show / Update / Delete |
| GET/POST |
/currencies |
List / Create |
| GET |
/accounting-periods |
List periods |
| GET/POST |
/journal-entries |
List / Create |
| GET |
/journal-entries/{id} |
Show with lines |
| PUT |
/journal-entries/{id} |
Update draft |
| POST |
/journal-entries/{id}/post |
Post draft |
| POST |
/journal-entries/{id}/void |
Void |
| POST |
/journal-entries/{id}/reverse |
Reverse |
| GET/POST |
/reconciliations |
List / Create |
| GET |
/bank-accounts |
List |
| GET |
/cost-centers |
List |
| GET |
/tax-codes |
List |
| GET |
/tax-rates |
List |
| GET |
/account-balance-snapshots |
Period-end snapshots |
All list endpoints support ?filter[field]=value, ?sort=field, ?page=N.
php artisan vendor:publish --tag=accounting-config
// config/accounting.php
return [
'ui_driver' => env('ACCOUNTING_UI_DRIVER', 'inertia'), // 'inertia' or 'blade'
'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'),
'middleware' => ['web', 'auth'],
'use_permissions' => true,
'defaults' => [
'currency_code' => env('ACCOUNTING_BASE_CURRENCY', 'PKR'),
'cash_account_code' => env('ACCOUNTING_CASH_ACCOUNT_CODE', '1101'),
'bank_account_code' => env('ACCOUNTING_BANK_ACCOUNT_CODE', '1102'),
'retained_earnings_account_code' => env('ACCOUNTING_RETAINED_EARNINGS_ACCOUNT_CODE', '3101'),
'rounding_account_code' => env('ACCOUNTING_ROUNDING_ACCOUNT_CODE', '5201'),
],
];
| Command |
Description |
accounting:install |
Full setup: publish all assets, migrate, seed, sync, verify |
accounting:update |
Re-publish assets + sync DB after package upgrade |
accounting:seed |
Seed account types, currencies, COA, permissions, periods |
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 the current accounting period |
accounting:open-period |
Open a new accounting period |
| Model |
Table |
Description |
AccountType |
accounting_account_types |
Asset, Liability, Equity, Revenue, Expense |
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/reversed) |
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 |
| Permission |
Description |
accounting.view |
View all accounting screens |
accounting.manage-settings |
Manage roles, users, periods |
account-types.view/create/update/delete |
Account type CRUD |
currencies.view/create/update/delete |
Currency CRUD |
periods.view/create/update/delete/close/reopen |
Period management |
chart-of-accounts.view/create/update/delete |
COA CRUD |
cost-centers.view/create/update/delete |
Cost center CRUD |
journal-entries.view/create/update/delete/post/reverse/void |
Journal entry workflow |
bank-accounts.view/create/update/delete |
Bank account CRUD |
reconciliations.view/create/update/delete |
Reconciliation CRUD |
tax-codes.view/create/update/delete |
Tax code CRUD |
tax-rates.view/create/update/delete |
Tax rate CRUD |
account-balance-snapshots.view |
View balance snapshots |
reports.*.view |
View individual reports (GL, TB, BS, IS, CF, AR, AP, BB, CB, AB) |
audit-logs.view |
View audit trail |
user.view/create/update/delete/assign-role/assign-permission |
User management |
Roles: super-admin (all), admin, accountant, viewer.
All <select> elements use Select2 4.1.0 served from public/vendor/accounting/:
public/vendor/accounting/jquery.min.js — jQuery 3.5.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:updated event
- Native
change event fired after Select2 selection to sync with Livewire state
To re-publish:
php artisan vendor:publish --tag=accounting-assets --force
| Status |
Description |
| Draft |
Editable, not in balances |
| Posted |
Locked, balances updated, period must be open |
| Reversed |
Counter-entry with swapped debits/credits (GAAP method) |
| Voided |
Cancelled without counter-entry |
Balance is enforced at three layers:
- UI — Save button disabled until balanced; real-time status badge
- Application —
save() rejects if |debits − credits| ≥ 0.01
- Service —
JournalEntryService::post() validates before posting
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 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: Re-publishes views, assets, config with --force, runs new migrations, syncs DB objects. Run after every composer update.
See CHANGELOG.md for full version history.
MIT — see LICENSE.
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