wayum999/laravel-accounting

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

Maintainers

Package info

github.com/wayum999/laravel-accounting

pkg:composer/wayum999/laravel-accounting

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-02-26 16:21 UTC

This package is auto-updated.

Last update: 2026-05-08 05:34:22 UTC


README

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

Requirements

Requirement Version
PHP 8.2+
Laravel 12.x
Database PostgreSQL, MySQL, SQLite, or SQL Server

Installation

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

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

This creates the following tables:

Table Purpose
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

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

Type Balance Type Increases With Examples
ASSET Debit Balance Debit Cash, Accounts Receivable, Inventory
LIABILITY Credit Balance Credit Accounts Payable, Loans Payable
EQUITY Credit Balance Credit Owner's Equity, Retained Earnings
REVENUE Credit Balance Credit Sales Revenue, Service Fees
EXPENSE Debit Balance Debit Salaries, Rent, Cost of Goods Sold
OTHER_INCOME Credit Balance Credit Gain on Sale of Assets, Insurance Recoveries
OTHER_EXPENSE Debit Balance Debit Loss 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

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

Parent Type Sub-Types
Asset 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

All monetary amounts are stored as integer cents to avoid floating-point rounding errors. This package uses moneyphp/money internally. Dollar convenience methods accept float values and handle the conversion automatically.

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 for details.

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

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

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

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

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

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

$customer = Customer::find(1);

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

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

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

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

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

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

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) Debit Credit
Liability, Equity, Revenue, Other Income (credit balance) Credit Debit

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

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

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

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

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

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

// 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 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

$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

$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

$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

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

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

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

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

All reports automatically exclude unposted (draft) entries.

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 & 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

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

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

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

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

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

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

What's Allowed

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

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

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

Account

Method Returns Description
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

Method Returns Description
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

Method Returns Description
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

Method Returns Description
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

Method Returns Description
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

Method Returns Description
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

Exception Thrown 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

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

composer test

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

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

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

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

Old Table New Table Notes
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

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

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_idaccount_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

This package is open-sourced software licensed under the MIT license.