PHPackages                             centrex/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. centrex/laravel-accounting

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

centrex/laravel-accounting
==========================

Manage accounts in laravel

v3.6.3(5d ago)13.3k2MITPHPPHP ^8.3CI failing

Since Nov 13Pushed 5d agoCompare

[ Source](https://github.com/centrex/laravel-accounting)[ Packagist](https://packagist.org/packages/centrex/laravel-accounting)[ Docs](https://github.com/centrex/laravel-accounting)[ RSS](/packages/centrex-laravel-accounting/feed)WikiDiscussions main Synced yesterday

READMEChangelogDependencies (49)Versions (64)Used By (0)

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

[](#laravel-accounting)

[![Latest Version on Packagist](https://camo.githubusercontent.com/097bbcce3b9da96e4b5028f964a03a27e07c3bc93cd2511bd21072c5a215265b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f63656e747265782f6c61726176656c2d6163636f756e74696e672e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/centrex/laravel-accounting)[![GitHub Tests Action Status](https://camo.githubusercontent.com/f20b61eb44068cf9ed37111fb4210058ff46d8caf9d7473a8367e7ae2a61bbbe/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f63656e747265782f6c61726176656c2d6163636f756e74696e672f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/centrex/laravel-accounting/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/52902a7f2f96dbe4d8084fb5ee149e30fdf91eebe6d6974eafb43436d73d1e09/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f63656e747265782f6c61726176656c2d6163636f756e74696e672f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/centrex/laravel-accounting/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/93e572b3693c8385f9308b4e682b9c56e02c8e03f0d771faf6adf8585cb0e757/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f63656e747265782f6c61726176656c2d6163636f756e74696e673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/centrex/laravel-accounting)

Full double-entry accounting system for Laravel. Includes a chart of accounts, journal entries with two-step approval workflow, invoices, bills, expenses, customer/vendor ledgers, financial reports (including A/R and A/P aging), month-end period closing with inventory reconciliation, fiscal year closing, budgets, QuickBooks Online two-way sync, and a Livewire UI with a complete REST API layer.

Developer architecture notes live in [docs/developer-architecture.md](docs/developer-architecture.md).

---

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Core Concepts](#core-concepts)
- [Chart of Accounts](#chart-of-accounts)
- [Journal Entries — Two-Step Workflow](#journal-entries--two-step-workflow)
- [Invoices &amp; Payments](#invoices--payments)
- [Bills &amp; Vendor Payments](#bills--vendor-payments)
- [Invoice &amp; Bill Charges / Discounts](#invoice--bill-charges--discounts)
- [Expenses](#expenses)
- [Requisitions](#requisitions)
- [Customer &amp; Vendor Ledger](#customer--vendor-ledger)
- [General Ledger](#general-ledger)
- [Financial Reports](#financial-reports)
- [SBU (Cost Centre) Filtering](#sbu-cost-centre-filtering)
- [Budgets](#budgets)
- [Period Closing (Month-End)](#period-closing-month-end)
- [Fiscal Year Closing](#fiscal-year-closing)
- [Inventory Financing](#inventory-financing)
- [Organizational Loans &amp; SBU-wise Tracking](#organizational-loans--sbu-wise-tracking)
- [Real-World Example — Trading Company Month-End Close](#real-world-example--trading-company-month-end-close)
- [Authorization Gates](#authorization-gates)
- [Web UI Routes](#web-ui-routes)
- [REST API](#rest-api)
- [Artisan Commands](#artisan-commands)
- [QuickBooks Online Integration](#quickbooks-online-integration)
- [Testing](#testing)

---

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

[](#installation)

```
composer require centrex/laravel-accounting
php artisan vendor:publish --tag="laravel-accounting-config"
php artisan migrate
```

Seed the standard chart of accounts (idempotent, safe to re-run):

```
use Centrex\Accounting\Facades\Accounting;

Accounting::initializeChartOfAccounts();
```

Standard accounts created: Cash (1000), Bank (1100), Accounts Receivable (1200), Inventory (1300), Accounts Payable (2000), Sales Tax Payable (2300), Share Capital (3000), Retained Earnings (3100), Sales Revenue (4000), COGS (5000), and more.

Publish views for customisation:

```
php artisan vendor:publish --tag="laravel-accounting-views"
```

---

Environment Variables
---------------------

[](#environment-variables)

```
ACCOUNTING_CURRENCY=BDT
ACCOUNTING_DB_CONNECTION=mysql          # optional separate DB connection for multi-tenancy
ACCOUNTING_TABLE_PREFIX=acct_
ACCOUNTING_FISCAL_START_MONTH=1         # 1 = January
ACCOUNTING_FISCAL_AUTO_CREATE=true      # auto-create fiscal periods
ACCOUNTING_ENFORCE_PERIOD_LOCK=true     # block posting to closed periods
ACCOUNTING_ROUNDING_TOLERANCE=0.005     # max debit/credit mismatch allowed
ACCOUNTING_ADMIN_ROLES=administrator,admin,superadmin
ACCOUNTING_ADMIN_ROLE_ATTRIBUTE=        # fallback role attribute name

# QuickBooks Online integration (optional)
QBO_CLIENT_ID=
QBO_CLIENT_SECRET=
QBO_REDIRECT_URI=https://yourapp.com/accounting/qbo/callback
QBO_ENVIRONMENT=sandbox                 # sandbox | production
QBO_REALM_ID=                           # QBO Company ID (filled after first OAuth connect)
QBO_WEBHOOK_VERIFIER_TOKEN=             # required only if using QBO webhooks
```

---

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

[](#core-concepts)

### Double-Entry Bookkeeping

[](#double-entry-bookkeeping)

Every `JournalEntry` has two or more lines. Debits must equal credits within the configured `rounding_tolerance` before an entry can be posted.

Account TypeNormal BalanceIncreases withDecreases withAssetDebitDebitCreditLiabilityCreditCreditDebitEquityCreditCreditDebitRevenueCreditCreditDebitExpenseDebitDebitCredit### Journal Entry Lifecycle

[](#journal-entry-lifecycle)

```
Draft ──► Submitted ──► Posted ──► Void
           (review)     (GL impact)

```

- **Draft** — being prepared, no GL impact
- **Submitted** — sent for approval, no GL impact
- **Posted** — hits the General Ledger, affects all reports
- **Void** — cancelled; a separate reversing entry must be used if previously posted

### WAC — Weighted Average Cost

[](#wac--weighted-average-cost)

Used for inventory valuation. Recalculated on every goods-in movement:

```
New WAC = (Current Stock Value + New Purchase Value)
          ────────────────────────────────────────────
                Current Qty + New Purchase Qty

```

**Example:**

EventQtyUnit CostTotal ValueWACOpening100৳ 50.00৳ 5,000৳ 50.00Purchase+50৳ 60.00+৳ 3,000**(5,000+3,000)÷150 = ৳ 53.33**Sale-80৳ 53.33COGS = ৳ 4,266.67৳ 53.33Balance70৳ 53.33৳ 3,733.33৳ 53.33WAC resets on each purchase. FIFO / LIFO batching is not used.

---

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

[](#chart-of-accounts)

```
use Centrex\Accounting\Models\Account;
use Centrex\Accounting\Enums\{AccountType, AccountSubtype};

// Create a custom account
$account = Account::create([
    'code'     => '1310',
    'name'     => 'Raw Materials Inventory',
    'type'     => 'asset',
    'subtype'  => 'current_asset',
    'currency' => 'BDT',
]);

// Create a child account (sub-account)
$sub = Account::create([
    'code'      => '1311',
    'name'      => 'Finished Goods Inventory',
    'type'      => 'asset',
    'subtype'   => 'current_asset',
    'parent_id' => $account->id,
]);

// Retrieve by code
$cash = Account::where('code', '1000')->first();

// Current balance of an account
$balance = $cash->getCurrentBalance(); // float
```

---

Journal Entries — Two-Step Workflow
-----------------------------------

[](#journal-entries--two-step-workflow)

### Standard Flow (with approval)

[](#standard-flow-with-approval)

```
use Centrex\Accounting\Facades\Accounting;
use Centrex\Accounting\Models\JournalEntry;

// Step 1: Accountant creates a draft
$entry = Accounting::createJournalEntry([
    'date'        => '2026-04-30',
    'reference'   => 'RENT-APR-2026',
    'type'        => 'general',      // general | closing | adjustment
    'description' => 'April office rent payment',
    'currency'    => 'BDT',
    'lines' => [
        ['account_id' => $rentExpenseId, 'type' => 'debit',  'amount' => 75000],
        ['account_id' => $bankId,        'type' => 'credit', 'amount' => 75000],
    ],
]);
// $entry->status === JvStatus::DRAFT

// Step 2: Accountant submits for review
$entry->submit();
// $entry->status === JvStatus::SUBMITTED
// submitted_by / submitted_at are now recorded

// Step 3a: Reviewer approves → posted to GL
$entry->post();
// $entry->status === JvStatus::POSTED
// approved_by / approved_at are recorded

// Step 3b: Reviewer rejects → back to draft with a note
$entry->returnToDraft('Wrong expense account — use 5200 for admin costs');
// $entry->status === JvStatus::DRAFT
// $entry->reviewer_note is set
```

### Admin Bypass (post directly from draft)

[](#admin-bypass-post-directly-from-draft)

```
// Users with accounting.journal.post gate can skip the submit step
$entry = Accounting::createJournalEntry([...]);
$entry->post();  // Draft → Posted in one step
```

### Multi-Line Complex Entry

[](#multi-line-complex-entry)

```
// Record a purchase: goods received, tax input credit, and advance payment applied
$entry = Accounting::createJournalEntry([
    'date'        => '2026-04-15',
    'reference'   => 'PO-2026-047',
    'description' => 'Purchase of electronics — Samsung batch',
    'lines' => [
        // What we received
        ['account_id' => $inventoryId,    'type' => 'debit',  'amount' => 500000, 'description' => 'Electronics inventory'],
        ['account_id' => $vatInputId,     'type' => 'debit',  'amount' => 75000,  'description' => 'VAT input credit 15%'],
        // How we paid
        ['account_id' => $advanceToSupId, 'type' => 'credit', 'amount' => 100000, 'description' => 'Advance applied'],
        ['account_id' => $apId,           'type' => 'credit', 'amount' => 475000, 'description' => 'Balance payable to supplier'],
    ],
]);

$entry->submit();
$entry->post();
```

### Void and Reverse a Posted Entry

[](#void-and-reverse-a-posted-entry)

```
// Void (marks as cancelled — does not reverse GL)
$entry->void();

// To reverse the GL effect, post a reversing entry manually
$reversal = Accounting::createJournalEntry([
    'date'        => today(),
    'reference'   => 'REV-' . $entry->entry_number,
    'type'        => 'adjustment',
    'description' => 'Reversal of ' . $entry->description,
    'lines' => $entry->lines->map(fn ($l) => [
        'account_id' => $l->account_id,
        'type'       => $l->type === 'debit' ? 'credit' : 'debit',
        'amount'     => $l->amount,
    ])->toArray(),
]);
$reversal->post();
```

---

Invoices &amp; Payments
-----------------------

[](#invoices--payments)

### Create and Post an Invoice

[](#create-and-post-an-invoice)

```
use Centrex\Accounting\Models\{Customer, Invoice, InvoiceItem};
use Centrex\Accounting\Facades\Accounting;

// Create customer
$customer = Customer::create([
    'code'          => 'CUST-001',
    'name'          => 'Rahman Brothers Ltd',
    'email'         => 'accounts@rahman.com',
    'phone'         => '+880 1711-000000',
    'credit_limit'  => 500000,
    'payment_terms' => 30,
    'currency'      => 'BDT',
]);

// Create invoice
$invoice = Invoice::create([
    'customer_id'     => $customer->id,
    'invoice_date'    => '2026-04-10',
    'due_date'        => '2026-05-10',
    'currency'        => 'BDT',
    'subtotal'        => 200000,
    'tax_amount'      => 30000,
    'discount_amount' => 10000,
    'total'           => 220000,
    'notes'           => 'Payment terms: Net 30. Cheque in favour of ABC Trading.',
]);

// Add line items
$invoice->items()->createMany([
    ['description' => 'Samsung TV 55"',  'quantity' => 10, 'unit_price' => 15000, 'total' => 150000],
    ['description' => 'Samsung Fridge',  'quantity' => 5,  'unit_price' => 10000, 'total' => 50000],
]);

// Post → creates JE: DR Accounts Receivable 1200 / CR Sales Revenue 4000 + CR Sales Tax 2300
$journalEntry = Accounting::postInvoice($invoice);
// Fires: InvoicePosted → SyncCustomerOutstanding
```

### Partial Payment, then Full Settlement

[](#partial-payment-then-full-settlement)

```
// First payment — partial
Accounting::recordInvoicePayment($invoice, [
    'date'      => '2026-04-25',
    'amount'    => 100000,
    'method'    => 'bank_transfer',
    'reference' => 'TT-DHAKA-20260425',
]);
// $invoice->status → 'partially_settled'
// JE: DR Bank 1100 / CR AR 1200

// Second payment — settles the balance
Accounting::recordInvoicePayment($invoice, [
    'date'      => '2026-05-08',
    'amount'    => 120000,
    'method'    => 'cheque',
    'reference' => 'CHQ-00547',
]);
// $invoice->status → 'settled'
```

### Multi-Currency Invoice

[](#multi-currency-invoice)

```
$invoice = Invoice::create([
    'customer_id'   => $exportCustomer->id,
    'invoice_date'  => today(),
    'due_date'      => today()->addDays(60),
    'currency'      => 'USD',
    'exchange_rate' => 110.50,      // 1 USD = 110.50 BDT (base currency)
    'subtotal'      => 5000,        // USD
    'total'         => 5000,
]);

// Post converts to BDT using exchange_rate
// JE posts 5000 × 110.50 = ৳ 552,500 to GL
Accounting::postInvoice($invoice);
```

---

Bills &amp; Vendor Payments
---------------------------

[](#bills--vendor-payments)

```
use Centrex\Accounting\Models\{Vendor, Bill};

$vendor = Vendor::create([
    'code'          => 'VEND-001',
    'name'          => 'Global Electronics Ltd',
    'email'         => 'billing@globalelec.com',
    'payment_terms' => 45,
    'currency'      => 'BDT',
    'tax_id'        => 'BIN-123456789',
]);

$bill = Bill::create([
    'vendor_id'  => $vendor->id,
    'bill_date'  => '2026-04-05',
    'due_date'   => '2026-05-20',
    'subtotal'   => 400000,
    'tax_amount' => 60000,
    'total'      => 460000,
    'notes'      => 'Invoice #GEL-2026-0312',
]);

$bill->items()->createMany([
    ['description' => 'Samsung TV 55" — 30 units',  'quantity' => 30, 'unit_price' => 12000, 'total' => 360000],
    ['description' => 'Samsung Fridge — 8 units',   'quantity' => 8,  'unit_price' => 5000,  'total' => 40000],
]);

// Post → JE: DR Expense 5000 + DR VAT Input 1250 / CR Accounts Payable 2000
Accounting::postBill($bill);

// Pay vendor by bank transfer
Accounting::recordBillPayment($bill, [
    'date'      => '2026-05-15',
    'amount'    => 460000,
    'method'    => 'bank_transfer',
    'reference' => 'TT-OUT-20260515',
]);
// $bill->status → 'settled'
```

---

Invoice &amp; Bill Charges / Discounts
--------------------------------------

[](#invoice--bill-charges--discounts)

The invoice and bill detail pages support recording additional charges and price adjustments with full double-entry journal entries. All amounts are guarded by an AR/AP balance check so payments plus discounts can never exceed the effective receivable or payable balance.

### Invoice Expenses (Delivery / COD)

[](#invoice-expenses-delivery--cod)

Records an additional company-borne expense against a posted invoice — e.g., a delivery fee, courier charge, or cash-on-delivery handling cost. Journal entry: DR Expense account / CR Cash (1000).

AccountCodeUseDelivery Charge4210Standard courier / shipping chargeCash on Delivery Charge4220COD handling feeCourier Bill / Charge6310Courier feeShipping / Transfer Bill (Carriage)6320Freight / carriageHand Carry Delivery6330Hand-carried deliveryDelivery Return Charge6340Return shipment fee```
use Centrex\Accounting\Models\{Account, Expense, Invoice};
use Centrex\Accounting\Facades\Accounting;

$invoice      = Invoice::find($id);
$cashAccount  = Account::where('code', '1000')->first();
$chargeAccount = Account::where('code', '4210')->first(); // or 4220

$expense = Expense::create([
    'chargeable_type' => Invoice::class,
    'chargeable_id'   => $invoice->id,
    'account_id'      => $chargeAccount->id,
    'expense_date'    => today(),
    'subtotal'        => 500,
    'total'           => 500,
    'paid_amount'     => 500,
    'currency'        => 'BDT',
    'status'          => 'paid',
    'payment_method'  => 'cash',
    'reference'       => $invoice->invoice_number,
]);

$entry = Accounting::createJournalEntry([
    'date'        => today(),
    'reference'   => $invoice->invoice_number,
    'type'        => 'general',
    'description' => 'Delivery expense — ' . $invoice->invoice_number,
    'currency'    => 'BDT',
    'lines' => [
        ['account_id' => $chargeAccount->id, 'type' => 'debit',  'amount' => 500, 'description' => 'Delivery Charge'],
        ['account_id' => $cashAccount->id,   'type' => 'credit', 'amount' => 500, 'description' => 'Cash payment for Delivery Charge'],
    ],
]);
$entry->post();
$expense->update(['journal_entry_id' => $entry->id]);
```

The UI equivalent is the **Record Charge** button on the invoice detail page (`/accounting/invoices/{id}`).

### Invoice Discounts (Sales Discount)

[](#invoice-discounts-sales-discount)

Records a price concession granted to the customer, reducing the AR balance. Journal entry: DR Sales Discount (6130) / CR Accounts Receivable (1200).

```
$discountAccount = Account::where('code', '6130')->first();
$arAccount       = Account::where('code', '1200')->first();

$expense = Expense::create([
    'chargeable_type' => Invoice::class,
    'chargeable_id'   => $invoice->id,
    'account_id'      => $discountAccount->id,
    'expense_date'    => today(),
    'subtotal'        => 1000,
    'total'           => 1000,
    'paid_amount'     => 1000,
    'currency'        => 'BDT',
    'status'          => 'paid',
    'payment_method'  => 'cash',
    'reference'       => $invoice->invoice_number,
]);

$entry = Accounting::createJournalEntry([
    'date'        => today(),
    'reference'   => $invoice->invoice_number,
    'type'        => 'general',
    'description' => 'Sales Discount — ' . $invoice->invoice_number,
    'currency'    => 'BDT',
    'lines' => [
        ['account_id' => $discountAccount->id, 'type' => 'debit',  'amount' => 1000, 'description' => 'Sales Discount'],
        ['account_id' => $arAccount->id,       'type' => 'credit', 'amount' => 1000, 'description' => 'Discount applied to AR'],
    ],
]);
$entry->post();
$expense->update(['journal_entry_id' => $entry->id]);
```

AR balance guard — discount amount is validated not to exceed `effective_ar`. The same guard prevents over-payment.

```
effective_ar = invoice->total − invoice->paid_amount − Σdiscounts(6130)

```

---

### Bill Charges (Freight / Shipping)

[](#bill-charges-freight--shipping)

Records an additional vendor cost against a posted bill — e.g., a courier charge or carriage fee.

Journal entry: DR Expense account / CR Cash (1000).

AccountCodeUseCourier Bill / Charge6310Standard courier feeShipping / Transfer Bill (Carriage)6320Freight / sea or land carriageHand Carry Delivery6330Hand-carried goods deliveryDelivery Return Charge6340Return shipment fee```
use Centrex\Accounting\Models\{Account, Bill, Expense};

$bill          = Bill::find($id);
$cashAccount   = Account::where('code', '1000')->first();
$chargeAccount = Account::where('code', '6310')->first(); // or 6320, 6330, 6340

$expense = Expense::create([
    'chargeable_type' => Bill::class,
    'chargeable_id'   => $bill->id,
    'account_id'      => $chargeAccount->id,
    'expense_date'    => today(),
    'subtotal'        => 300,
    'total'           => 300,
    'paid_amount'     => 300,
    'currency'        => 'BDT',
    'status'          => 'paid',
    'payment_method'  => 'cash',
    'reference'       => $bill->bill_number,
]);

$entry = Accounting::createJournalEntry([
    'date'        => today(),
    'reference'   => $bill->bill_number,
    'type'        => 'general',
    'description' => 'Courier Charge — ' . $bill->bill_number,
    'currency'    => 'BDT',
    'lines' => [
        ['account_id' => $chargeAccount->id, 'type' => 'debit',  'amount' => 300, 'description' => 'Courier Charge'],
        ['account_id' => $cashAccount->id,   'type' => 'credit', 'amount' => 300, 'description' => 'Cash payment for Courier Charge'],
    ],
]);
$entry->post();
$expense->update(['journal_entry_id' => $entry->id]);
```

### Bill Discounts (Purchase Discount)

[](#bill-discounts-purchase-discount)

Records a price reduction granted by the vendor, reducing the AP balance. Journal entry: DR Accounts Payable (2000) / CR Purchase Discount (5500).

```
$discountAccount = Account::where('code', '5500')->first();
$apAccount       = Account::where('code', '2000')->first();

$expense = Expense::create([
    'chargeable_type' => Bill::class,
    'chargeable_id'   => $bill->id,
    'account_id'      => $discountAccount->id,
    'expense_date'    => today(),
    'subtotal'        => 2000,
    'total'           => 2000,
    'paid_amount'     => 2000,
    'currency'        => 'BDT',
    'status'          => 'paid',
    'payment_method'  => 'cash',
    'reference'       => $bill->bill_number,
]);

$entry = Accounting::createJournalEntry([
    'date'        => today(),
    'reference'   => $bill->bill_number,
    'type'        => 'general',
    'description' => 'Purchase Discount — ' . $bill->bill_number,
    'currency'    => 'BDT',
    'lines' => [
        ['account_id' => $apAccount->id,       'type' => 'debit',  'amount' => 2000, 'description' => 'Discount applied to AP'],
        ['account_id' => $discountAccount->id, 'type' => 'credit', 'amount' => 2000, 'description' => 'Purchase Discount'],
    ],
]);
$entry->post();
$expense->update(['journal_entry_id' => $entry->id]);
```

AP balance guard — discount amount is validated not to exceed `effective_ap`.

```
effective_ap = bill->total − bill->paid_amount − Σdiscounts(5500)

```

The UI equivalent is the **Record Charge** and **Record Discount** buttons on the bill detail page (`/accounting/bills/{id}`).

### Journal Flow at a Glance

[](#journal-flow-at-a-glance)

EventDRCRInvoice delivery/COD expenseDelivery/COD/Courier expense `4210` / `4220` / `6310–6340`Cash `1000`Invoice sales discountSales Discount `6130`AR `1200`Bill freight chargeCourier/Shipping `6310–6340`Cash `1000`Bill purchase discountAP `2000`Purchase Discount `5500`### Convenience Methods

[](#convenience-methods)

Instead of building the `Expense` + `JournalEntry` manually, you can call the facade helpers which create both in one atomic transaction and auto-post the journal entry:

```
use Centrex\Accounting\Facades\Accounting;
use Centrex\Accounting\Models\{Invoice, Bill};

// Record a delivery charge against an invoice (cash payment)
$expense = Accounting::recordInvoiceExpense($invoice, [
    'account_id'     => $chargeAccountId,  // e.g. Account::where('code','4210')->value('id')
    'amount'         => 500,
    'tax_amount'     => 0,
    'payment_method' => 'cash',            // 'cash' → CR Cash 1000; 'credit' → CR AP 2000
    'date'           => today(),
    'description'    => 'Delivery Charge',
    'reference'      => $invoice->invoice_number,
]);

// Record a freight charge against a bill (on credit / AP)
$expense = Accounting::recordBillExpense($bill, [
    'account_id'     => $freightAccountId, // e.g. Account::where('code','6320')->value('id')
    'amount'         => 300,
    'payment_method' => 'credit',
    'date'           => today(),
]);
```

Both methods return the persisted `Expense` model with `journal_entry_id` already set.

---

Expenses
--------

[](#expenses)

```
use Centrex\Accounting\Models\Expense;

// Cash expense — paid immediately
$expense = Expense::create([
    'account_id'     => $officeSuppliesAccountId,
    'expense_date'   => today(),
    'subtotal'       => 12000,
    'tax_amount'     => 1800,
    'total'          => 13800,
    'payment_method' => 'cash',
    'vendor_name'    => 'Bashundhara City Shop',
    'notes'          => 'Printer cartridges and paper — Q2 office supplies',
]);

$expense->items()->createMany([
    ['description' => 'HP 67 Black Ink — 3 units',  'amount' => 6000],
    ['description' => 'A4 Paper — 10 reams',         'amount' => 6000],
]);

// Post → JE: DR Office Supplies 5400 / CR Cash 1000
Accounting::postExpense($expense);

// Credit expense — pay later
$creditExpense = Expense::create([
    'account_id'     => $marketingAccountId,
    'expense_date'   => today(),
    'due_date'       => today()->addDays(15),
    'total'          => 50000,
    'payment_method' => 'credit',  // creates AP entry
    'vendor_name'    => 'DigiAds Bangladesh',
]);

Accounting::postExpense($creditExpense);
// JE: DR Marketing Expense 5600 / CR Accounts Payable 2000

// Pay when due
Accounting::recordExpensePayment($creditExpense, [
    'date'   => today()->addDays(15),
    'amount' => 50000,
    'method' => 'bank_transfer',
]);
```

---

Requisitions
------------

[](#requisitions)

A **requisition** is a pre-approval request to spend money, raised before a Bill or Expense is created. Two types exist:

- `purchase` — becomes a **Bill** once approved (goods/services from a vendor)
- `expense` — becomes a **Expense** once approved (internal cost on a GL account)

### Lifecycle

[](#lifecycle)

```
Draft ──► Submitted ──► Approved ──► Converted
                  └──► Rejected ──► (closed)

```

### Create and Submit a Requisition

[](#create-and-submit-a-requisition)

```
use Centrex\Accounting\Facades\Accounting;

// Purchase requisition — routed to vendor as a bill
$req = Accounting::createRequisition([
    'type'           => 'purchase',
    'title'          => 'Q3 Office Supplies',
    'vendor_id'      => $vendor->id,
    'requested_by'   => auth()->id(),
    'requested_date' => today(),
    'required_date'  => today()->addDays(7),
    'currency'       => 'BDT',
    'notes'          => 'Urgent: needed before staff training on July 5',
    'items' => [
        ['description' => 'A4 Paper — 20 reams',       'quantity' => 20, 'unit_price' => 300],
        ['description' => 'Whiteboard Markers (set)',   'quantity' => 5,  'unit_price' => 250],
    ],
]);
// $req->status === RequisitionStatus::DRAFT

// Expense requisition — routed to GL account as an expense
$expReq = Accounting::createRequisition([
    'type'           => 'expense',
    'title'          => 'Team lunch — Q2 close',
    'account_id'     => $entertainmentAccountId,
    'requested_by'   => auth()->id(),
    'requested_date' => today(),
    'currency'       => 'BDT',
    'items' => [
        ['description' => 'Team lunch at La Bella', 'quantity' => 1, 'unit_price' => 8500],
    ],
]);

// Requester submits for manager approval
Accounting::submitRequisition($req);
// $req->status === RequisitionStatus::SUBMITTED
```

### Approve, Reject, or Convert

[](#approve-reject-or-convert)

```
// Manager approves
Accounting::approveRequisition($req, auth()->id());
// $req->status === RequisitionStatus::APPROVED

// Manager rejects (sends back with reason)
Accounting::rejectRequisition($req, 'Over budget — reduce to 10 reams of paper.', auth()->id());
// $req->status === RequisitionStatus::REJECTED

// Convert approved purchase requisition → Bill (items map 1-to-1)
$bill = Accounting::convertRequisitionToBill($req);
// $req->status → RequisitionStatus::CONVERTED
// $bill->status === 'draft' — ready to post via Accounting::postBill($bill)

// Convert approved expense requisition → Expense
$expense = Accounting::convertRequisitionToExpense($expReq);
// $expense->status === 'draft', payment_method = 'credit'
// Ready to post via Accounting::postExpense($expense)
```

### Enums

[](#enums)

EnumCases`RequisitionStatus`DRAFT, SUBMITTED, APPROVED, REJECTED, CONVERTED`RequisitionType`PURCHASE (`purchase`), EXPENSE (`expense`)---

Customer &amp; Vendor Ledger
----------------------------

[](#customer--vendor-ledger)

### Per-Entity Ledger (Statement of Account)

[](#per-entity-ledger-statement-of-account)

```
use Centrex\Accounting\Models\Customer;

// Navigate to the individual ledger page via web UI:
// GET /accounting/customers/{customer}/ledger?startDate=2026-01-01&endDate=2026-04-30

// Or query the data programmatically:
$customer = Customer::find($id);

// Outstanding balance attribute (active invoices only)
$outstanding = $customer->total_outstanding; // float — sum of (total - paid_amount) for issued/partial/overdue invoices
```

### Ledger Index (All Customers / Vendors)

[](#ledger-index-all-customers--vendors)

```
GET /accounting/ledger/customers   — paginated list with outstanding per customer
GET /accounting/ledger/vendors     — paginated list with outstanding per vendor

```

Each index row shows: total invoiced, total received, and net outstanding for active (issued/partial/overdue) documents only.

---

General Ledger
--------------

[](#general-ledger)

```
use Centrex\Accounting\Facades\Accounting;

// All accounts, full year
$gl = Accounting::getGeneralLedger(
    accountId: null,
    startDate: '2026-01-01',
    endDate:   '2026-04-30',
    sbuCode:   null,
);

// Single account with SBU filter
$gl = Accounting::getGeneralLedger(
    accountId: $bankAccountId,
    startDate: '2026-04-01',
    endDate:   '2026-04-30',
    sbuCode:   'OCT',           // Strategic Business Unit code
);

// Response structure
foreach ($gl['accounts'] as $section) {
    $section['account'];           // Account model
    $section['opening_balance'];   // float — balance before start date
    $section['period_debits'];     // float
    $section['period_credits'];    // float
    $section['closing_balance'];   // float
    $section['entries'];           // array of posted GL lines with running_balance
}
```

---

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

[](#financial-reports)

```
use Centrex\Accounting\Facades\Accounting;

// Trial Balance — checks if all posted entries balance
$tb = Accounting::getTrialBalance('2026-01-01', '2026-04-30');
// [
//   'accounts'      => [['account' => ..., 'debit' => x, 'credit' => x, 'balance' => x], ...],
//   'total_debits'  => 1250000.00,
//   'total_credits' => 1250000.00,
//   'is_balanced'   => true,
// ]

// Balance Sheet — point-in-time snapshot
$bs = Accounting::getBalanceSheet('2026-04-30');
// [
//   'assets'      => ['accounts' => [...], 'total' => 8500000.00],
//   'liabilities' => ['accounts' => [...], 'total' => 2300000.00],
//   'equity'      => ['accounts' => [...], 'total' => 5870000.00, 'total_with_income' => 6200000.00],
//   'is_balanced' => true,
// ]

// Income Statement (P&L)
$pl = Accounting::getIncomeStatement('2026-04-01', '2026-04-30');
// [
//   'revenue'      => ['accounts' => [...], 'total' => 1500000.00],
//   'expenses'     => ['accounts' => [...], 'total' => 1170000.00],
//   'gross_profit' => 330000.00,
//   'net_income'   => 330000.00,
// ]

// Cash Flow Statement
$cf = Accounting::getCashFlowStatement('2026-04-01', '2026-04-30');
// [
//   'operating_activities'  => 280000.00,
//   'investing_activities'  => -50000.00,
//   'financing_activities'  => 0.00,
//   'net_change'            => 230000.00,
// ]

// A/R Aging — QBO-compatible buckets (current, 1-30, 31-60, 61-90, 91+)
$arAging = Accounting::getArAging('2026-04-30');
// [
//   'as_of_date' => '2026-04-30',
//   'rows' => [
//     ['name' => 'Acme Corp', 'current' => 5000.00, '1_30' => 1200.00, '31_60' => 0.00, '61_90' => 800.00, 'over_90' => 0.00, 'total' => 7000.00],
//   ],
//   'totals' => ['current' => 5000.00, '1_30' => 1200.00, '31_60' => 0.00, '61_90' => 800.00, 'over_90' => 0.00, 'total' => 7000.00],
// ]

// A/P Aging
$apAging = Accounting::getApAging('2026-04-30');
```

---

SBU (Cost Centre) Filtering
---------------------------

[](#sbu-cost-centre-filtering)

All reports and the General Ledger accept a `sbuCode` parameter to filter by Strategic Business Unit. The SBU code is stored on each `JournalEntry` row.

```
// Manual SBU tagging
$entry = Accounting::createJournalEntry([
    'date'        => today(),
    'description' => 'Dhaka branch rent',
    'sbu_code'    => 'DHK',
    'lines'       => [...],
]);

// Reports filtered by SBU
$pl  = Accounting::getIncomeStatement('2026-01-01', '2026-04-30', sbuCode: 'DHK');
$gl  = Accounting::getGeneralLedger(null, '2026-04-01', '2026-04-30', sbuCode: 'DHK');
$tb  = Accounting::getTrialBalance('2026-01-01', '2026-04-30', sbuCode: 'DHK');
$bs  = Accounting::getBalanceSheet('2026-04-30', sbuCode: 'DHK');
```

---

Budgets
-------

[](#budgets)

```
use Centrex\Accounting\Facades\Accounting;
use Centrex\Accounting\Models\FiscalYear;

$fy = FiscalYear::where('is_current', true)->first();

$budget = Accounting::createBudget([
    'name'         => 'Q2 2026 Operating Budget',
    'fiscal_year_id' => $fy->id,
    'period_start' => '2026-04-01',
    'period_end'   => '2026-06-30',
    'currency'     => 'BDT',
    'items' => [
        ['account_id' => $rentExpenseId,     'description' => 'Office rent Q2',      'amount' => 225000],
        ['account_id' => $salariesId,         'description' => 'Staff salaries Q2',   'amount' => 900000],
        ['account_id' => $marketingId,        'description' => 'Digital marketing',   'amount' => 150000],
        ['account_id' => $officeSuppliesId,   'description' => 'Office supplies',     'amount' => 36000],
    ],
]);

// Approve the budget
Accounting::approveBudget($budget, auth()->id());

// Budget vs Actual comparison
$comparison = Accounting::getBudgetVsActual($budget);
foreach ($comparison['items'] as $item) {
    echo $item['account']->name . ': ';
    echo 'Budget ৳' . number_format($item['budgeted']) . ', ';
    echo 'Actual ৳' . number_format($item['actual'])   . ', ';
    echo 'Remaining ৳' . number_format($item['remaining']) . ' ';
    echo '(' . $item['percentage_used'] . '% used)' . PHP_EOL;
}
```

---

Period Closing (Month-End)
--------------------------

[](#period-closing-month-end)

### Pre-Close Checks

[](#pre-close-checks)

```
use Centrex\Accounting\Facades\Accounting;
use Centrex\Accounting\Models\FiscalPeriod;

$period = FiscalPeriod::where('name', 'April 2026')->first();

$checks = Accounting::getPeriodCloseChecks($period);
// [
//   'unposted_journals' => 2,   // ← BLOCKER: must be 0 before closing
//   'open_invoices'     => 5,   // warning only (still editable after close)
//   'open_bills'        => 1,   // warning only
//   'has_blockers'      => true,
//   'has_warnings'      => true,
// ]

if ($checks['has_blockers']) {
    // Resolve the 2 unposted entries first
}
```

### Close the Period

[](#close-the-period)

```
$result = Accounting::closeFiscalPeriod(
    period: $period,
    snapshotInventory: true,   // requires laravel-inventory with ERP bridge enabled
);

// $result['period']    — the now-closed FiscalPeriod model
// $result['inventory'] — inventory reconciliation data (or null)

if ($result['inventory']) {
    $inv = $result['inventory'];
    echo "Inventory snapshot: {$inv['snapshot_count']} lines captured\n";
    echo "Physical value:  ৳ " . number_format($inv['physical_value'], 2) . "\n";
    echo "GL balance (1300): ৳ " . number_format($inv['gl_balance'], 2) . "\n";
    echo "Variance:          ৳ " . number_format($inv['variance'], 2) . "\n";
    echo "Reconciled: " . ($inv['is_reconciled'] ? 'YES ✓' : 'NO — post an adjustment') . "\n";
}
```

After closing, the period is **locked**. Attempting to post a journal entry dated within a closed period throws `AccountingException`:

```
// ACCOUNTING_ENFORCE_PERIOD_LOCK=true (default)
$entry->post();
// ✗ AccountingException: Cannot post to a closed period.
//   Entry date 2026-04-15 falls in a locked accounting period.

// Internal closing/adjusting entries bypass the lock automatically
// (closeFiscalYear() and closeFiscalPeriod() call post(bypassPeriodLock: true) internally)
```

To disable the lock temporarily for data migrations:

```
ACCOUNTING_ENFORCE_PERIOD_LOCK=false
```

---

Fiscal Year Closing
-------------------

[](#fiscal-year-closing)

```
use Centrex\Accounting\Models\FiscalYear;
use Centrex\Accounting\Facades\Accounting;

$fy = FiscalYear::where('name', '2025-26')->first();

Accounting::closeFiscalYear($fy);
// 1. Calculates net income for the year
// 2. Creates closing JE: DR Income Summary (3900) / CR Retained Earnings (3100)
// 3. Posts the entry (bypasses period lock)
// 4. Sets fiscal_year.is_closed = true
```

---

Inventory Financing
-------------------

[](#inventory-financing)

Inventory financing is a revolving short-term credit facility where a lender advances funds specifically to purchase stock. The inventory itself serves as collateral. Each lender is registered as a **facility** and receives its own pair of dedicated GL sub-accounts, keeping balances fully isolated per lender.

### Account Structure

[](#account-structure)

`initializeChartOfAccounts()` seeds these parent accounts automatically:

CodeNameTypeNotes`2150`Inventory Financing PayableLiabilityParent — never posted to directly`2151–2169`Per-lender payableLiabilityAuto-created on `addFinancingFacility()``2170`Accrued Interest — Inventory FinancingLiabilityParent — never posted to directly`2171–2189`Per-lender accrued interestLiabilityAuto-created on `addFinancingFacility()``6710`Interest Expense — Inventory FinancingExpenseChild of `6700 Interest Expense`### Register Lenders

[](#register-lenders)

Each call to `addFinancingFacility()` creates the facility record and allocates the next available sub-account codes under `2150` and `2170`:

```
use Centrex\Accounting\Facades\Accounting;

// Lender 1 — bank (gets 2151 + 2171)
$brac = Accounting::addFinancingFacility(
    lenderName:  'BRAC Bank Ltd',
    lenderType:  'bank',
    monthlyRate: 0.02,          // 2% per month
    creditLimit: 5_000_000.00,  // ৳50 lakh ceiling
    contact:     'Md. Hasan, 01700-000001',
);

// Lender 2 — private party (gets 2152 + 2172)
$karim = Accounting::addFinancingFacility(
    lenderName:  'Mr. Abdul Karim',
    lenderType:  'private',
    monthlyRate: 0.02,
    creditLimit: 1_500_000.00,
);

// Lender 3 — microfinance institution (gets 2153 + 2173)
$buro = Accounting::addFinancingFacility(
    lenderName:  'BURO Bangladesh',
    lenderType:  'mfi',
    monthlyRate: 0.02,
    creditLimit: 800_000.00,
);
```

### Draw Down Funds to Purchase Inventory

[](#draw-down-funds-to-purchase-inventory)

Each draw-down posts immediately as a draft journal entry. Submit and approve via the normal two-step workflow:

```
// BRAC Bank advances ৳20,00,000 for Samsung batch purchase
$entry = Accounting::drawdownFinancing(
    facility:    $brac,
    amount:      2_000_000.00,
    date:        '2026-04-05',
    reference:   'BRAC-DD-2026-001',
    description: 'Samsung Galaxy A-series batch — PO-2026-047',
);
$entry->submit();    // accountant submits for review
$entry->post();      // finance manager approves
// DR Inventory 1300  ৳20,00,000
// CR BRAC Bank Payable 2151  ৳20,00,000

// Private party advances ৳8,00,000 for accessories stock
$entry2 = Accounting::drawdownFinancing(
    facility:  $karim,
    amount:    800_000.00,
    date:      '2026-04-10',
    reference: 'KARIM-DD-2026-001',
);
$entry2->submit();
$entry2->post();
// DR Inventory 1300  ৳8,00,000
// CR Mr. Abdul Karim Payable 2152  ৳8,00,000
```

A second draw-down from the same lender stacks on top:

```
// Mid-month top-up from BRAC Bank
Accounting::drawdownFinancing($brac, 500_000.00, '2026-04-18', 'BRAC-DD-2026-002')
    ->submit();
// Outstanding under BRAC: 2151 balance = ৳25,00,000
// Attempting to exceed credit limit throws RuntimeException
```

### Month-End Interest Accrual

[](#month-end-interest-accrual)

Run once at month-end for all active facilities in a single call, or accrue per-facility for fine-grained control:

```
// Accrue all active facilities at once (run via scheduler on the 28th)
$results = Accounting::accrueAllFinancingInterest(date: '2026-04-30');
// Returns: [facility_id => JournalEntry|null]

// Per-facility breakdown:
// BRAC Bank — principal ৳25,00,000 × 2% = ৳50,000
//   DR Interest Expense — Inv. Financing 6710  ৳50,000
//   CR Accrued Interest — BRAC Bank 2171       ৳50,000

// Mr. Karim — principal ৳8,00,000 × 2% = ৳16,000
//   DR Interest Expense — Inv. Financing 6710  ৳16,000
//   CR Accrued Interest — Mr. Abdul Karim 2172 ৳16,000

// Accrue a single facility manually (for corrections/adjustments)
$je = Accounting::accrueFinancingInterest($brac, date: '2026-04-30');
// Returns null and skips cleanly if outstanding principal is zero
```

Automate via the scheduler so it never gets missed:

```
// routes/console.php
Schedule::call(fn () => Accounting::accrueAllFinancingInterest())
    ->monthlyOn(28, '23:00')
    ->name('accounting:accrue-inventory-interest')
    ->withoutOverlapping();
```

### Pay the Interest

[](#pay-the-interest)

```
// Pay BRAC Bank interest for April
Accounting::payFinancingInterest(
    facility:  $brac,
    amount:    50_000.00,
    date:      '2026-05-05',
    reference: 'BRAC-INT-APR-2026',
);
// DR Accrued Interest — BRAC Bank 2171  ৳50,000
// CR Bank Account 1100                  ৳50,000

// Pay private party interest
Accounting::payFinancingInterest($karim, 16_000.00, '2026-05-05', 'KARIM-INT-APR-2026');
```

### Repay Principal as Inventory Sells

[](#repay-principal-as-inventory-sells)

Repay each lender as the financed goods are sold and cash comes in. The method validates the amount does not exceed outstanding principal:

```
// ৳10,00,000 of Samsung stock sold — repay BRAC proportionally
Accounting::repayFinancing(
    facility:  $brac,
    amount:    1_000_000.00,
    date:      '2026-05-10',
    reference: 'BRAC-REPAY-2026-001',
);
// DR BRAC Bank Payable 2151  ৳10,00,000
// CR Bank Account 1100       ৳10,00,000
// Remaining BRAC balance: 2151 = ৳15,00,000

// Full repayment of private party facility
Accounting::repayFinancing($karim, 800_000.00, '2026-05-15', 'KARIM-REPAY-2026-001');
// 2152 balance = ৳0 — facility fully settled
```

### Portfolio Summary

[](#portfolio-summary)

```
$summary = Accounting::getFinancingSummary();

// Returns per-facility:
// [
//   'lender_name'           => 'BRAC Bank Ltd',
//   'lender_type'           => 'bank',
//   'is_active'             => true,
//   'monthly_rate'          => 0.02,
//   'credit_limit'          => 5000000.0,
//   'outstanding_principal' => 1500000.0,   // ৳15,00,000 remaining
//   'accrued_interest'      => 0.0,         // paid already
//   'monthly_interest'      => 30000.0,     // next month estimate
//   'principal_account'     => '2151 Inv. Financing Payable — BRAC Bank Ltd',
//   'interest_account'      => '2171 Accrued Interest — BRAC Bank Ltd',
// ],
// [
//   'lender_name'           => 'Mr. Abdul Karim',
//   'outstanding_principal' => 0.0,         // fully repaid
//   'monthly_interest'      => 0.0,
//   ...
// ],
```

### Pre-Close Check Integration

[](#pre-close-check-integration)

The period-close wizard automatically flags financing imbalances before locking the period:

```
$checks = Accounting::getPeriodCloseChecks($period);

// Example blocker if financing > inventory value:
// [
//   'type'    => 'blocker',
//   'message' => 'Inventory financing (৳28,00,000) exceeds inventory GL balance (৳24,50,000).
//                 Possible unrecorded goods receipt or over-draw.',
// ]
```

### Journal Flow at a Glance

[](#journal-flow-at-a-glance-1)

EventDRCRDraw down (buy inventory)Inventory `1300`Lender Payable `215x`Month-end interest accrualInterest Expense `6710`Accrued Interest `217x`Pay interestAccrued Interest `217x`Bank `1100`Repay principalLender Payable `215x`Bank `1100`---

Organizational Loans &amp; SBU-wise Tracking
--------------------------------------------

[](#organizational-loans--sbu-wise-tracking)

Covers every loan a company takes from an external lender or an internal entity — term loans, working-capital lines, inter-company advances, director loans, equipment finance, and overdraft facilities. Each loan is a **facility** with its own GL sub-accounts. The `sbu_code` field on the facility tags every journal entry automatically, so SBU-filtered P&amp;L and balance sheets work without any extra steps.

### Loan Types

[](#loan-types)

`loan_type`Typical use`term_loan`Fixed-amount, fixed-tenure bank loan`working_capital`Short-term revolving credit for operations`inter_company`Advance from / to a sister or parent company`director`Unsecured loan from a director / shareholder`equipment`Asset-backed financing for machinery or vehicles`overdraft`Bank overdraft against current account`bridge`Short-term bridge pending long-term funding### Account Structure

[](#account-structure-1)

`initializeChartOfAccounts()` seeds these parent accounts automatically:

CodeNameTermNotes`2400`Short-term Loans PayableShortParent — never posted to directly`2401–2419`Per-facility payableShortAuto-created on `addLoanFacility()``2420`Accrued Interest — Short-term LoansShortParent`2421–2439`Per-facility accrued interestShortAuto-created`2500`Long-term Loans PayableLongParent`2501–2519`Per-facility payableLongAuto-created`2520`Accrued Interest — Long-term LoansLongParent`2521–2539`Per-facility accrued interestLongAuto-created`6720`Interest Expense — Short-term Loans—Child of `6700``6730`Interest Expense — Long-term Loans—Child of `6700`The `loan_term` field drives which range is used: `short_term` (&lt; 12 months) → `240x/242x`; `long_term` (≥ 12 months) → `250x/252x`.

---

### Register Loan Facilities

[](#register-loan-facilities)

```
use Centrex\Accounting\Facades\Accounting;

// 1. Short-term working-capital line from Dutch-Bangla Bank — North SBU
$wcLine = Accounting::addLoanFacility(
    lenderName:    'Dutch-Bangla Bank Ltd',
    loanType:      'working_capital',
    loanTerm:      'short_term',
    monthlyRate:   0.02,                // 2% per month
    sbuCode:       'NORTH',             // all JEs tagged NORTH
    loanAmount:    3_000_000.00,
    disbursedAt:   '2026-04-01',
    dueAt:         '2026-10-01',
    tenureMonths:  6,
    contact:       'Branch Manager, Gulshan',
);
// → creates 2401 "Working Capital Payable — Dutch-Bangla Bank Ltd"
// → creates 2421 "Accrued Interest — Dutch-Bangla Bank Ltd"

// 2. Long-term term loan from BRAC Bank — South SBU
$termLoan = Accounting::addLoanFacility(
    lenderName:   'BRAC Bank Ltd',
    loanType:     'term_loan',
    loanTerm:     'long_term',
    monthlyRate:  0.02,
    sbuCode:      'SOUTH',
    loanAmount:   10_000_000.00,
    disbursedAt:  '2026-01-01',
    dueAt:        '2028-12-31',
    tenureMonths: 36,
);
// → creates 2501 "Term Loan Payable — BRAC Bank Ltd"
// → creates 2521 "Accrued Interest — BRAC Bank Ltd"

// 3. Inter-company advance from parent — Head Office SBU
$icLoan = Accounting::addLoanFacility(
    lenderName:  'ABC Holdings Ltd (Parent)',
    loanType:    'inter_company',
    loanTerm:    'short_term',
    monthlyRate: 0.02,
    sbuCode:     'HO',
    loanAmount:  5_000_000.00,
);
// → creates 2402 / 2422

// 4. Director loan — no SBU (company-wide)
$directorLoan = Accounting::addLoanFacility(
    lenderName:  'Mr. Rahim (Director)',
    loanType:    'director',
    loanTerm:    'short_term',
    monthlyRate: 0.02,
    loanAmount:  1_000_000.00,
);
// → creates 2403 / 2423 — sbu_code = null, JEs carry no SBU tag
```

---

### Drawdown — Receive Loan Proceeds

[](#drawdown--receive-loan-proceeds)

```
// Dutch-Bangla working-capital: ৳30,00,000 received into bank
$entry = Accounting::drawdownLoan(
    facility:  $wcLine,
    amount:    3_000_000.00,
    date:      '2026-04-01',
    reference: 'DBBL-WC-2026-001',
);
$entry->submit();
$entry->post();
// DR Bank 1100               ৳30,00,000   [sbu_code = NORTH]
// CR Working Capital Payable 2401  ৳30,00,000

// BRAC term loan — first tranche ৳60,00,000
Accounting::drawdownLoan($termLoan, 6_000_000.00, '2026-04-01', 'BRAC-TL-2026-T1')
    ->submit();
// DR Bank 1100              ৳60,00,000   [sbu_code = SOUTH]
// CR Term Loan Payable 2501  ৳60,00,000

// Second tranche ৳40,00,000 a month later
Accounting::drawdownLoan($termLoan, 4_000_000.00, '2026-05-01', 'BRAC-TL-2026-T2')
    ->submit();
// Total outstanding on 2501: ৳1,00,00,000

// Inter-company advance — override SBU at draw-down level if needed
Accounting::drawdownLoan(
    facility: $icLoan,
    amount:   5_000_000.00,
    date:     '2026-04-10',
    reference: 'IC-ADV-2026-001',
    sbuCode:  'EAST',   // overrides facility-level 'HO' for this specific entry
);
```

---

### Month-End Interest Accrual

[](#month-end-interest-accrual-1)

```
// Accrue all active loan facilities in one call
$results = Accounting::accrueAllLoanInterest(date: '2026-04-30');

// Facility-by-facility breakdown:
// Dutch-Bangla WC: ৳30,00,000 × 2% = ৳60,000  [NORTH]
//   DR Interest Expense — Short-term 6720  ৳60,000
//   CR Accrued Interest — DBBL 2421         ৳60,000

// BRAC Term Loan: ৳60,00,000 × 2% = ৳1,20,000  [SOUTH]
//   DR Interest Expense — Long-term 6730    ৳1,20,000
//   CR Accrued Interest — BRAC 2521          ৳1,20,000

// Director loan: ৳10,00,000 × 2% = ৳20,000  [no SBU]
//   DR Interest Expense — Short-term 6720   ৳20,000
//   CR Accrued Interest — Mr. Rahim 2423     ৳20,000

foreach ($results as $facilityId => $je) {
    if ($je) {
        $je->submit();
        $je->post();
    }
}

// Accrue a single facility (e.g., missed in bulk run)
$je = Accounting::accrueLoanInterest($icLoan, date: '2026-04-30');

// Scheduler — run on the 28th of each month at 11 PM
Schedule::call(fn () => Accounting::accrueAllLoanInterest())
    ->monthlyOn(28, '23:00')
    ->name('accounting:accrue-loan-interest')
    ->withoutOverlapping();
```

---

### Pay Interest

[](#pay-interest)

```
// Pay Dutch-Bangla interest for April
Accounting::payLoanInterest(
    facility:  $wcLine,
    amount:    60_000.00,
    date:      '2026-05-05',
    reference: 'DBBL-INT-APR26',
);
// DR Accrued Interest — DBBL 2421  ৳60,000   [sbu_code = NORTH]
// CR Bank 1100                      ৳60,000

// Pay BRAC interest
Accounting::payLoanInterest($termLoan, 120_000.00, '2026-05-05', 'BRAC-INT-APR26');
```

---

### Repay Principal

[](#repay-principal)

```
// Monthly instalment on working-capital line: ৳5,00,000
Accounting::repayLoan(
    facility:  $wcLine,
    amount:    500_000.00,
    date:      '2026-05-01',
    reference: 'DBBL-REPAY-APR26',
);
// DR Working Capital Payable 2401  ৳5,00,000   [sbu_code = NORTH]
// CR Bank 1100                      ৳5,00,000

// Full director loan repayment
Accounting::repayLoan($directorLoan, 1_000_000.00, '2026-05-15', 'DIR-REPAY-2026');
// 2403 balance = ৳0 — facility fully settled
```

---

### SBU-wise Loan Portfolio

[](#sbu-wise-loan-portfolio)

```
// All facilities, all SBUs
$all = Accounting::getLoanSummary();

// Only NORTH SBU
$north = Accounting::getLoanSummary(sbuCode: 'NORTH');

// Returns per facility:
// [
//   'lender_name'           => 'Dutch-Bangla Bank Ltd',
//   'loan_type'             => 'working_capital',
//   'loan_term'             => 'short_term',
//   'sbu_code'              => 'NORTH',
//   'monthly_rate'          => 0.02,
//   'loan_amount'           => 3000000.0,
//   'disbursed_at'          => '2026-04-01',
//   'due_at'                => '2026-10-01',
//   'months_remaining'      => 5,
//   'outstanding_principal' => 2500000.0,   // after one instalment
//   'accrued_interest'      => 0.0,         // paid
//   'monthly_interest'      => 50000.0,     // next estimate on reduced balance
//   'principal_account'     => '2401 Working Capital Payable — Dutch-Bangla Bank Ltd',
//   'interest_account'      => '2421 Accrued Interest — Dutch-Bangla Bank Ltd',
// ]
```

---

### SBU-Filtered Financial Reports

[](#sbu-filtered-financial-reports)

Because every drawdown, accrual, and repayment journal entry is tagged with `sbu_code`, the standard report methods filter correctly with no extra work:

```
// NORTH SBU income statement — interest expense attributed correctly
$pl = Accounting::getIncomeStatement('2026-04-01', '2026-04-30', sbuCode: 'NORTH');
// expenses will include ৳60,000 interest on the Dutch-Bangla WC line

// SOUTH SBU balance sheet — term loan liability appears under long-term
$bs = Accounting::getBalanceSheet('2026-04-30', sbuCode: 'SOUTH');
// liabilities will include 2501 = ৳1,00,00,000 (both tranches)

// Head-office consolidated trial balance (no SBU filter)
$tb = Accounting::getTrialBalance('2026-04-01', '2026-04-30');
// Includes all SBUs — 2400-parent and 2500-parent roll up correctly
```

---

### Journal Flow at a Glance

[](#journal-flow-at-a-glance-2)

EventDRCRSBU tagReceive loan proceedsBank `1100`Loan Payable `240x`/`250x`Facility SBUMonth-end interest accrualInterest Expense `6720`/`6730`Accrued Interest `242x`/`252x`Facility SBUPay interestAccrued Interest `242x`/`252x`Bank `1100`Facility SBURepay principalLoan Payable `240x`/`250x`Bank `1100`Facility SBU---

Real-World Example — Trading Company Month-End Close
----------------------------------------------------

[](#real-world-example--trading-company-month-end-close)

**Scenario:** ABC Trading Ltd imports electronics from South Korea and sells in Bangladesh. The company carries two active inventory financing facilities — BRAC Bank (৳50 lakh limit, 2%/month) and a private party Mr. Karim (৳15 lakh limit, 2%/month). April 2026 had 80 purchase transactions, 140 sales orders, and 6 stock adjustments.

### Week 4 Pre-Close Workflow (Days 27–30)

[](#week-4-pre-close-workflow-days-2730)

#### Day 27–28: Cut-Off Procedures

[](#day-2728-cut-off-procedures)

Ensure all economic events before April 30 are captured:

```
// 1. Every goods received note (GRN) for April must be posted
//    → JE: DR Inventory 1300 ৳ 4,218,500 / CR Accounts Payable 2000 ৳ 4,218,500

// 2. All April sale fulfillments posted
//    → JE: DR COGS 5000 ৳ 2,940,000 / CR Inventory 1300 ৳ 2,940,000

// 3. All customer invoices for April shipments issued
//    → JE: DR AR 1200 ৳ 5,100,000 / CR Sales Revenue 4000 ৳ 4,500,000
//                                   / CR VAT Payable 2300  ৳   600,000

// 4. All vendor bills for April purchases posted
//    → JE: DR Inventory 1300 ৳ 4,200,000 / DR VAT Input ৳ 630,000
//         / CR Accounts Payable 2000 ৳ 4,830,000

// 5. Bank receipts from customers posted
Accounting::recordInvoicePayment($invoice, [
    'date' => '2026-04-28', 'amount' => 2800000, 'method' => 'bank_transfer',
]);

// 6. Vendor payments posted
Accounting::recordBillPayment($bill, [
    'date' => '2026-04-28', 'amount' => 3200000, 'method' => 'bank_transfer',
]);
```

#### Day 29: Manual Adjusting Entries

[](#day-29-manual-adjusting-entries)

```
// Accrual: April electricity bill — received but not invoiced yet
$entry = Accounting::createJournalEntry([
    'date'        => '2026-04-30',
    'reference'   => 'ACCR-UTIL-APR26',
    'type'        => 'adjustment',
    'description' => 'Accrual — April utilities bill (estimated)',
    'lines' => [
        ['account_id' => $utilitiesExpenseId, 'type' => 'debit',  'amount' => 18000],
        ['account_id' => $accruedLiabId,      'type' => 'credit', 'amount' => 18000],
    ],
]);
$entry->submit();
$entry->post(); // Approved by Finance Manager

// Depreciation: Monthly depreciation for equipment
$depEntry = Accounting::createJournalEntry([
    'date'        => '2026-04-30',
    'reference'   => 'DEP-APR26',
    'type'        => 'adjustment',
    'description' => 'Monthly depreciation — warehouse equipment',
    'lines' => [
        ['account_id' => $depreciationExpenseId, 'type' => 'debit',  'amount' => 25000],
        ['account_id' => $accumDepreciationId,   'type' => 'credit', 'amount' => 25000],
    ],
]);
$depEntry->submit();
$depEntry->post();

// Inventory financing interest accrual — all active facilities
// BRAC Bank: outstanding ৳38,00,000 × 2% = ৳76,000
// Mr. Karim:  outstanding ৳12,00,000 × 2% = ৳24,000
$accruals = Accounting::accrueAllFinancingInterest(date: '2026-04-30');

foreach ($accruals as $facilityId => $je) {
    if ($je) {
        $je->submit();
        $je->post(); // Finance Manager approves each
    }
}
// Total interest expense booked: ৳1,00,000
// DR Interest Expense — Inv. Financing 6710  ৳1,00,000
// CR Accrued Interest — BRAC Bank 2171        ৳76,000
// CR Accrued Interest — Mr. Abdul Karim 2172  ৳24,000
```

#### Day 30: Run Pre-Close Checks

[](#day-30-run-pre-close-checks)

```
$period = FiscalPeriod::where('name', 'April 2026')->first();

$checks = Accounting::getPeriodCloseChecks($period);
// unposted_journals: 0  ✓
// open_invoices:     3  (3 customers haven't paid — OK, will carry forward)
// open_bills:        1  (1 vendor bill payment due May 15 — OK)
// has_blockers:      false  ✓  Ready to close
```

#### Day 30: Review Income Statement Before Closing

[](#day-30-review-income-statement-before-closing)

```
$pl = Accounting::getIncomeStatement('2026-04-01', '2026-04-30');

// Revenue
//   Sales Revenue (4000):    ৳ 51,00,000
//   Total Revenue:           ৳ 51,00,000

// Expenses
//   COGS (5000):             ৳ 29,40,000
//   Office Rent (5100):      ৳    75,000
//   Salaries (5200):         ৳  3,00,000
//   Marketing (5600):        ৳    50,000
//   Utilities (5700):        ৳    18,000
//   Depreciation (5800):     ৳    25,000
//   Total Expenses:          ৳ 34,08,000

// Net Profit for April 2026: ৳ 16,92,000  ✓
```

#### Day 30: Physical Inventory Count

[](#day-30-physical-inventory-count)

Warehouse team counts all items and updates system stock levels. A 3-unit shrinkage of Samsung TVs (WAC ৳ 12,500 each) is found:

```
$shrinkageEntry = Accounting::createJournalEntry([
    'date'        => '2026-04-30',
    'reference'   => 'SHRINK-APR26-TV',
    'type'        => 'adjustment',
    'description' => 'Inventory shrinkage — 3× Samsung TV 55" (physical count variance)',
    'lines' => [
        ['account_id' => $inventoryShrinkageId, 'type' => 'debit',  'amount' => 37500],
        ['account_id' => $inventoryAssetId,     'type' => 'credit', 'amount' => 37500],
    ],
]);
$shrinkageEntry->submit();
$shrinkageEntry->post();
```

#### Day 30: Close the Period + Inventory Snapshot

[](#day-30-close-the-period--inventory-snapshot)

```
$result = Accounting::closeFiscalPeriod($period, snapshotInventory: true);

// Inventory reconciliation output:
// Snapshot lines:    427 (product × warehouse combinations)
// Physical value:    ৳ 4,21,85,000.00
// GL balance (1300): ৳ 4,21,85,000.00
// Variance:          ৳          0.00  ✓ Reconciled

// Period is now LOCKED — no more entries can be posted with April dates
```

#### Final Balance Sheet — April 30, 2026

[](#final-balance-sheet--april-30-2026)

```
$bs = Accounting::getBalanceSheet('2026-04-30');

// ASSETS
//   Cash (1000):               ৳  45,00,000
//   Bank (1100):               ৳  82,50,000
//   Accounts Receivable (1200):৳  24,60,000   (3 open invoices)
//   Inventory (1300):          ৳ 4,21,85,000  (verified by snapshot)
//   Total Assets:              ৳ 5,73,95,000

// LIABILITIES
//   Accounts Payable (2000):          ৳  38,50,000   (1 open bill)
//   Inv. Financing Payable — BRAC (2151): ৳  38,00,000
//   Inv. Financing Payable — Karim (2152):৳  12,00,000
//   Accrued Interest — BRAC (2171):   ৳      76,000   (booked Apr 30, paid May 5)
//   Accrued Interest — Karim (2172):  ৳      24,000
//   VAT Payable (2300):               ৳   6,00,000
//   Accrued Liabilities:              ৳      18,000
//   Total Liabilities:                ৳  95,68,000

// EQUITY
//   Share Capital (3000):      ৳ 4,96,85,000
//   Retained Earnings (3100):  ৳  15,00,000   (prior periods)
//   Current Period Income:     ৳  15,92,000   (April profit after ৳1,00,000 financing interest)
//   Total Equity:              ৳ 5,27,77,000

// BALANCE CHECK: ৳ 5,73,95,000  ✓  (assets unchanged; liabilities + equity = assets)
```

May 1 — a new period opens automatically. The April numbers are frozen.

---

Authorization Gates
-------------------

[](#authorization-gates)

The package registers fine-grained gates. Override any gate in your `AppServiceProvider`:

```
// Grant full access via the super-gate
Gate::define('accounting-admin', fn ($user) => $user->hasRole('accountant'));

// Or override individual abilities
Gate::define('accounting.journal.submit', fn ($user) => $user->hasRole(['accountant', 'junior_accountant']));
Gate::define('accounting.journal.post',   fn ($user) => $user->hasRole(['finance_manager', 'cfo']));
Gate::define('accounting.fiscal-year.close', fn ($user) => $user->hasRole('cfo'));
```

Available gates:

GateDescription`accounting.journal.view`See journal entries`accounting.journal.create`Create/edit draft entries`accounting.journal.submit`Submit entry for approval`accounting.journal.post`Approve and post to GL`accounting.journal.void`Void a posted entry`accounting.invoice.view`View invoices`accounting.invoice.create`Create/edit invoices`accounting.invoice.post`Post invoice to GL`accounting.invoice.payment`Record payments`accounting.bill.view`View bills`accounting.bill.create`Create/edit bills`accounting.bill.post`Post bill to GL`accounting.bill.payment`Record payments`accounting.reports.view`View all financial reports`accounting.accounts.view`View chart of accounts`accounting.accounts.manage`Create/edit accounts`accounting.customers.view`View customers`accounting.customers.manage`Create/edit customers`accounting.vendors.view`View vendors`accounting.vendors.manage`Create/edit vendors`accounting.budget.view`View budgets`accounting.budget.manage`Create/edit budgets`accounting.budget.approve`Approve budgets`accounting.fiscal-year.close`Close fiscal year`accounting.qbo.connect`Initiate QBO OAuth2 connect / disconnect`accounting.qbo.sync`Trigger push sync to QBO or pull QBO reports---

Web UI Routes
-------------

[](#web-ui-routes)

All routes are protected by `web_middleware` (default `['web', 'auth']`) under `web_prefix` (default `accounting`):

Route nameURLDescription`accounting.dashboard``/accounting/dashboard`Overview dashboard with pending approvals widget`accounting.accounts``/accounting/accounts`Chart of accounts`accounting.journal``/accounting/journal-entries`Journal entries with two-step workflow`accounting.ledger``/accounting/ledger`General ledger`accounting.ledger.customers``/accounting/ledger/customers`Customer ledger index`accounting.ledger.vendors``/accounting/ledger/vendors`Vendor ledger index`accounting.customers.ledger``/accounting/customers/{customer}/ledger`Per-customer statement`accounting.vendors.ledger``/accounting/vendors/{vendor}/ledger`Per-vendor statement`accounting.invoices``/accounting/invoices`Invoice management`accounting.invoices.show``/accounting/invoices/{invoice}`Invoice detail`accounting.bills``/accounting/bills`Bill management`accounting.bills.show``/accounting/bills/{bill}`Bill detail`accounting.expenses``/accounting/expenses`Expense management`accounting.expenses.show``/accounting/expenses/{expense}`Expense detail`accounting.customers``/accounting/customers`Customer list`accounting.vendors``/accounting/vendors`Vendor list`accounting.requisitions``/accounting/requisitions`Purchase &amp; expense requisitions`accounting.reports``/accounting/reports`Financial reports (trial balance, P&amp;L, balance sheet, cash flow)`accounting.budgets``/accounting/budgets`Budget management`accounting.period-close``/accounting/period-close`Month-end period close wizard---

REST API
--------

[](#rest-api)

Base prefix: `api/accounting`. Default middleware: `['api', 'auth:sanctum']`.

### Journal Entries

[](#journal-entries)

MethodEndpointAction`GET``/api/accounting/journal-entries`List entries (filterable by status, date, search)`POST``/api/accounting/journal-entries`Create entry`GET``/api/accounting/journal-entries/{id}`Get entry`POST``/api/accounting/journal-entries/{id}/post`Post entry to GL`POST``/api/accounting/journal-entries/{id}/void`Void entry### Accounts

[](#accounts)

MethodEndpointAction`GET``/api/accounting/accounts`List accounts`POST``/api/accounting/accounts`Create account`GET``/api/accounting/accounts/{id}`Get account`PUT``/api/accounting/accounts/{id}`Update account`GET``/api/accounting/accounts/{id}/balance`Current account balance### Invoices

[](#invoices)

MethodEndpointAction`GET``/api/accounting/invoices`List invoices`POST``/api/accounting/invoices`Create invoice`GET``/api/accounting/invoices/{id}`Get invoice`PUT``/api/accounting/invoices/{id}`Update invoice`DELETE``/api/accounting/invoices/{id}`Delete invoice`POST``/api/accounting/invoices/{id}/post`Post invoice to GL`POST``/api/accounting/invoices/{id}/payments`Record payment`POST``/api/accounting/invoices/{id}/expenses`Record charge/expense against invoice### Bills

[](#bills)

MethodEndpointAction`GET``/api/accounting/bills`List bills`POST``/api/accounting/bills`Create bill`GET``/api/accounting/bills/{id}`Get bill`DELETE``/api/accounting/bills/{id}`Delete bill`POST``/api/accounting/bills/{id}/post`Post bill to GL`POST``/api/accounting/bills/{id}/payments`Record payment`POST``/api/accounting/bills/{id}/expenses`Record charge/expense against bill### Expenses

[](#expenses-1)

MethodEndpointAction`GET``/api/accounting/expenses`List expenses`POST``/api/accounting/expenses`Create expense`GET``/api/accounting/expenses/{id}`Get expense`DELETE``/api/accounting/expenses/{id}`Delete expense`POST``/api/accounting/expenses/{id}/post`Post expense to GL`POST``/api/accounting/expenses/{id}/payments`Record payment### Customers

[](#customers)

MethodEndpointAction`GET``/api/accounting/customers`List customers`POST``/api/accounting/customers`Create customer`GET``/api/accounting/customers/{id}`Get customer`PUT``/api/accounting/customers/{id}`Update customer`DELETE``/api/accounting/customers/{id}`Delete customer### Vendors

[](#vendors)

MethodEndpointAction`GET``/api/accounting/vendors`List vendors`POST``/api/accounting/vendors`Create vendor`GET``/api/accounting/vendors/{id}`Get vendor`PUT``/api/accounting/vendors/{id}`Update vendor`DELETE``/api/accounting/vendors/{id}`Delete vendor### Budgets

[](#budgets-1)

MethodEndpointAction`GET``/api/accounting/budgets`List budgets`POST``/api/accounting/budgets`Create budget`GET``/api/accounting/budgets/{id}`Get budget`PUT``/api/accounting/budgets/{id}`Update budget`DELETE``/api/accounting/budgets/{id}`Delete budget`POST``/api/accounting/budgets/{id}/approve`Approve budget`GET``/api/accounting/budgets/{id}/vs-actual`Budget vs actual comparison### Reports

[](#reports)

MethodEndpointAction`GET``/api/accounting/reports/trial-balance`Trial balance (`?start=&end=&sbu=`)`GET``/api/accounting/reports/balance-sheet`Balance sheet (`?date=&sbu=`)`GET``/api/accounting/reports/income-statement`P&amp;L (`?start=&end=&sbu=`)`GET``/api/accounting/reports/cash-flow`Cash flow (`?start=&end=&sbu=`)`GET``/api/accounting/reports/general-ledger`General ledger (`?account_id=&start=&end=&sbu=`)`GET``/api/accounting/reports/ar-aging`A/R aging (`?as_of_date=&sbu_code=`)`GET``/api/accounting/reports/ap-aging`A/P aging (`?as_of_date=&sbu_code=`)---

Artisan Commands
----------------

[](#artisan-commands)

### Demo data

[](#demo-data)

```
# Seed a full multi-entity demo dataset (invoices, bills, expenses, journal entries, budgets)
php artisan accounting:demo
```

### Report generation

[](#report-generation)

```
# Print a report to the terminal
php artisan accounting:report income-statement --start=2026-01-01 --end=2026-04-30 --format=table
php artisan accounting:report balance-sheet    --date=2026-04-30  --format=table
php artisan accounting:report trial-balance    --start=2026-01-01 --end=2026-04-30 --format=json

# Export to file
php artisan accounting:report all --start=2026-01-01 --end=2026-12-31 --format=csv --output=reports/fy2026.csv
```

Available types: `all` (default) | `trial-balance` | `balance-sheet` | `income-statement` | `cash-flow`

Available formats: `table` | `csv` | `json`

### QBO sync

[](#qbo-sync)

```
# Push all entities to QuickBooks Online
php artisan accounting:qbo-sync

# Push specific entities only
php artisan accounting:qbo-sync --entity=accounts --entity=customers --entity=invoices

# Push with a date filter (only records modified since this date)
php artisan accounting:qbo-sync --entity=invoices --since=2026-05-01

# Target a specific QBO company (overrides QBO_REALM_ID)
php artisan accounting:qbo-sync --realm=123456789

# Pull a named report from QBO and dump JSON
php artisan accounting:qbo-sync --pull=profit-and-loss --start=2026-01-01 --end=2026-04-30
php artisan accounting:qbo-sync --pull=balance-sheet   --start=2026-04-30
php artisan accounting:qbo-sync --pull=aged-receivables
```

Available `--pull` slugs: `profit-and-loss`, `balance-sheet`, `cash-flow`, `trial-balance`, `aged-receivables`, `aged-payables`, `general-ledger`, `transaction-list`

---

QuickBooks Online Integration
-----------------------------

[](#quickbooks-online-integration)

Two-way sync with QuickBooks Online via the v3 REST API and OAuth 2.0.

**Push** your local data to QBO — Chart of Accounts, Customers, Vendors, Invoices, Bills, Journal Entries.

**Pull** QBO-native reports back into your app.

**Format** your local reports (P&amp;L, Balance Sheet, Cash Flow, Trial Balance, A/R Aging, A/P Aging) in QBO-compatible structure using `QuickBooksReportFormatter`.

```
use Centrex\Accounting\QuickBooks\{QuickBooksSyncService, QuickBooksReportFormatter};

// Push all accounts to QBO
$result = app(QuickBooksSyncService::class)->syncAccounts(config('accounting.quickbooks.default_realm_id'));
// ['created' => 5, 'updated' => 12, 'skipped' => 3, 'errors' => []]

// Get local P&L in QBO section structure
use Centrex\Accounting\Facades\Accounting;
$pl     = Accounting::getIncomeStatement('2026-01-01', '2026-04-30');
$qboFmt = app(QuickBooksReportFormatter::class)->profitAndLoss($pl);
// ['income' => [...], 'cost_of_goods_sold' => [...], 'gross_profit' => x, 'expenses' => [...], 'net_income' => x]
```

See [docs/quickbooks.md](docs/quickbooks.md) for the full setup guide, OAuth flow, account type mapping reference, and webhook documentation.

---

Testing
-------

[](#testing)

```
composer test          # full suite: rector dry-run, pint check, phpstan, pest
composer test:unit     # pest only
composer test:types    # phpstan static analysis
composer test:lint     # pint style check
composer lint          # apply pint formatting
composer refacto       # apply rector refactors

# Single test
vendor/bin/pest tests/Feature/JournalEntryTest.php
vendor/bin/pest --filter "journal entry must balance"
```

---

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Credits
-------

[](#credits)

- [centrex](https://github.com/centrex)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance99

Actively maintained with recent releases

Popularity26

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity72

Established project with proven stability

 Bus Factor1

Top contributor holds 94% 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

Every ~16 days

Recently: every ~2 days

Total

60

Last Release

5d ago

Major Versions

v0.1.0 → v1.0.02023-12-14

v1.5.5 → v2.0.02026-04-07

v2.3.0 → v3.0.02026-04-23

PHP version history (6 changes)v0.1.0PHP ^8.0|^8.1

v1.0.1PHP ^8.1|^8.2

v1.3.0PHP ^8.1|^8.2|^8.3

v1.5.0PHP ^8.2|^8.3|^8.4

v2.0.0PHP ^8.2

v3.0.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/29769944?v=4)[Raisul Islam](/maintainers/rochi88)[@rochi88](https://github.com/rochi88)

---

Top Contributors

[![rochi88](https://avatars.githubusercontent.com/u/29769944?v=4)](https://github.com/rochi88 "rochi88 (158 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (9 commits)")[![kolazsys](https://avatars.githubusercontent.com/u/110965082?v=4)](https://github.com/kolazsys "kolazsys (1 commits)")

---

Tags

laravellaravel-accountingcentrex

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M132](/packages/laravel-pulse)[livewire/flux

The official UI component library for Livewire.

9527.8M128](/packages/livewire-flux)[venturedrake/laravel-crm

A free open source CRM built as a package for laravel projects

43311.1k](/packages/venturedrake-laravel-crm)[api-platform/laravel

API Platform support for Laravel

58171.4k14](/packages/api-platform-laravel)[wearepixel/laravel-cart

A cart implementation for Laravel

1374.8k](/packages/wearepixel-laravel-cart)

PHPackages © 2026

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