Download the PHP package ringlesoft/laravel-accounting without Composer
On this page you can find all versions of the php package ringlesoft/laravel-accounting. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download ringlesoft/laravel-accounting
More information about ringlesoft/laravel-accounting
Files in ringlesoft/laravel-accounting
Package laravel-accounting
Short Description A double-entry accounting engine for Laravel - attach any model via a trait, post through templates or raw vouchers, and get balanced, immutable journal entries with multi-currency, multi-tenancy, fiscal periods, and financial reports built in.
License MIT
Homepage https://github.com/ringlesoft/laravel-accounting
Informations about the package laravel-accounting
Laravel Accounting
PHP 8.1+, Laravel: 10+
A double-entry accounting engine for Laravel.
Turn any model to a source of accounting transactions. Attach a Postable trait, map it to an AccountingTemplate, and
posting produces a balanced, immutable journal entry.
- Double-entry bookkeeping — every transaction maintains balanced debits and credits
- Immutable records — posted entries can't be edited, only reversed or voided
- Template-based posting — 39 ready-made templates for common transactions (sales, purchases, payroll, depreciation, loans, equity...), or easily write your own
- Multi-currency, multi-tenancy, and fiscal period support
- Reports: Trial Balance, Balance Sheet, Income Statement, Cash Flow Statement, General Ledger, Account Ledger, Chart of Accounts
Project status: Fully functional and under active development, not yet 1.0 (still improving based on feedback).
Table of Contents
- Installation
- Quick Start
- Diving Deeper
- Core Concepts
- Accounts & Account Presets
- Journal Entries, Journal Entry Items & Voucher Data
- Accounting Templates
- The Postable Trait
- Posting Transactions
- Reversing and Voiding
- Fiscal Periods
- Multi-Currency & Multi-Tenancy
- Reports
- Configuration
- Manual Installation and Resources Publishing
- Troubleshooting & FAQ
- API Reference
- Core Concepts
- Testing
- Contributing
- License
Installation
-
Install using composer
- Run the installer: it publishes the config, migrations and seeders, offers to run migrations, and offers to seed account types + a full chart of accounts + fiscal periods for the current year:
Quick Start
Option A. Using PHP attributes
Implement the PostableModel interface and the getPostableAmount() method. Apply the #[DefaultAccountingTemplate]
attribute
Now you can post your model
Option B. Manual Template assigning
Post with an explicit template and overridden accounts instead of the default attribute:
Note: Accounts are never posted to a parent.
Diving Deeper
Core Concepts
The mental model is a straight line, and each link below is explained in its own subsection:
- An Account, identified by an Account Preset so you never hardcode IDs, is where money lives.
- An Accounting Template describes the debit/credit shape of a transaction type (e.g. a credit sale debits Accounts Receivable and credits Sales Revenue).
- Your own Eloquent model becomes a transaction source via the Postable trait, which builds Voucher Data from the template and posts it as an immutable Journal Entry (made up of balanced Journal Entry Items).
- Mistakes are fixed by reversing or voiding, never editing.
- Posting only succeeds inside an open Fiscal Period.
- Every account and entry automatically respects multi-currency and multi-tenancy.
- Reports read the ledger back out as Trial Balance, Balance Sheet, Income Statement, etc.
Accounts & Account Presets
This package ships with a complete Chart of Accounts (COA) that you can modify and extend for your own needs. Accounts can be nested (a parent category with child accounts), but accounts are never posted to directly if they have children — posting always targets a specific leaf account.
To avoid hardcoding database IDs or names, every account can carry an Account Preset: an enum of ~250 predefined
roles (CASH, ACCOUNTS_RECEIVABLE, SALES_REVENUE, ...) that templates and your own code refer to instead of a
concrete account. This is what lets an AccountingTemplate be defined once and still resolve to the correct account in
any application's chart of accounts.
Journal Entries, Journal Entry Items & Voucher Data
A Journal Entry plus its balanced debit/credit Journal Entry Items are the immutable ledger components — posted entries can only be reversed or voided, never edited.
Before a Journal Entry exists, a template (or your own code) builds a Voucher Data object: an intermediate DTO
that standardizes how debit/credit lines are described. Journal Entries only ever accept lines in this format, which
is why both posting paths below — a template, or a hand-built VoucherData — end up going through the same
createDraft()/post() pipeline.
Accounting Templates
An accounting template encapsulates the debit/credit pattern for a transaction type. It supports any number of lines
per side, optional conditional lines (e.g. tax), and dynamic amounts via closures.
The package comes with 39 prebuilt accounting templates that cover most of the well known accounting operations. You
can always create yours on the fly, or by extending the AccountingTemplate class — see Creating Custom Templates
in docs/templates-api.md.
The Postable Trait
This is the trait that turns your model into a source of a transaction in your ledger. This opinionated approach to
ledger posting makes the accounting abstraction even more valuable by eliminating the need to manually write posting
code for every type of transaction.
The trait gives any Eloquent model post(), toVoucher(), postWithTemplate(), isPosted(), journalEntry(), and
expects you to override getPostableAmount(), which is the trusted source of what amount is posted (see the
Quick Start above for a minimal example).
Posting Transactions
Once a model uses Postable (or you'd rather skip models entirely), there are several ways to actually post:
Via a template directly
Via PostingService
For common transactions, PostingService wraps the right template plus createDraft() + post() in one call:
See the PostingService Method column in docs/templates-api.md for every available method.
Two-step posting (review before posting)
Resolving ambiguous accounts (children of a preset)
Sometimes a preset (e.g. COST_OF_GOODS_SOLD) resolves to an account that has multiple children under it. Since
accounts are never posted to a parent, posting then fails with ParentAccountPostingException — the package refuses to
guess which child you meant. There are two ways to resolve it:
-
Pass the template → account mapping as you instantiate the template:
- Implement
ResolvesAccountPresetson the postable model and return the mapping account viaresolveAccountForPreset():
See docs/templates-api.md for the full catalog of 39 preset templates (sales, purchases, payroll, depreciation, loans, equity, inventory, bad debt).
Reversing and Voiding
Posted entries are immutable — correct them by reversing (creates an offsetting entry, full audit trail) rather than editing:
void() only applies to a draft, unposted entry (cancelling it before it's ever posted) — once posted,
canBeVoided() returns false and you must reverse() instead.
Fiscal Periods
Posting validates the fiscal period is open; closing validates every entry in it is posted and the trial balance is in balance.
Closing entries
PeriodService::closePeriod() doesn't post anything by itself — it just validates and locks. To actually zero out
Revenue/Expense into Retained Earnings before closing a period (the step that was missing before, and why the Balance
Sheet wouldn't balance at year-end), use ClosingService:
Each of the four steps (close Revenue, close Expenses, close Income Summary → Retained Earnings, close Dividends →
Retained Earnings) also has its own previewCloseX()/commitCloseX() pair if you want to run them independently rather
than all at once — e.g. commitCloseRevenue($period). Every step reads the ledger's actual current state, not a cached
calculation, so calling them in any order, or calling the same one twice, is safe: a step with nothing left to close
just returns null. Requires INCOME_SUMMARY and RETAINED_EARNINGS accounts to be seeded (throws
AccountNotFoundException if not); a missing DIVIDENDS_DECLARED account just means that step is skipped, since most
businesses never declare dividends at all. Must run before PeriodService::closePeriod(), since posting is rejected
once a period is closed.
Scheduling period closing
php artisan accounting:close-period wraps the two steps above (ClosingService::commit() then
PeriodService::closePeriod()) into one command built to run unattended: with no options it finds every open
fiscal period whose end_date has already passed and closes each one, continuing past any individual failure rather
than aborting the whole run.
It exits 0 only if every period it processed closed successfully, and 1 if at least one failed (e.g. an
unbalanced trial balance, or an unposted draft entry still sitting in that period) — check the exit code if you wire
alerting around it. A period that's already closed is reported and skipped rather than erroring.
Schedule it like any other command — in routes/console.php (Laravel 11+):
or in app/Console/Kernel.php's schedule() method on earlier Laravel versions:
Running it daily (rather than exactly on the period boundary) is deliberate: a period only closes once its end_date
has passed, so a daily run just means it closes the morning after the period actually ends, whenever that falls.
Since this runs outside an authenticated context, it is not scoped to a single tenant by the usual global scope
(see Multi-Currency & Multi-Tenancy below) — one scheduled run naturally closes the
due period for every tenant, which is normally what you want; pass --tenant= if you need to isolate it to one.
Multi-Currency & Multi-Tenancy
Multi-tenancy isn't a toggle — any model using the MultiTenant trait (Account, JournalEntry, FiscalYear, FiscalPeriod,
AccountBalance all do) is always scoped to a tenant_id column via a global scope. multi_tenancy_field only controls
which attribute is read off the authenticated user to populate it on create (defaults to tenant_id); if there's no
authenticated user (queues, CLI) it's left unset.
By default (strict_multi_tenancy_check => true), every record must belong to exactly one resolvable tenant: a row with
a null tenant_id is never visible to an authenticated tenant, and creating a record with no resolvable tenant_id (no
authenticated user, and none set explicitly) throws TenantRequiredException.
Set it to false to allow genuinely global/shared records instead — null-tenant rows become visible to every tenant,
and can be created without a resolvable tenant (e.g. from a seeder or console command) without being rejected.
Recommended is true whenever using multi-tenancy.
Reports
This package generates all the standard (IFRS) reports for accounting systems — made possible by the opinionated
structure of the COA and the use of account presets. Every report goes through
LaravelAccounting::reports()->{reportType}($params), which returns an already-generated report - call ->getData()
(or ->toArray()/->toJson()) directly. $params keys are snake_case and vary per report:
| Report | Key params |
|---|---|
| Trial Balance | as_of_date |
| Balance Sheet | as_of_date |
| Income Statement | start_date, end_date |
| Cash Flow Statement | start_date, end_date |
| General Ledger | start_date, end_date, optional account_id |
| Account Ledger | account_id (required), start_date, end_date |
| Chart of Accounts | include_inactive, account_type, show_balances, as_of_date |
Results are cached per report_cache_ttl, keyed by report type + params, and invalidated automatically whenever a
journal entry posts, reverses, or voids - LaravelAccounting::reports()->clearCache($reportType) clears it manually if
needed. Calling ->generate() again on a returned report is safe but recomputes live and bypasses the cache for that
call, so there's no need to.
Configuration
The installer (or a manual vendor:publish, see below) publishes config/laravel_accounting.php into your Laravel
application:
Posting fails with a validation error ("the fiscal period id field is required") if no fiscal period covers today — make sure
FiscalPeriodSeederhas run, or create one viaLaravelAccounting::periods()->createPeriodsForYear($fiscalYear).
Manual Installation and Resources Publishing
If you don't want to run the install command, you can manually publish package resources. This is what the install
command does in the background:
Troubleshooting & FAQ
"Failed to post to journal. The fiscal period id field is required."
No fiscal period covers the transaction date. Seed one (FiscalPeriodSeeder) or create it directly:
LaravelAccounting::periods()->createPeriodsForYear($fiscalYear).
ArgumentCountError when calling TemplateRegistry::make('key') or YourTemplate::getPostableAccounts()
Both call new $class() with zero arguments. Every constructor parameter on a template needs a default value — see
Gotchas in docs/templates-api.md.
ParentAccountPostingException
A preset (e.g. COST_OF_GOODS_SOLD, ACCOUNTS_RECEIVABLE) resolved to a parent/category account with more than one
child, so the package refuses to guess which one to post to — accounts are never posted to a parent. Pick one with
resolveAccount(), or implement ResolvesAccountPresets on the model —
see Posting Transactions above.
AccountNotFoundException
No seeded account carries the preset a template needs. Check your chart of accounts, or verify with
YourTemplate::getPostableAccounts().
JournalNotBalancedException
Debits ≠ credits. Common causes: a template's configure() was called twice (entries only ever append, never replace —
never call ->configure() yourself), or a closure-based amount produced an unexpected value.
Why doesn't my Balance Sheet balance at year-end?
Run LaravelAccounting::closing()->commit($period) before closing the period — see Closing entries
under Fiscal Periods above. Nothing runs this automatically; PeriodService::closePeriod() won't do
it for you. BalanceService::calculateUnclosedEarnings() still exists as a lower-level primitive if you need the
unclosed-earnings figure without actually posting anything.
Is multi-tenancy optional?
No — it's not a config toggle. Any model using the MultiTenant trait (Account, JournalEntry, FiscalYear,
FiscalPeriod, AccountBalance) is always scoped to tenant_id via a global scope. multi_tenancy_field only
controls which attribute is read off the authenticated user to populate it.
The tenant scope relies on Auth::user() — what happens in a queued job or artisan command, where there's no
authenticated user?
No tenant filter is applied at all in that context — the global scope only ever narrows results to the authenticated
user's own tenant; it never fails open onto other tenants' data. Under the default strict check it never matches a
null-tenant_id row either. But without an authenticated user, there's nothing for it to scope to, so a queued job or
console command operating across tenants is your responsibility to scope explicitly (e.g.
Account::where('tenant_id', $tenantId)->get(), or set tenant_id yourself before saving). Don't assume the package
will scope non-HTTP code for you.
I want some accounts/data shared across every tenant (e.g. a default chart of accounts), not owned by just one.
Set strict_multi_tenancy_check to false. A record with no resolvable tenant — created explicitly with
tenant_id = null, or from a seeder/console command with no authenticated user — is then allowed to exist and becomes
visible to every tenant, instead of being rejected at creation and invisible everywhere except to whoever created it
outside an authenticated context. Tenant-owned rows (a real tenant_id set) are still isolated from each other exactly
as before — this only changes how null-tenant rows behave.
Can I use cash-basis instead of accrual accounting?
The cash_basis config flag is informational only — it isn't enforced anywhere. All shipped templates post on an
accrual basis (revenue/expense recognized when earned/incurred, not when cash moves). See Accounting Basis: Accrual vs
Cash in docs/templates-api.md before building cash-basis behavior yourself.
UUIDs or auto-incrementing IDs?
A one-time choice at install (config('laravel_accounting.use_uuid')). Switching after you have real data requires a
manual data migration — decide before going to production.
How do I write my own template?
See Creating Custom Templates in docs/templates-api.md — covers registration,
validate(), testing it without a database, and common gotchas.
Does the package handle authorization (who's allowed to post, close a period, etc.)?
No — that's left entirely to your application. There are no policy classes or permission checks anywhere in the package;
wrap calls into PostingService/PeriodService/JournalService with your own authorization logic (a policy, a
middleware, a form request) before invoking them.
API Reference
Facade
Models
Account, AccountType, AccountBalance (cached per-period balances), JournalEntry, JournalEntryItem,
FiscalYear, FiscalPeriod, ExchangeRate.
Services
JournalService (create/validate/post/reverse/void), AccountService (CRUD, hierarchy, preset lookup),
BalanceService (balance calculation, trial balance, net income), ReportService, PostingService (named transaction
shortcuts), PeriodService (open/close/lock fiscal periods), ClosingService (preview/commit period-end closing
entries).
Key Exceptions
All extend LaravelAccountingException: AccountNotFoundException, AccountInactiveException,
ParentAccountPostingException (preset resolves to a parent/category account with no unambiguous child),
JournalNotBalancedException, DuplicatePostingException, PeriodClosedException, InvalidVoucherException,
TemplateNotFoundException. See src/Exceptions/ for the full list.
Testing
Contributing
Let me pave the way, I'll let you know when you can contribute.
Security
If you discover any security-related issues, please email [email protected] or use the issue tracker.
Credits
- David Ringle - Developer
License
The MIT License (MIT). Please see License File for more information.
Support
-
Issues: https://github.com/ringlesoft/laravel-accounting/issues
Contacts
Follow me on X: @ringunger
Email me: [email protected]
Website: https://github.com/ringlesoft/laravel-accounting
All versions of laravel-accounting with dependencies
illuminate/contracts Version ^10.0|^11.0|^12.0|^13.0
illuminate/database Version ^10.0|^11.0|^12.0|^13.0
illuminate/routing Version ^10.24|^11.0|^12.0|^13.0