Overview
The Accounting module implements a complete double-entry bookkeeping system that automatically generates journal entries from business transactions. It manages the chart of accounts, receivables, payments, and provides financial reports.
Double-Entry Balanced journal entries
Automated Entries from sales and payments
Receivables Credit sales tracking
Core Components
Chart of Accounts
The foundation of the accounting system:
app/Models/Accounting/AccountingAccount.php
protected $fillable = [
'code' , // e.g., "1.1.01", "4.1"
'name' , // e.g., "Cash", "Sales Revenue"
'type' , // asset, liability, equity, revenue, expense
'subtype' , // current_asset, fixed_asset, etc.
'is_active' ,
];
Common Account Structure:
Assets (1.x)
Liabilities (2.x)
Equity (3.x)
Revenue (4.x)
Expenses (5.x)
1.1 Current Assets
1.1.01 Cash
1.1.02 Accounts Receivable
1.1.03 Inventory
1.2 Fixed Assets
1.2.01 Equipment
1.2.02 Vehicles
2.1 Current Liabilities
2.1.01 Accounts Payable
2.1.02 Taxes Payable
2.2 Long-term Liabilities
2.2.01 Loans Payable
3.1 Owner's Equity
3.1.01 Capital
3.1.02 Retained Earnings
4.1 Sales Revenue
4.2 Service Revenue
4.3 Other Income
5.1 Cost of Goods Sold
5.2 Operating Expenses
5.3 Inventory Variance
Journal Entries
Records all financial transactions:
app/Models/Accounting/JournalEntry.php
protected $fillable = [
'entry_date' ,
'reference' , // e.g., "FAC-2024-001"
'description' ,
'status' , // draft, posted, cancelled
'created_by' ,
];
const STATUS_DRAFT = 'draft' ;
const STATUS_POSTED = 'posted' ;
const STATUS_CANCELLED = 'cancelled' ;
Entry Status Flow:
Journal Items
The individual debit and credit lines:
app/Models/Accounting/JournalItem.php
protected $fillable = [
'journal_entry_id' ,
'accounting_account_id' ,
'debit' ,
'credit' ,
];
Every journal entry must balance: Total Debits = Total Credits
Creating Journal Entries
Manual Entry
use App\Services\Accounting\JournalEntries\ JournalEntryService ;
$entryService = app ( JournalEntryService :: class );
$entry = $entryService -> create ([
'entry_date' => now (),
'reference' => 'MANUAL-001' ,
'description' => 'Initial capital investment' ,
'status' => JournalEntry :: STATUS_POSTED ,
'items' => [
[
'accounting_account_id' => 1 , // Cash account
'debit' => 50000 ,
'credit' => 0 ,
],
[
'accounting_account_id' => 15 , // Capital account
'debit' => 0 ,
'credit' => 50000 ,
],
],
]);
Automatic from Sales
The system automatically generates entries for sales:
Cash Sale:
app/Services/Sales/SalesServices/SaleService.php
protected function generateSaleAccountingEntry ( Sale $sale )
{
$incomeAccount = AccountingAccount :: where ( 'code' , '4.1' ) -> first ();
$debitAccountId = $sale -> tipoPago ?-> accounting_account_id
?? AccountingAccount :: where ( 'code' , '1.1.01' ) -> value ( 'id' );
$this -> journalService -> create ([
'entry_date' => $sale -> sale_date ,
'reference' => $sale -> number ,
'description' => "Venta Contado - { $sale -> client -> name }" ,
'status' => JournalEntry :: STATUS_POSTED ,
'items' => [
[ 'accounting_account_id' => $debitAccountId ,
'debit' => $sale -> total_amount , 'credit' => 0 ],
[ 'accounting_account_id' => $incomeAccount -> id ,
'debit' => 0 , 'credit' => $sale -> total_amount ]
]
]);
}
Result:
Date: 2024-03-05
Reference: FAC-2024-001
Description: Venta Contado - ABC Company
Account | Debit | Credit
---------------------|----------|----------
1.1.01 Cash | $1,000 |
4.1 Sales Revenue | | $1,000
---------------------|----------|----------
Total | $1,000 | $1,000 ✓ Balanced
Receivables Management
Tracks amounts owed by customers from credit sales.
Receivable Structure
app/Models/Accounting/Receivable.php
protected $fillable = [
'client_id' ,
'total_amount' ,
'current_balance' ,
'emission_date' ,
'due_date' ,
'status' , // unpaid, partial, paid, overdue
'document_number' , // e.g., "FAC-2024-001"
'reference_type' , // Polymorphic: Sale, etc.
'reference_id' ,
];
const STATUS_UNPAID = 'unpaid' ;
const STATUS_PARTIAL = 'partial' ;
const STATUS_PAID = 'paid' ;
const STATUS_OVERDUE = 'overdue' ;
Creating Receivables
Automatically created from credit sales:
app/Services/Sales/SalesServices/SaleService.php
if ( $sale -> payment_type === Sale :: PAYMENT_CREDIT ) {
$this -> receivableService -> createReceivable ([
'client_id' => $sale -> client_id ,
'total_amount' => $sale -> total_amount ,
'emission_date' => $sale -> sale_date ,
'due_date' => $sale -> sale_date -> copy () -> addDays ( 30 ),
'document_number' => $sale -> number ,
'reference_type' => Sale :: class ,
'reference_id' => $sale -> id ,
'description' => "Venta a crédito"
]);
}
Applying Payments
app/Services/Accounting/Receivable/ReceivableService.php
public function applyPayment ( Receivable $receivable , Payment $payment ) : void
{
DB :: transaction ( function () use ( $receivable , $payment ) {
// Reduce receivable balance
$receivable -> current_balance -= $payment -> amount ;
$receivable -> save ();
// Update status
$this -> updateStatusBasedOnBalance ( $receivable );
// Link payment to receivable
$payment -> update ([
'receivable_id' => $receivable -> id ,
]);
});
}
Status Updates:
if ( $receivable -> current_balance <= 0.01 ) {
$receivable -> status = Receivable :: STATUS_PAID ;
} elseif ( $receivable -> current_balance < $receivable -> total_amount ) {
$receivable -> status = Receivable :: STATUS_PARTIAL ;
} elseif ( now () -> isAfter ( $receivable -> due_date )) {
$receivable -> status = Receivable :: STATUS_OVERDUE ;
}
Payment Processing
Recording Payments
app/Models/Accounting/Payment.php
protected $fillable = [
'client_id' ,
'receivable_id' ,
'amount' ,
'payment_date' ,
'payment_method' , // cash, transfer, check, card
'reference_number' , // Check/transfer number
'notes' ,
];
Payment Workflow
Create Payment
Record the payment from the customer. $payment = Payment :: create ([
'client_id' => $clientId ,
'amount' => 500 ,
'payment_date' => now (),
'payment_method' => 'transfer' ,
'reference_number' => 'TRANS-12345' ,
]);
Apply to Receivable
Link the payment to an outstanding receivable. $receivableService -> applyPayment ( $receivable , $payment );
Generate Journal Entry
Create accounting entry for the payment. Debit: Cash/Bank (1.1.01) $500
Credit: Accounts Receivable (1.1.02) $500
Update Client Balance
Refresh the client’s total outstanding balance. $client -> refreshBalance ();
Document Types
Manages sequential numbering for financial documents:
app/Models/Accounting/DocumentType.php
protected $fillable = [
'code' , // e.g., "FAC" for invoices
'name' ,
'prefix' , // e.g., "FAC-"
'current_number' ,
'padding' , // Number of digits
];
public function getNextNumberFormatted () : string
{
$number = str_pad (
$this -> current_number + 1 ,
$this -> padding ,
'0' ,
STR_PAD_LEFT
);
return $this -> prefix . date ( 'Y' ) . '-' . $number ;
}
Example Output: FAC-2024-001, FAC-2024-002, etc.
Financial Reports
Trial Balance
$trialBalance = JournalItem :: join ( 'accounting_accounts' , ... )
-> selectRaw ( '
accounting_account_id,
SUM(debit) as total_debit,
SUM(credit) as total_credit,
SUM(debit) - SUM(credit) as balance
' )
-> groupBy ( 'accounting_account_id' )
-> get ();
Aging Report
$aging = Receivable :: select ([
'client_id' ,
DB :: raw ( 'SUM(CASE WHEN DATEDIFF(NOW(), due_date) <= 30 THEN current_balance ELSE 0 END) as current' ),
DB :: raw ( 'SUM(CASE WHEN DATEDIFF(NOW(), due_date) BETWEEN 31 AND 60 THEN current_balance ELSE 0 END) as days_31_60' ),
DB :: raw ( 'SUM(CASE WHEN DATEDIFF(NOW(), due_date) > 60 THEN current_balance ELSE 0 END) as over_60' ),
])
-> whereIn ( 'status' , [ Receivable :: STATUS_UNPAID , Receivable :: STATUS_PARTIAL , Receivable :: STATUS_OVERDUE ])
-> groupBy ( 'client_id' )
-> get ();
Income Statement
$revenue = JournalItem :: whereHas ( 'account' , function ( $q ) {
$q -> where ( 'type' , 'revenue' );
})
-> whereBetween ( 'created_at' , [ $startDate , $endDate ])
-> sum ( 'credit' );
$expenses = JournalItem :: whereHas ( 'account' , function ( $q ) {
$q -> where ( 'type' , 'expense' );
})
-> whereBetween ( 'created_at' , [ $startDate , $endDate ])
-> sum ( 'debit' );
$netIncome = $revenue - $expenses ;
Best Practices
Validate that debits equal credits before posting: $totalDebit = collect ( $items ) -> sum ( 'debit' );
$totalCredit = collect ( $items ) -> sum ( 'credit' );
if ( abs ( $totalDebit - $totalCredit ) > 0.01 ) {
throw new \Exception ( 'Entry is not balanced' );
}
Create entries as drafts first, review, then post: $entry = $entryService -> create ([
'status' => JournalEntry :: STATUS_DRAFT ,
// ...
]);
// After review
$entryService -> post ( $entry );
Reversal Instead of Deletion
Never delete posted entries. Create reversals instead: protected function generateCancellationAccountingEntry ( Sale $sale )
{
// Create opposite entry
$this -> journalService -> create ([
'reference' => "REV-{ $sale -> number }" ,
'description' => "Anulación Venta { $sale -> number }" ,
'items' => [
// Flip debits and credits
]
]);
}
Periodically verify that:
Trial balance is balanced
Client balances match receivables
Bank statements match cash accounts
Integration Points
Sales Module
Generates entries for:
Cash sales (Debit: Cash, Credit: Revenue)
Credit sales (Debit: A/R, Credit: Revenue)
Sale cancellations (Reversal entries)
Inventory Module
Generates entries for:
Purchases (Debit: Inventory, Credit: A/P)
Cost of goods sold (Debit: COGS, Credit: Inventory)
Adjustments (Debit/Credit: Variance)
Payment Module
Generates entries for:
Customer payments (Debit: Cash, Credit: A/R)
Vendor payments (Debit: A/P, Credit: Cash)
Related Documentation
Journal Entries Detailed entry creation guide
Receivables Credit sales management
Sales Module Sales integration
Journal Entry Service Service API reference