Skip to content

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 | COGS
    • sub_type: Additional categorization
    • normal_side: D (Debit) or C (Credit)
    • parent_account_id: For hierarchical account structure
    • is_active: Active/inactive status
  • Relationships:
    • parent(): Parent account
    • children(): Child accounts
    • journalLines(): All journal lines posting to this account
  • Methods:
    • getBalanceAttribute(): Calculate current account balance
    • scopeActive(): Filter active accounts
    • scopeOfType(): Filter by account type
    • scopeRoot(): 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 start
    • end_date: Period end
    • is_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 type
    • fiscal_period_id: Accounting period
    • entry_date: Transaction date
    • reference_no: Reference number
    • description: Entry description
    • source_module: Origin module (MANUAL, AR, AP, etc.)
    • source_table: Source table name
    • source_id: Source record ID
    • status: DRAFT | POSTED | VOID
    • posted_at: Posting timestamp
  • Relationships:
    • journal(): Journal type
    • lines(): Journal lines (debits and credits)
    • fiscalPeriod(): Fiscal period
  • Methods:
    • isBalanced(): Check if debits equal credits
    • getTotalDebitsAttribute(): Calculate total debits
    • getTotalCreditsAttribute(): Calculate total credits
  • Traits: BelongsToProperty
JournalLine (JournalLine.php)
  • Represents individual debit/credit lines
  • Table: accounting_journal_lines
  • Key Fields:
    • journal_entry_id: Parent entry
    • line_no: Line number
    • account_id: GL account
    • party_id: Related party (customer/vendor) if applicable
    • memo: Line description
    • debit: Debit amount
    • credit: Credit amount
  • Relationships:
    • journalEntry(): Parent entry
    • account(): GL account
    • party(): Related party
  • Links journal entries to source documents
  • Table: accounting_links
  • Key Fields:
    • journal_entry_id: Journal entry
    • module: Source module
    • record_table: Source table
    • record_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

php
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
): JournalEntry

Lines Array Format:

php
[
    [
        '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

php
public function postEntry(JournalEntry $entry): void

Validations:

  • 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

php
public function voidEntry(JournalEntry $entry, string $reason): JournalEntry

Validations:

  • 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:

php
// Original Entry
Debit: Cash           1000
Credit: Revenue       1000

// Reversing Entry
Debit: Revenue        1000
Credit: Cash          1000
getAccountBalance()

Calculates account balance as of a date

php
public function getAccountBalance(Account $account, ?Carbon $asOfDate = null): float

Returns: 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

mermaid
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

php
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

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

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

  1. Navigate to Accounting > Journal Entries
  2. Click Create Entry
  3. Select journal type
  4. Enter date and description
  5. Add lines (debits and credits must balance)
  6. Review entry
  7. Post entry (or save as draft)

2. Configuring System Mappings (Accounting Setup)

  1. Navigate to Accounting > Accounting Setup
  2. Review the list of logical keys (grouped by module: Assets, Revenue, etc.)
  3. Use the dropdowns to map each key to a Chart of Accounts GL account
  4. Click Save Mappings
  5. Ensure "Unmapped" count is zero for critical operations

3. Setting Up Chart of Accounts

  1. Navigate to Accounting > Chart of Accounts
  2. Create root accounts for each type:
    • ASSET: 1000
    • LIABILITY: 2000
    • EQUITY: 3000
    • REVENUE: 4000
    • EXPENSE: 5000
  3. Create sub-accounts under each root
  4. Link accounts to operational records via AccountingLink

3. Closing a Fiscal Period

  1. Navigate to Accounting > Fiscal Periods
  2. Ensure all entries for period are posted
  3. Review period reports (Trial Balance, P&L)
  4. Click Close Period
  5. Period is now locked (no new postings allowed)

4. Voiding an Entry

  1. Navigate to Accounting > Journal Entries
  2. Find the posted entry to void
  3. Click Void
  4. Enter void reason
  5. System creates reversing entry
  6. Original marked as VOID

Configuration

Journal Codes

Standard journal codes used in the system:

CodeNamePurpose
GJGeneral JournalManual entries, adjustments
SJSales JournalRevenue entries (AR)
PJPurchase JournalExpense entries (AP)
CRCash ReceiptsCustomer payments
CPCash PaymentsVendor payments
PRPayrollSalary and wage entries

Account Types & Normal Sides

TypeNormal SideIncreases OnExample
ASSETDebitDebitCash, Inventory
LIABILITYCreditCreditAccounts Payable
EQUITYCreditCreditRetained Earnings
REVENUECreditCreditRoom Revenue
EXPENSEDebitDebitUtilities
COGSDebitDebitRoom 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

  1. Always use AccountingPoster: Never create JournalEntry or JournalLine directly
  2. Provide source tracking: Always pass sourceTable and sourceId when posting from modules
  3. Use transactions: Wrap complex operations including accounting posts in DB transactions
  4. Validate fiscal period: Check period is open before attempting to post
  5. Use account links: Link operational records to GL accounts via AccountingLink

For Users

  1. Regular reconciliation: Reconcile accounts monthly
  2. Close periods promptly: Close fiscal periods immediately after month-end
  3. Document entries: Use clear, descriptive text for manual entries
  4. Review before posting: Draft entries allow review before posting
  5. Use correct journals: Use appropriate journal codes for entry types

API Reference

AccountingMapService

Service for resolving logical keys to Account IDs.

php
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

php
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

php
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_id parameter or ensure current_property_id is 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