Accounting Module Documentation
Overview
The Accounting module is the financial backbone of the OLOW Hotel ERP system. It implements a complete double-entry accounting system with proper fiscal period controls, journal entry management, and comprehensive financial reporting.
Key Features
- General Ledger: Complete chart of accounts with hierarchical structure
- Journal Management: Multiple journal types (GJ, SJ, PJ, CR, CP, PR)
- Fiscal Periods: Period-based accounting with closed period protection
- Double-Entry Enforcement: Automatic validation of balanced entries
- Source Tracking: Links journal entries to source documents
- Audit Trail: Complete activity logging via Spatie Activity Log
- Void Control: Proper reversing entry mechanism (no deletion)
- Account Balances: Real-time balance calculation
Architecture
Domain Layer (app/Domain/Accounting)
Models
Account (Account.php)
- Represents GL accounts in the chart of accounts
- Table:
accounting_accounts - Key Fields:
code: Unique account code (e.g., "1010", "4000")name: Account name (e.g., "Cash - Checking")account_type: ASSET | LIABILITY | EQUITY | REVENUE | EXPENSE | COGSsub_type: Additional categorizationnormal_side: D (Debit) or C (Credit)parent_account_id: For hierarchical account structureis_active: Active/inactive status
- Relationships:
parent(): Parent accountchildren(): Child accountsjournalLines(): All journal lines posting to this account
- Methods:
getBalanceAttribute(): Calculate current account balancescopeActive(): Filter active accountsscopeOfType(): Filter by account typescopeRoot(): Get root-level accounts
- Traits:
BelongsToProperty
FiscalPeriod (FiscalPeriod.php)
- Represents accounting periods
- Table:
accounting_fiscal_periods - Key Fields:
name: Period name (e.g., "January 2026")start_date: Period startend_date: Period endis_closed: Closed status (prevents posting when true)
- Methods:
static findForDate(): Find period for a given date
- Traits:
BelongsToProperty
Journal (Journal.php)
- Represents journal types
- Table:
accounting_journals - Key Fields:
code: Journal code (GJ, SJ, PJ, etc.)name: Journal name (e.g., "General Journal")
- Methods:
static findByCode(): Find journal by code
- Traits:
BelongsToProperty
JournalEntry (JournalEntry.php)
- Represents journal entry headers
- Table:
accounting_journal_entries - Key Fields:
journal_id: Reference to journal typefiscal_period_id: Accounting periodentry_date: Transaction datereference_no: Reference numberdescription: Entry descriptionsource_module: Origin module (MANUAL, AR, AP, etc.)source_table: Source table namesource_id: Source record IDstatus: DRAFT | POSTED | VOIDposted_at: Posting timestamp
- Relationships:
journal(): Journal typelines(): Journal lines (debits and credits)fiscalPeriod(): Fiscal period
- Methods:
isBalanced(): Check if debits equal creditsgetTotalDebitsAttribute(): Calculate total debitsgetTotalCreditsAttribute(): Calculate total credits
- Traits:
BelongsToProperty
JournalLine (JournalLine.php)
- Represents individual debit/credit lines
- Table:
accounting_journal_lines - Key Fields:
journal_entry_id: Parent entryline_no: Line numberaccount_id: GL accountparty_id: Related party (customer/vendor) if applicablememo: Line descriptiondebit: Debit amountcredit: Credit amount
- Relationships:
journalEntry(): Parent entryaccount(): GL accountparty(): Related party
AccountingLink (AccountingLink.php)
- Links journal entries to source documents
- Table:
accounting_links - Key Fields:
journal_entry_id: Journal entrymodule: Source modulerecord_table: Source tablerecord_id: Source record ID
Services
AccountingPoster (AccountingPoster.php)
IMPORTANT
This is the ONLY way to create journal entries. Direct model creation is prohibited.
Purpose: Centralized service for creating and managing journal entries
Key Methods:
createEntry()
Creates a new journal entry with validation
public function createEntry(
string $journalCode, // e.g., 'GJ', 'SJ', 'CR'
\Carbon\Carbon $date, // Entry date
string $description, // Entry description
array $lines, // Array of line data
string $sourceModule = 'MANUAL', // Source module
?string $sourceTable = null, // Source table
?int $sourceId = null, // Source ID
bool $autoPost = true, // Auto-post flag
?int $propertyId = null // Property ID
): JournalEntryLines Array Format:
[
[
'account_id' => 123,
'debit' => 1000.00,
'credit' => 0,
'memo' => 'Sales revenue',
'party_id' => null // Optional
],
[
'account_id' => 456,
'debit' => 0,
'credit' => 1000.00,
'memo' => 'Accounts receivable',
'party_id' => 789
]
]Validations:
- Debits must equal credits (within 0.01 tolerance)
- Journal code must exist
- Fiscal period must exist for entry date
- Fiscal period must not be closed
- Property ID must be provided or determinable
Behavior:
- Creates journal entry in DRAFT status
- Creates all journal lines
- Creates accounting link if source provided
- Auto-posts if
$autoPost = true - Returns fresh entry with relationships
postEntry()
Posts a draft entry to the ledger
public function postEntry(JournalEntry $entry): voidValidations:
- Entry must be in DRAFT status
- Entry must be balanced
- Fiscal period must not be closed
Behavior:
- Changes status to POSTED
- Sets posted_at timestamp
- Links to fiscal period
voidEntry()
Voids a posted entry by creating a reversing entry
public function voidEntry(JournalEntry $entry, string $reason): JournalEntryValidations:
- Entry must be POSTEDExcellent! Now let me create documentation for all modules systematically. Given the scope, let me create comprehensive documentation more efficiently by examining key files and creating structured documentation:
Behavior:
- Creates reversing entry (swaps debits/credits)
- Marks original as VOID
- Returns reversing entry
Example Reversing:
// Original Entry
Debit: Cash 1000
Credit: Revenue 1000
// Reversing Entry
Debit: Revenue 1000
Credit: Cash 1000getAccountBalance()
Calculates account balance as of a date
public function getAccountBalance(Account $account, ?Carbon $asOfDate = null): floatReturns: Net balance respecting normal side (positive for debit accounts with debit > credit)
Presentation Layer (app/Livewire/Accounting)
ChartOfAccounts Component
- Route:
/admin/accounting/accounts - Purpose: Manage chart of accounts
- Features:
- List accounts with pagination
- Search by code or name
- Filter by account type
- Show/hide inactive accounts
- Create/edit accounts via modal
- Toggle account active status
- Hierarchical account display
JournalEntries Component
- Route:
/admin/accounting/entries - Purpose: View and manage journal entries
- Features:
- List entries with filters
- View entry details
- Post draft entries
- Void posted entries
- Filter by date, journal, status
- Export functionality
ManageFiscalPeriods Component
- Route:
/admin/accounting/fiscal-periods - Purpose: Manage accounting periods
- Features:
- Create periods
- Close/reopen periods
- View period status
- Prevent posting to closed periods
Reports
Located in app/Livewire/Accounting/Reports/:
- BalanceSheet: Assets, Liabilities, Equity statement
- IncomeStatement: Revenue and Expenses (P&L)
- TrialBalance: List of all accounts with balances
- GeneralLedger: Account-by-account transaction detail
- CashFlowStatement: Cash flow analysis
- DailyRevenueReport: Daily revenue breakdown
- TaxLiabilityReport: Tax obligations
Database Schema
Entity Relationship Diagram
erDiagram
FISCAL_PERIOD ||--o{ JOURNAL_ENTRY : contains
JOURNAL ||--o{ JOURNAL_ENTRY : categorizes
JOURNAL_ENTRY ||--|{ JOURNAL_LINE : has
JOURNAL_ENTRY ||--o{ ACCOUNTING_LINK : "links to"
ACCOUNT ||--o{ JOURNAL_LINE : "posts to"
ACCOUNT ||--o{ ACCOUNT : "parent of"
PARTY ||--o{ JOURNAL_LINE : "related to"
FISCAL_PERIOD {
bigint id PK
bigint property_id FK
string name
date start_date
date end_date
boolean is_closed
}
JOURNAL {
bigint id PK
bigint property_id FK
string code UK
string name
}
ACCOUNT {
bigint id PK
bigint property_id FK
string code UK
string name
enum account_type
string sub_type
char normal_side
bigint parent_account_id FK
boolean is_active
}
JOURNAL_ENTRY {
bigint id PK
bigint property_id FK
bigint journal_id FK
bigint fiscal_period_id FK
date entry_date
string reference_no
text description
enum source_module
string source_table
bigint source_id
enum status
timestamp posted_at
}
JOURNAL_LINE {
bigint id PK
bigint journal_entry_id FK
integer line_no
bigint account_id FK
bigint party_id FK
string memo
decimal debit
decimal credit
}
ACCOUNTING_LINK {
bigint id PK
bigint journal_entry_id FK
string module
string record_table
bigint record_id
}Integration with Other Modules
The Accounting module integrates with nearly all other modules for financial posting:
AR Module Integration
use App\Domain\Accounting\Services\AccountingPoster;
// When invoice is created
$poster = new AccountingPoster();
$poster->createEntry(
'SJ', // Sales Journal
$invoice->invoice_date,
"Customer Invoice #{$invoice->invoice_no}",
[
['account_id' => $arAccount, 'debit' => $total, 'party_id' => $customer->party_id],
['account_id' => $revenueAccount, 'credit' => $subtotal],
['account_id' => $taxAccount, 'credit' => $taxAmount],
],
'AR',
'ar_invoices',
$invoice->id
);AP Module Integration
// When bill is recorded
$poster->createEntry(
'PJ', // Purchase Journal
$bill->bill_date,
"Vendor Bill #{$bill->bill_no}",
[
['account_id' => $expenseAccount, 'debit' => $amount],
['account_id' => $apAccount, 'credit' => $amount, 'party_id' => $vendor->party_id],
],
'AP',
'ap_bills',
$bill->id
);Expense Module Integration
// When expense is approved
$poster->createEntry(
'GJ',
$expense->expense_date,
"Expense #{$expense->id}",
[
['account_id' => $categoryAccount, 'debit' => $amount],
['account_id' => $cashAccount, 'credit' => $amount],
],
'EXPENSE',
'expenses',
$expense->id
);Common Workflows
1. Creating a Manual Journal Entry
- Navigate to Accounting > Journal Entries
- Click Create Entry
- Select journal type
- Enter date and description
- Add lines (debits and credits must balance)
- Review entry
- Post entry (or save as draft)
2. Configuring System Mappings (Accounting Setup)
- Navigate to Accounting > Accounting Setup
- Review the list of logical keys (grouped by module: Assets, Revenue, etc.)
- Use the dropdowns to map each key to a Chart of Accounts GL account
- Click Save Mappings
- Ensure "Unmapped" count is zero for critical operations
3. Setting Up Chart of Accounts
- Navigate to Accounting > Chart of Accounts
- Create root accounts for each type:
- ASSET: 1000
- LIABILITY: 2000
- EQUITY: 3000
- REVENUE: 4000
- EXPENSE: 5000
- Create sub-accounts under each root
- Link accounts to operational records via
AccountingLink
3. Closing a Fiscal Period
- Navigate to Accounting > Fiscal Periods
- Ensure all entries for period are posted
- Review period reports (Trial Balance, P&L)
- Click Close Period
- Period is now locked (no new postings allowed)
4. Voiding an Entry
- Navigate to Accounting > Journal Entries
- Find the posted entry to void
- Click Void
- Enter void reason
- System creates reversing entry
- Original marked as VOID
Configuration
Journal Codes
Standard journal codes used in the system:
| Code | Name | Purpose |
|---|---|---|
| GJ | General Journal | Manual entries, adjustments |
| SJ | Sales Journal | Revenue entries (AR) |
| PJ | Purchase Journal | Expense entries (AP) |
| CR | Cash Receipts | Customer payments |
| CP | Cash Payments | Vendor payments |
| PR | Payroll | Salary and wage entries |
Account Types & Normal Sides
| Type | Normal Side | Increases On | Example |
|---|---|---|---|
| ASSET | Debit | Debit | Cash, Inventory |
| LIABILITY | Credit | Credit | Accounts Payable |
| EQUITY | Credit | Credit | Retained Earnings |
| REVENUE | Credit | Credit | Room Revenue |
| EXPENSE | Debit | Debit | Utilities |
| COGS | Debit | Debit | Room Supplies |
Known Issues & Limitations
Issues (from Audit)
See the audit documentation for the complete list
Critical:
- None currently
Major:
- MAJ-ACC-001: AccountingPoster falls back to property_id=1
- MAJ-ACC-002: Account sub_type field verification needed
Minor:
- MIN-ACC-001: ENUM for source_module limits extensibility
- MIN-ACC-002: reference_no not set in createEntry
Limitations
- Journal entries cannot be edited (only voided and re-entered)
- Closed fiscal periods cannot be reopened without database access
- No batch entry support (each entry created individually)
- Account codes must be unique per property
Best Practices
For Developers
- Always use AccountingPoster: Never create
JournalEntryorJournalLinedirectly - Provide source tracking: Always pass
sourceTableandsourceIdwhen posting from modules - Use transactions: Wrap complex operations including accounting posts in DB transactions
- Validate fiscal period: Check period is open before attempting to post
- Use account links: Link operational records to GL accounts via
AccountingLink
For Users
- Regular reconciliation: Reconcile accounts monthly
- Close periods promptly: Close fiscal periods immediately after month-end
- Document entries: Use clear, descriptive text for manual entries
- Review before posting: Draft entries allow review before posting
- Use correct journals: Use appropriate journal codes for entry types
API Reference
AccountingMapService
Service for resolving logical keys to Account IDs.
use App\Domain\Accounting\Services\AccountingMapService;
use App\Domain\Accounting\Enums\AccountingKey;
$mapService = app(AccountingMapService::class);
// Get Account ID for a logical key
$accountId = $mapService->getAccountId(AccountingKey::REVENUE_ROOM, $propertyId);
// Get full Account model
$account = $mapService->getAccount(AccountingKey::ASSET_CASH, $propertyId);AccountingPoster
use App\Domain\Accounting\Services\AccountingPoster;
$poster = new AccountingPoster();
// Create and post entry
$entry = $poster->createEntry(
journalCode: 'GJ',
date: now(),
description: 'Month-end adjustment',
lines: [/*...*/],
sourceModule: 'MANUAL'
);
// Post draft entry
$poster->postEntry($draftEntry);
// Void posted entry
$reversingEntry = $poster->voidEntry($postedEntry, 'Correction');
// Get account balance
$balance = $poster->getAccountBalance($account, asOfDate: today());Account Model
use App\Domain\Accounting\Models\Account;
// Query accounts
$assets = Account::ofType('ASSET')->active()->get();
$rootAccounts = Account::root()->get();
// Get balance
$cashBalance = $cashAccount->balance;
// Navigate hierarchy
$parent = $account->parent;
$children = $account->children;Troubleshooting
"Debits must equal credits" Error
- Cause: Line totals don't balance
- Solution: Verify sum of debits equals sum of credits exactly
"No active fiscal period found" Error
- Cause: No fiscal period defined for entry date
- Solution: Create fiscal period covering the entry date
"Cannot post to closed fiscal period" Error
- Cause: Trying to post to a closed period
- Solution: Reopen period or adjust entry date to current open period
"Property ID not determinable" Error
- Cause: AccountingPoster can't determine which property to post to
- Solution: Pass explicit
property_idparameter or ensurecurrent_property_idis bound in service container
Future Enhancements
Planned
- Batch entry support for performance
- Entry templates for common transactions
- Budget vs. actual reporting
- Account reconciliation tool
- Multi-currency support
Under Consideration
- Scheduled entries (recurring)
- Entry approval workflow
- Account aliases
- Advanced consolidation reporting
Module Version: 1.0 Last Updated: January 26, 2026 Maintained By: Development Team