PHPackages                             wayum999/laravel-accounting - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. wayum999/laravel-accounting

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

wayum999/laravel-accounting
===========================

Laravel Double-Entry Accounting Package with proper debit/credit rules, General Journal, General Ledger, and non-posting transactions

v1.0.0(2mo ago)00MITPHPPHP ^8.2|^8.3|^8.4|^8.5

Since Feb 26Pushed 2mo agoCompare

[ Source](https://github.com/wayum999/laravel-accounting)[ Packagist](https://packagist.org/packages/wayum999/laravel-accounting)[ RSS](/packages/wayum999-laravel-accounting/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (6)Versions (4)Used By (0)

Laravel Accounting
==================

[](#laravel-accounting)

A double-entry accounting package for Laravel. Built on proper accounting principles with immutable ledger entries, running balances, draft transaction support, and financial reporting.

Contents
--------

[](#contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Core Concepts](#core-concepts)
- [Chart of Accounts](#chart-of-accounts)
- [Attaching Accounts to Models](#attaching-accounts-to-models)
- [Recording Transactions](#recording-transactions)
- [Double-Entry Transactions](#double-entry-transactions)
- [Draft Transactions](#draft-transactions)
- [Journal Entries](#journal-entries)
- [Reversals and Voids](#reversals-and-voids)
- [Financial Reports](#financial-reports)
- [Immutability](#immutability)
- [API Reference](#api-reference)
- [Exceptions](#exceptions)
- [Testing](#testing)
- [Migrating from a Journal-Based Accounting Model](#migrating-from-a-journal-based-accounting-model)

---

Requirements
------------

[](#requirements)

RequirementVersionPHP8.2+Laravel12.xDatabasePostgreSQL, MySQL, SQLite, or SQL Server---

Installation
------------

[](#installation)

### 1. Install via Composer

[](#1-install-via-composer)

```
composer require wayum999/laravel-accounting
```

The service provider is automatically discovered. No manual registration is required.

### 2. Publish and Run Migrations

[](#2-publish-and-run-migrations)

```
php artisan vendor:publish --provider="App\Accounting\Providers\AccountingServiceProvider"
php artisan migrate
```

This creates the following tables:

TablePurpose`accounting_accounts`Chart of accounts with type, sub-type, and polymorphic ownership`accounting_journal_entries`Groups of balanced ledger entries (UUID primary key)`accounting_ledger_entries`Individual debit/credit entries with running balances---

Core Concepts
-------------

[](#core-concepts)

### Double-Entry Accounting

[](#double-entry-accounting)

Every financial transaction is recorded as at least one debit and one credit of equal amounts. The `TransactionBuilder` enforces this rule and throws `UnbalancedTransactionException` if you attempt to commit an unbalanced transaction.

### The Seven Account Types

[](#the-seven-account-types)

TypeBalance TypeIncreases WithExamples`ASSET`Debit BalanceDebitCash, Accounts Receivable, Inventory`LIABILITY`Credit BalanceCreditAccounts Payable, Loans Payable`EQUITY`Credit BalanceCreditOwner's Equity, Retained Earnings`REVENUE`Credit BalanceCreditSales Revenue, Service Fees`EXPENSE`Debit BalanceDebitSalaries, Rent, Cost of Goods Sold`OTHER_INCOME`Credit BalanceCreditGain on Sale of Assets, Insurance Recoveries`OTHER_EXPENSE`Debit BalanceDebitLoss on Sale of Assets, Lawsuit Settlements`OTHER_INCOME` and `OTHER_EXPENSE` are used for non-operating items that appear below operating income on the income statement.

### Account Sub-Types

[](#account-sub-types)

Accounts are further classified by sub-type, following the QuickBooks model. Sub-types control how accounts are grouped in financial reports:

Parent TypeSub-TypesAsset`BANK`, `ACCOUNTS_RECEIVABLE`, `OTHER_CURRENT_ASSET`, `INVENTORY`, `FIXED_ASSET`, `OTHER_ASSET`Liability`ACCOUNTS_PAYABLE`, `CREDIT_CARD`, `OTHER_CURRENT_LIABILITY`, `LONG_TERM_LIABILITY`Equity`OWNERS_EQUITY`, `RETAINED_EARNINGS`Revenue`REVENUE`, `OTHER_INCOME`Other Income`GAIN_ON_SALE`, `OTHER_GAIN`Expense`COST_OF_GOODS_SOLD`, `OPERATING_EXPENSE`Other Expense`LOSS_ON_SALE`, `OTHER_LOSS`, `OTHER_EXPENSE````
use App\Accounting\Enums\AccountType;
use App\Accounting\Enums\AccountSubType;

// Get all sub-types for a given type
$assetSubTypes = AccountSubType::forType(AccountType::ASSET);

// Sub-type metadata
AccountSubType::BANK->parentType();   // AccountType::ASSET
AccountSubType::BANK->reportGroup();  // "Current Assets"
AccountSubType::BANK->isCurrent();    // true
AccountSubType::BANK->label();        // "Bank"
```

### Money Handling

[](#money-handling)

All monetary amounts are stored as **integer cents** to avoid floating-point rounding errors. This package uses [moneyphp/money](https://github.com/moneyphp/money) internally. Dollar convenience methods accept float values and handle the conversion automatically.

### Immutable Ledger

[](#immutable-ledger)

Ledger entries are **immutable after creation**. They cannot be updated or deleted. This is standard accounting practice — to correct an error, create a reversing journal entry. See [Immutability](#immutability) for details.

### Running Balances

[](#running-balances)

Each posted ledger entry stores a `running_balance` computed at insert time. This represents the cumulative account balance after that entry, respecting the account's normal balance direction.

---

Chart of Accounts
-----------------

[](#chart-of-accounts)

### Using the Seeder

[](#using-the-seeder)

The `ChartOfAccountsSeeder` provides three built-in templates:

```
use App\Accounting\Services\ChartOfAccountsSeeder;

// Minimal template (5 core accounts)
ChartOfAccountsSeeder::seed();

// Service business template (12 accounts)
ChartOfAccountsSeeder::seed('service');

// Retail business template (14 accounts, includes inventory and COGS)
ChartOfAccountsSeeder::seed('retail');

// Custom currency
ChartOfAccountsSeeder::seed('minimal', 'EUR');
```

The seeder is idempotent — running it again will not create duplicate accounts.

### Creating Accounts Manually

[](#creating-accounts-manually)

```
use App\Accounting\Models\Account;
use App\Accounting\Enums\AccountType;
use App\Accounting\Enums\AccountSubType;

$cash = Account::create([
    'name' => 'Operating Cash',
    'code' => '1000',
    'type' => AccountType::ASSET,
    'sub_type' => AccountSubType::BANK,
    'currency' => 'USD',
]);

$revenue = Account::create([
    'name' => 'Sales Revenue',
    'code' => '4000',
    'type' => AccountType::REVENUE,
    'sub_type' => AccountSubType::REVENUE,
    'currency' => 'USD',
]);
```

### Parent-Child Accounts

[](#parent-child-accounts)

Accounts support hierarchical nesting via `parent_id`:

```
$parentAsset = Account::create([
    'name' => 'Current Assets',
    'code' => '1000',
    'type' => AccountType::ASSET,
]);

$cash = Account::create([
    'name' => 'Cash',
    'code' => '1010',
    'type' => AccountType::ASSET,
    'sub_type' => AccountSubType::BANK,
    'parent_id' => $parentAsset->id,
]);

$parentAsset->children; // Collection of child accounts
$cash->parent;          // The parent account
```

### Custom Templates

[](#custom-templates)

```
use App\Accounting\Enums\AccountSubType;
use App\Accounting\Enums\AccountType;

ChartOfAccountsSeeder::seedFromTemplate([
    [
        'name' => 'Operating Cash',
        'code' => '1000',
        'type' => AccountType::ASSET,
        'sub_type' => AccountSubType::BANK,
    ],
    [
        'name' => 'Client Revenue',
        'code' => '4000',
        'type' => AccountType::REVENUE,
        'sub_type' => AccountSubType::REVENUE,
    ],
]);
```

---

Attaching Accounts to Models
----------------------------

[](#attaching-accounts-to-models)

Any Eloquent model can own accounting accounts via the `HasAccounting` trait.

```
use App\Accounting\Traits\HasAccounting;

class Customer extends Model
{
    use HasAccounting;
}
```

### Creating Accounts for a Model

[](#creating-accounts-for-a-model)

```
$customer = Customer::find(1);

$account = $customer->createAccount(
    name: 'Accounts Receivable',
    type: AccountType::ASSET,
    code: 'AR-001',
    currency: 'USD',
    subType: AccountSubType::ACCOUNTS_RECEIVABLE,
);
```

### Retrieving Accounts

[](#retrieving-accounts)

```
// Get all accounts
$customer->accounts();

// Get a specific account by name
$customer->account('Accounts Receivable');

// Get the first account (or null)
$customer->account();
```

> Calling `createAccount` with a duplicate name on the same model throws `AccountAlreadyExistsException`.

---

Recording Transactions
----------------------

[](#recording-transactions)

### TransactionBuilder (Recommended)

[](#transactionbuilder-recommended)

The `TransactionBuilder` is the primary way to record transactions. It enforces double-entry balance, wraps everything in a database transaction, and creates a `JournalEntry` with linked `LedgerEntry` records.

```
use App\Accounting\Services\TransactionBuilder;

// Record a sale: debit Cash, credit Revenue
$journalEntry = TransactionBuilder::create()
    ->date('2025-01-15')
    ->memo('Invoice #1042')
    ->reference('INV-1042')
    ->debit($cash, 120000)      // $1,200.00 in cents
    ->credit($revenue, 120000)
    ->commit();
```

### Dollar Amounts

[](#dollar-amounts)

```
$journalEntry = TransactionBuilder::create()
    ->debitDollars($cash, 1200.00)
    ->creditDollars($revenue, 1200.00)
    ->commit();
```

### Money Objects

[](#money-objects)

```
use Money\Money;
use Money\Currency;

$amount = new Money(150000, new Currency('EUR'));

$journalEntry = TransactionBuilder::create()
    ->debit($euroCash, $amount)
    ->credit($euroRevenue, $amount)
    ->commit();
```

### Multi-Line Transactions

[](#multi-line-transactions)

A single transaction can have any number of entries as long as total debits equal total credits:

```
// Purchase equipment: pay some cash, put the rest on credit
$journalEntry = TransactionBuilder::create()
    ->memo('Office server purchase')
    ->debit($equipment, 300000)       // $3,000 asset increase
    ->credit($cash, 100000)           // $1,000 cash payment
    ->credit($accountsPayable, 200000) // $2,000 on account
    ->commit();
```

### Increase and Decrease

[](#increase-and-decrease)

When you don't want to think about debits and credits, use `increase()` and `decrease()`. These auto-select the correct side based on the account type:

```
$journalEntry = TransactionBuilder::create()
    ->increase($cash, 50000)        // Asset → debit
    ->increase($revenue, 50000)     // Revenue → credit
    ->commit();

$journalEntry = TransactionBuilder::create()
    ->decrease($cash, 20000)        // Asset → credit
    ->increase($expense, 20000)     // Expense → debit
    ->commit();
```

Account Type`increase()``decrease()`Asset, Expense, Other Expense (debit balance)DebitCreditLiability, Equity, Revenue, Other Income (credit balance)CreditDebit### Referencing Models

[](#referencing-models)

Attach any Eloquent model to individual entries via the polymorphic `ledgerable` relationship:

```
$invoice = Invoice::find(42);

$journalEntry = TransactionBuilder::create()
    ->debit($accountsReceivable, 120000, 'Invoice payment', $invoice)
    ->credit($revenue, 120000)
    ->commit();
```

### Per-Entry Memos

[](#per-entry-memos)

Each entry can have its own memo. Entries without a memo inherit the transaction-level memo:

```
$journalEntry = TransactionBuilder::create()
    ->memo('Monthly payroll')
    ->debit($salaryExpense, 500000, 'Salary - John')
    ->debit($salaryExpense, 450000, 'Salary - Jane')
    ->credit($cash, 950000) // inherits "Monthly payroll"
    ->commit();
```

### Inspecting Pending Entries

[](#inspecting-pending-entries)

```
$builder = TransactionBuilder::create()
    ->debit($cash, 5000)
    ->credit($revenue, 5000);

$pending = $builder->getPendingEntries();
// Array of ['account' => ..., 'debit' => ..., 'credit' => ..., ...]
```

### Standalone Account Methods

[](#standalone-account-methods)

> **Deprecated:** These methods create ledger entries without a parent `JournalEntry`, which orphans them from the journal and breaks the audit trail. They also bypass the double-entry invariant. Use `TransactionBuilder` instead.

```
// DEPRECATED — Use TransactionBuilder
$cash->debit(50000);
$cash->credit(50000);
$cash->debitDollars(500.00);
$cash->creditDollars(500.00);
$cash->increase(50000);
$cash->decrease(20000);
```

**Preferred alternative using TransactionBuilder:**

```
// Record a payment received (debit cash, credit revenue)
TransactionBuilder::create()
    ->memo('Customer payment')
    ->debit($cash, 50000)
    ->credit($revenue, 50000)
    ->commit();

// Using increase/decrease (auto-selects correct side)
TransactionBuilder::create()
    ->increase($cash, 50000)    // Asset → debit
    ->increase($revenue, 50000) // Revenue → credit
    ->commit();
```

---

Double-Entry Transactions
-------------------------

[](#double-entry-transactions)

The `TransactionBuilder` creates a `JournalEntry` (the header) with linked `LedgerEntry` records (the lines). Each journal entry has a UUID primary key.

```
JournalEntry (UUID)
├── LedgerEntry: Cash          DR 1,200.00
└── LedgerEntry: Revenue                    CR 1,200.00

```

### Checking Balances

[](#checking-balances)

```
// Current balance (Money object, in cents)
$balance = $account->getBalance();

// Balance in dollars
$dollars = $account->getBalanceInDollars();

// Balance as of a specific date
$balance = $account->getBalanceOn(Carbon::parse('2024-06-30'));

// Cached balance (from the `cached_balance` column, auto-maintained)
$balance = $account->balance; // Money object

// Daily activity
$debited  = $account->getDollarsDebitedToday();
$credited = $account->getDollarsCreditedToday();
$debited  = $account->getDollarsDebitedOn(Carbon::parse('2024-06-30'));
$credited = $account->getDollarsCreditedOn(Carbon::parse('2024-06-30'));
```

All balance methods automatically exclude unposted (draft) entries.

---

Draft Transactions
------------------

[](#draft-transactions)

Draft transactions allow you to record entries without affecting account balances or financial reports. This is useful for pending invoices, unapproved expenses, or any transaction that needs review before posting.

### Creating a Draft

[](#creating-a-draft)

```
$journalEntry = TransactionBuilder::create()
    ->draft()
    ->memo('Pending invoice #2001')
    ->debit($accountsReceivable, 250000)
    ->credit($revenue, 250000)
    ->commit();

$journalEntry->is_posted; // false
```

Draft entries:

- Have `is_posted = false` on both the JournalEntry and its LedgerEntries
- Do **not** affect account balances (`cached_balance` is unchanged)
- Do **not** appear in financial reports (TrialBalance, BalanceSheet, etc.)
- Have a `running_balance` of `0`

### Posting a Draft

[](#posting-a-draft)

```
$journalEntry->post();

$journalEntry->is_posted; // true
// Account balances and running balances are now computed
```

Posting recalculates the `running_balance` for each ledger entry and updates the affected accounts' `cached_balance`.

### Unposting a Transaction

[](#unposting-a-transaction)

```
$journalEntry->unpost();

$journalEntry->is_posted; // false
// Account balances are recalculated to exclude these entries
```

Both `post()` and `unpost()` are idempotent — calling them when already in that state is a no-op.

---

Journal Entries
---------------

[](#journal-entries)

### Structure

[](#structure)

```
use App\Accounting\Models\JournalEntry;

$je = JournalEntry::create([
    'date' => '2025-01-15',
    'reference_number' => 'INV-001',
    'memo' => 'January sale',
]);

// Access ledger entries
$je->ledgerEntries;

// Balance checks
$je->totalDebits();   // Sum of all debit amounts
$je->totalCredits();  // Sum of all credit amounts
$je->isBalanced();    // true if debits == credits
```

---

Reversals and Voids
-------------------

[](#reversals-and-voids)

Ledger entries are immutable — you cannot edit or delete them. Instead, use reversals and voids to correct errors. Both create new journal entries with swapped debits and credits.

### Reversing a Journal Entry

[](#reversing-a-journal-entry)

Creates a new journal entry with debits and credits swapped, dated today:

```
$reversal = $journalEntry->reverse('Correcting entry for INV-001');

// Net effect on all accounts is zero
$cash->refresh();
$cash->getBalanceInDollars(); // Back to original
```

### Voiding a Journal Entry

[](#voiding-a-journal-entry)

Creates a reversal using the **original date** and prefixes the memo with `VOID:`:

```
$void = $journalEntry->void();

$void->memo;              // "VOID: January sale"
$void->date->toDateString(); // Same date as original
```

> Both `reverse()` and `void()` throw `LogicException` if called on an unposted journal entry.

---

Financial Reports
-----------------

[](#financial-reports)

All reports automatically exclude unposted (draft) entries.

### Trial Balance

[](#trial-balance)

```
use App\Accounting\Services\Reports\TrialBalance;

$report = TrialBalance::generate(
    asOf: Carbon::parse('2025-01-31'),
    currency: 'USD',
    includeZeroBalances: false, // default
);

// $report = [
//     'accounts' => [
//         ['account_id' => 1, 'code' => '1000', 'name' => 'Cash', 'type' => 'asset',
//          'sub_type' => AccountSubType::BANK, 'debit' => 50000, 'credit' => 0],
//         ...
//     ],
//     'total_debits' => 150000,
//     'total_credits' => 150000,   // Always balanced
//     'is_balanced' => true,
//     'as_of' => '2025-01-31',
//     'currency' => 'USD',
// ]
```

### Income Statement (Profit &amp; Loss)

[](#income-statement-profit--loss)

Separates income and expenses into categories: Revenue, COGS, Operating Expenses, and Other Income/Expenses. Computes gross profit and operating income.

```
use App\Accounting\Services\Reports\IncomeStatement;

$report = IncomeStatement::generate(
    from: Carbon::parse('2025-01-01'),
    to: Carbon::parse('2025-12-31'),
);

// Detailed structure
$report['revenue'];             // Revenue accounts
$report['cost_of_goods_sold'];  // COGS accounts
$report['gross_profit'];        // Revenue - COGS
$report['operating_expenses'];  // Operating expense accounts
$report['operating_income'];    // Gross Profit - Operating Expenses
$report['other_income'];        // Other income accounts
$report['other_expenses'];      // Other expense accounts
$report['net_income'];          // Total Income - Total Expenses

// Backward-compatible flat arrays
$report['income'];              // All income accounts
$report['expenses'];            // All expense accounts
$report['total_income'];
$report['total_expenses'];
```

### Balance Sheet

[](#balance-sheet)

Computes assets, liabilities, equity, and net income. Groups accounts by sub-type (Current Assets, Fixed Assets, Current Liabilities, etc.).

```
use App\Accounting\Services\Reports\BalanceSheet;

$report = BalanceSheet::generate(
    asOf: Carbon::parse('2025-12-31'),
);

// Grouped by sub-type
$report['grouped_assets'];      // ['Current Assets' => [...], 'Fixed Assets' => [...]]
$report['grouped_liabilities']; // ['Current Liabilities' => [...], 'Long-Term Liabilities' => [...]]
$report['grouped_equity'];      // ['Equity' => [...]]

// Flat arrays (backward-compatible)
$report['assets'];
$report['liabilities'];
$report['equity'];

// Totals
$report['total_assets'];
$report['total_liabilities'];
$report['total_equity'];
$report['net_income'];           // From IncomeStatement for the period
$report['is_balanced'];          // Assets == Liabilities + Equity + Net Income
```

### Cash Flow Statement

[](#cash-flow-statement)

Direct method cash flow statement, categorized by operating, investing, and financing activities based on contra-account types:

```
use App\Accounting\Services\Reports\CashFlowStatement;

$report = CashFlowStatement::generate(
    from: Carbon::parse('2025-01-01'),
    to: Carbon::parse('2025-12-31'),
    cashAccount: null,   // null = all bank-type accounts
    currency: 'USD',
);

$report['operating'];         // Cash flows from income/expense accounts
$report['investing'];         // Cash flows from asset accounts
$report['financing'];         // Cash flows from liability/equity accounts
$report['total_operating'];
$report['total_investing'];
$report['total_financing'];
$report['net_cash_flow'];
$report['beginning_balance'];
$report['ending_balance'];
```

### Aging Report

[](#aging-report)

Categorizes receivables or payables into aging buckets:

```
use App\Accounting\Services\Reports\AgingReport;

$report = AgingReport::generate(
    type: AccountType::ASSET, // AR aging; use LIABILITY for AP
    asOf: Carbon::now(),
);

// $report['details'] = [
//     ['account_id' => 1, 'name' => 'AR - Customer A', 'total' => 50000,
//      'buckets' => [
//          ['label' => 'Current (0-30)', 'amount' => 30000],
//          ['label' => '31-60', 'amount' => 20000],
//          ...
//      ]],
// ]
// $report['summary'] = [['label' => 'Current (0-30)', 'amount' => ...], ...]
// $report['total_outstanding'] = 50000

// Custom buckets
$report = AgingReport::generate(
    type: AccountType::ASSET,
    customBuckets: [
        ['label' => '0-15 days', 'min' => 0, 'max' => 15],
        ['label' => '16-45 days', 'min' => 16, 'max' => 45],
        ['label' => '46+ days', 'min' => 46, 'max' => null],
    ],
);
```

---

Immutability
------------

[](#immutability)

Ledger entries are immutable — they cannot be updated or deleted after creation. This is standard accounting practice to maintain a complete audit trail.

### What's Enforced

[](#whats-enforced)

```
// Throws ImmutableEntryException
$entry->memo = 'Changed';
$entry->save();

// Throws ImmutableEntryException
$entry->delete();
```

### What's Allowed

[](#whats-allowed)

The `is_posted` flag can be changed via `JournalEntry::post()` and `JournalEntry::unpost()`. No other fields can be modified.

### How to Correct Errors

[](#how-to-correct-errors)

```
// Wrong: trying to edit an entry
$entry->debit = 5000; // throws ImmutableEntryException

// Right: create a reversing entry
$reversal = $journalEntry->reverse('Correcting error');

// Then record the correct transaction
$corrected = TransactionBuilder::create()
    ->memo('Corrected entry')
    ->debit($cash, 5000)
    ->credit($revenue, 5000)
    ->commit();
```

### Database-Level Protection

[](#database-level-protection)

The `account_id` foreign key on ledger entries uses `RESTRICT` on delete — an account cannot be deleted if it has ledger entries. Accounts use soft deletes, so `$account->delete()` sets `deleted_at` without triggering the FK constraint.

---

API Reference
-------------

[](#api-reference)

### Account

[](#account)

MethodReturnsDescription`getBalance()``Money`Live-calculated balance from all posted entries`getBalanceInDollars()``float`Balance in dollars`getCurrentBalance()``Money`Alias for `getBalance()``getBalanceOn(Carbon $date)``Money`Balance as of a specific date`debit(int|Money $amount, ?string $memo, ?Carbon $postDate, ?Model $reference)``LedgerEntry`Post a debit entry`credit(int|Money $amount, ?string $memo, ?Carbon $postDate, ?Model $reference)``LedgerEntry`Post a credit entry`debitDollars(float $dollars, ?string $memo, ?Carbon $postDate)``LedgerEntry`Post a debit in dollars`creditDollars(float $dollars, ?string $memo, ?Carbon $postDate)``LedgerEntry`Post a credit in dollars`increase(int $amount, ?string $memo, ?Carbon $postDate)``LedgerEntry`Increase balance (auto debit/credit)`decrease(int $amount, ?string $memo, ?Carbon $postDate)``LedgerEntry`Decrease balance (auto debit/credit)`recalculateBalance()``Money`Recompute `cached_balance` from ledger entries`getDollarsDebitedOn(Carbon $date)``float`Total debits on a date`getDollarsCreditedOn(Carbon $date)``float`Total credits on a date`entriesReferencingModel(Model $model)``HasMany`Ledger entries linked to a model`isDebitNormal()``bool`True for debit balance accounts (Asset, Expense)### TransactionBuilder

[](#transactionbuilder)

MethodReturnsDescription`TransactionBuilder::create()``self`New builder instance`date(Carbon|string $date)``self`Set the transaction date`memo(string $memo)``self`Set the transaction memo`reference(string $ref)``self`Set the reference number`draft()``self`Mark as draft (unposted)`debit(Account, int|Money, ?string, ?Model)``self`Add a debit entry`credit(Account, int|Money, ?string, ?Model)``self`Add a credit entry`increase(Account, int|Money, ?string, ?Model)``self`Increase account (auto debit/credit)`decrease(Account, int|Money, ?string, ?Model)``self`Decrease account (auto debit/credit)`debitDollars(Account, float, ?string, ?Model)``self`Add a debit in dollars`creditDollars(Account, float, ?string, ?Model)``self`Add a credit in dollars`getPendingEntries()``array`Inspect entries before committing`commit()``JournalEntry`Validate balance and persist### JournalEntry

[](#journalentry)

MethodReturnsDescription`totalDebits()``int`Sum of all debit amounts`totalCredits()``int`Sum of all credit amounts`isBalanced()``bool`True if debits == credits`post()``self`Post the journal entry and all ledger entries`unpost()``self`Unpost the journal entry and all ledger entries`reverse(?string $memo)``JournalEntry`Create a reversing entry (today's date)`void()``JournalEntry`Create a voiding entry (original date)### HasAccounting Trait

[](#hasaccounting-trait)

MethodReturnsDescription`accounts()``MorphMany`All accounting accounts for this model`account(?string $name)``?Account`Get account by name, or first account`createAccount(string $name, AccountType, ?string $code, string $currency, ?AccountSubType)``Account`Create a new account### AccountType Enum

[](#accounttype-enum)

MethodReturnsDescription`isDebitNormal()``bool`True for debit balance types (ASSET, EXPENSE, OTHER\_EXPENSE)`isCreditNormal()``bool`True for credit balance types (LIABILITY, EQUITY, REVENUE, OTHER\_INCOME)`balanceSign()``int``1` for debit balance, `-1` for credit balance`label()``string`Human-readable label`values()``array`All enum string values### AccountSubType Enum

[](#accountsubtype-enum)

MethodReturnsDescription`parentType()``AccountType`The parent account type`reportGroup()``string`Report section label (e.g., "Current Assets")`isCurrent()``bool`Whether this is a current (short-term) item`label()``string`Human-readable label`forType(AccountType)``array`All sub-types for a given type---

Exceptions
----------

[](#exceptions)

ExceptionThrown When`UnbalancedTransactionException``TransactionBuilder::commit()` is called with unequal debits and credits`InvalidAmountException`An entry with an amount of zero or less is added`InvalidEntryMethodException`A method other than `'debit'` or `'credit'` is used internally`ImmutableEntryException`A ledger entry is updated or deleted`AccountAlreadyExistsException``createAccount()` is called with a duplicate name on the same model---

Testing
-------

[](#testing)

### With Docker

[](#with-docker)

```
# Start the test environment
make up

# Run the full test suite with coverage
make test

# Run tests without coverage
make test-fast

# Stop the environment
make down
```

### Without Docker

[](#without-docker)

```
composer test
```

### Test Suite

[](#test-suite)

The test suite includes 119 tests across unit, functional, and complex use-case categories:

- **Unit tests** — Account, JournalEntry, LedgerEntry models, enums, exceptions
- **Functional tests** — TransactionBuilder, ChartOfAccountsSeeder, HasAccounting trait, all reports
- **Complex use cases** — Full company lifecycle, reversals and voids, multi-currency, polymorphic ownership

---

Migrating from a Journal-Based Accounting Model
-----------------------------------------------

[](#migrating-from-a-journal-based-accounting-model)

If you are transitioning from a journal-based accounting package (one that uses a `Journal → JournalTransaction` model where each Eloquent model owns a single journal), this section maps the old API to this package.

### Conceptual Differences

[](#conceptual-differences)

The old model gave each Eloquent model a single journal (a flat ledger with no type information). This package gives each model one or more typed **Accounts** that live in a shared chart of accounts. The key advantages:

- Accounts have a **type** (Asset, Liability, Revenue, etc.) that determines their normal balance direction
- Transactions are grouped in **JournalEntries** that enforce the double-entry invariant
- All accounts live in one chart, enabling cross-entity financial reports

### API Mapping

[](#api-mapping)

Old (journal-based)New (this package)`$model->initJournal()``$model->createAccount('Main', AccountType::ASSET)``$model->journal->debit(100)``TransactionBuilder::create()->debit($account, 100)->credit($other, 100)->commit()``$model->journal->credit(100)``TransactionBuilder::create()->credit($account, 100)->debit($other, 100)->commit()``$model->journal->getCurrentBalance()``$account->getBalance()``$model->journal->getCurrentBalanceInDollars()``$account->getBalanceInDollars()``$model->journal->getBalanceOn($date)``$account->getBalanceOn($date)``$model->journal->getDebitBalanceOn($date)``$account->getDebitBalanceOn($date)``$model->journal->getCreditBalanceOn($date)``$account->getCreditBalanceOn($date)``$model->journal->getDollarsDebitedToday()``$account->getDollarsDebitedToday()``$model->journal->getDollarsCreditedToday()``$account->getDollarsCreditedToday()``$model->journal->getDollarsDebitedOn($date)``$account->getDollarsDebitedOn($date)``$model->journal->getDollarsCreditedOn($date)``$account->getDollarsCreditedOn($date)``$transaction->referencesObject($model)`Pass `$model` as 4th arg to `TransactionBuilder::debit()` / `credit()``$transaction->getReferencedObject()``$ledgerEntry->getReferencedModel()`### Database Table Mapping

[](#database-table-mapping)

Old TableNew TableNotes`accounting_ledgers`—No equivalent. Account type is now on the Account record itself.`accounting_journals``accounting_accounts`One account per model (or many, if needed)`accounting_journal_transactions``accounting_ledger_entries`Individual debit/credit lines—`accounting_journal_entries`Groups a balanced set of ledger entries (new concept)### Trait Migration

[](#trait-migration)

**Old trait:**

```
use Scottlaurent\Accounting\ModelTraits\AccountingJournal;

class Order extends Model
{
    use AccountingJournal;
}

// Setup
$order->initJournal('USD');

// Access
$order->journal->debit(5000, 'Payment received');
$balance = $order->journal->getCurrentBalance();
```

**New trait:**

```
use App\Accounting\Traits\HasAccounting;

class Order extends Model
{
    use HasAccounting;
}

// Setup — create a typed account for the order
$account = $order->createAccount(
    name: 'Order Balance',
    type: AccountType::ASSET,
    currency: 'USD',
);

// Record a balanced transaction
TransactionBuilder::create()
    ->memo('Payment received')
    ->debit($account, 5000)       // increase asset balance
    ->credit($revenueAccount, 5000) // credit revenue
    ->commit();

// Query balance
$balance = $account->getBalance();
$dollars = $account->getBalanceInDollars();
```

### Data Migration

[](#data-migration)

If you have existing data in the old tables, you will need to:

1. Create corresponding `accounting_accounts` rows for each old `accounting_journals` row, setting the correct `type`, `accountable_type`, and `accountable_id`
2. Create `accounting_journal_entries` rows (one per transaction group, or one per old transaction)
3. Migrate `accounting_journal_transactions` rows to `accounting_ledger_entries`, mapping `journal_id` → `account_id` and setting `journal_entry_id`

Since the old model does not enforce double-entry (each transaction is one-sided), you will need to create offsetting entries to satisfy the double-entry invariant, or import existing entries as `is_posted = false` drafts until they can be reviewed.

---

License
-------

[](#license)

This package is open-sourced software licensed under the [MIT license](LICENSE).

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance86

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 92.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

76d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ee4a02f0072cc59f35e808ac47f8e630be92216d6cf78b06e98b597e1390ef7b?d=identicon)[wayum999](/maintainers/wayum999)

---

Top Contributors

[![williamalfi](https://avatars.githubusercontent.com/u/241830185?v=4)](https://github.com/williamalfi "williamalfi (12 commits)")[![wayum999](https://avatars.githubusercontent.com/u/64566558?v=4)](https://github.com/wayum999 "wayum999 (1 commits)")

---

Tags

laravelAccounting

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/wayum999-laravel-accounting/health.svg)

```
[![Health](https://phpackages.com/badges/wayum999-laravel-accounting/health.svg)](https://phpackages.com/packages/wayum999-laravel-accounting)
```

###  Alternatives

[livewire/volt

An elegantly crafted functional API for Laravel Livewire.

4195.3M84](/packages/livewire-volt)[gehrisandro/tailwind-merge-laravel

TailwindMerge for Laravel merges multiple Tailwind CSS classes by automatically resolving conflicts between them

341682.2k18](/packages/gehrisandro-tailwind-merge-laravel)[nickurt/laravel-akismet

Akismet for Laravel 11.x/12.x/13.x

97139.6k2](/packages/nickurt-laravel-akismet)[whitecube/laravel-timezones

Store UTC dates in the database and work with custom timezones in the application.

106106.2k](/packages/whitecube-laravel-timezones)[forxer/laravel-gravatar

A library providing easy gravatar integration in a Laravel project.

4235.6k](/packages/forxer-laravel-gravatar)[iteks/laravel-enum

A comprehensive Laravel package providing enhanced enum functionalities, including attribute handling, select array conversions, and fluent facade interactions for robust enum management in Laravel applications.

2516.7k](/packages/iteks-laravel-enum)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
