Search by

nml / fincore

Premod1

NML FinCore - Robust Double-Entry Accounting Engine for Laravel ERPs

Package info

github.com/Premod1/nml-fincore

Homepage

pkg:composer/nml/fincore

Fund package maintenance!

Premod1

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 30

v1.0.8 2026-11-04 12:03 UTC

This package is auto-updated.

Last update: 2026-11-09 17:20:42 UTC


README

NML FinCore is a robust, decoupled double-entry accounting engine designed for Laravel applications. It enables real-time ledger generation, polymorphic linking to transactional models, multi-SBU reporting, and strict period locks.

💖 NML FinCore PRO (Commercial UI Kit & ERP Auto-Posting Engine)

Looking for a Ready-Made QuickBooks-style Accounting Dashboard, Live Drill-Down Reports, and Zero-Accounting-Knowledge ERP Event Bridge?

Check out NML FinCore PRO!

NML FinCore PRO Demo

🌟 What's in FinCore PRO?

  • Full QuickBooks Online (QBO) UI Suite: Chart of Accounts, Journal Voucher Manager, Trial Balance, Profit & Loss, Balance Sheet, and General Ledger.
  • Zero-Accounting-Knowledge Event Bridge (FinCorePro::recordErpEvent): Record Sales, Payments, Bills, and Expenses directly from ERP controllers without calculating Debits, Credits, VAT, or COGS!
  • Dynamic ERP Posting Rules UI (/accounting/settings): Accountants can dynamically re-map accounts from the browser without code changes.
  • Dual-Mode Integration: Embeds seamlessly into your existing Bootstrap 4/5 or Blade ERP layout via Scoped CSS (.fincore-scope).

🚀 Get FinCore PRO ($29 Sponsor Tier)

Sponsor $29 on GitHub Sponsors (Premod1) to receive instant private repository access, automated collaborator invitation, and all future updates:

👉 Sponsor & Get FinCore PRO ($29)

Once sponsored, install via Composer:

"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/Premod1/nml-fincore-pro.git"
    }
],
"require": {
    "nml/fincore-pro": "^1.0"
}

Installation

Install the package via Composer:

composer require nml/fincore

Migration Setup

The package migrations will be loaded automatically. Run the database migrations to create the necessary tables:

php artisan migrate

Configuration (Optional)

Publish the configuration file to customize the table prefix, rounding tolerance, currency, and period locks:

php artisan vendor:publish --tag=fincore-config

Core Feature List

  • Double-entry accounting engine: Enforces exact balance matching for debit and credit lines within a configurable rounding tolerance.
  • Dynamic chart of accounts: Classifies and rolls up accounts hierarchically under Assets, Liabilities, Equity, Revenue, and Expenses with parent-child relationship support.
  • Real-time general ledger generation: Generates real-time ledger entries and balances.
  • Trial Balance reporting: Dynamic Trial Balance generation based on date range and SBU codes.
  • Balance Sheet reporting: Dynamic Balance Sheet generation showing Assets, Liabilities, and Equity.
  • Income Statement (P&L): Dynamic Profit & Loss reporting based on Revenue and Expense accounts.
  • Cash Flow statements: Automatically generated Cash Flow statements based on account movements.
  • Multi-SBU / branch reporting: Tracks entries using Strategic Business Unit (SBU) codes for departmental or branch-level accounting.
  • Fiscal period locking: Prevents retrospective postings or modifications to closed accounting periods and years.
  • Polymorphic transaction linking: Links journal entries directly to external source models (Invoices, Sales, Purchases, Payments).
  • Multi-currency support: Supports transactions in foreign currencies with exchange rates.
  • Tax/VAT handling: Enforces tax rules, inclusive/exclusive calculations, and automated tax line generation.
  • Fixed asset management: Fixed asset register with purchase cost and accumulated depreciation tracking.
  • Automated depreciation posting: Automatically calculates and posts monthly straight-line depreciation entries.
  • Budgeting & variance reporting: Sets monthly budget targets per account and generates Budget vs. Actual variance reports.
  • Bank reconciliation matching: Automatically matches bank statement transactions with ledger lines and supports manual clearing.
  • AR/AP ageing reports: Tracks partner receivables and payables with FIFO payment allocation and ageing buckets.
  • Fiscal year closing engine: Automatically zero-out temporary Revenue/Expense accounts and transfers Net Profit/Loss to Retained Earnings.
  • Full audit trail & activity logging: Automatically logs all creations, updates, status transitions, and deletions, capturing user ID, IP address, user agent, and JSON diffs.

Initialization

Seed the standard Chart of Accounts using the provided seeder or calling the initializer class directly:

Option A: Using Laravel Database Seeder

Add the package seeder class to your main database/seeders/DatabaseSeeder.php file:

public function run(): void
{
    $this->call(\Nml\FinCore\Database\Seeders\FinCoreSeeder::class);
}

Or run it directly from the terminal:

php artisan db:seed --class="Nml\FinCore\Database\Seeders\FinCoreSeeder"

Option B: Programmatic Initialization

You can seed the standard Chart of Accounts programmatically (e.g., in a tenant setup flow or custom script):

use Nml\FinCore\Services\ChartOfAccountsInitializer;

// Seeds the default chart of accounts (idempotent, safe to rerun)
ChartOfAccountsInitializer::initialize('LKR');

Chart of Accounts Management

To create and manage custom accounts (including parent-child hierarchies), use the Account model:

Creating a Root Account

use Nml\FinCore\Models\Account;
use Nml\FinCore\Enums\AccountType;

$assetsParent = Account::create([
    'code' => '1000',
    'name' => 'Assets',
    'type' => AccountType::ASSET->value,
    'subtype' => 'asset',
    'parent_id' => null, // Root account
]);

Creating a Sub-Account (Child Account)

Link it to its parent by passing the parent's id:

$bankAccount = Account::create([
    'code' => '1100',
    'name' => 'Seylan Bank A/C',
    'type' => AccountType::ASSET->value,
    'subtype' => 'current_asset',
    'parent_id' => $assetsParent->id, // Linked to the parent account
]);

Usage Scenarios

1. Creating and Posting a Journal Entry

Create a journal entry as a draft, validate it, and post it to the General Ledger.

use Nml\FinCore\Facades\FinCore;
use Nml\FinCore\Enums\JvType;

// Create the journal entry draft
$entry = FinCore::createJournalEntry([
    'date' => '2026-06-18',
    'reference' => 'INV-2026-0001',
    'type' => JvType::GENERAL->value,
    'description' => 'Credit sale invoice registration',
    'sbu_code' => 'COLOMBO_SBU',
    'journalable_type' => 'App\Models\Invoice', // Optional polymorphic model link
    'journalable_id' => 45,
    'lines' => [
        [
            'account_id' => 3, // Accounts Receivable (Asset)
            'type' => 'debit',
            'amount' => 50000.00,
            'description' => 'Debit A/R for customer invoice'
        ],
        [
            'account_id' => 20, // Sales Revenue (Revenue)
            'type' => 'credit',
            'amount' => 50000.00,
            'description' => 'Credit Sales Revenue'
        ]
    ]
]);

// Post to General Ledger (Performs balance matching & period lock checks)
try {
    $entry->post();
} catch (\Nml\FinCore\Exceptions\AccountingException $e) {
    // Handle validation errors (e.g., Unbalanced Debits/Credits or Closed Period)
    Log::error($e->getMessage());
}

1.1. Creating a Manual Journal Voucher (JV)

For manual journal adjustments entered by accountants via a voucher UI, collect the form inputs (rows with Account, Type, Debit/Credit, and optional Notes) and post them directly:

use Nml\FinCore\Facades\FinCore;
use Nml\FinCore\Enums\JvType;

// Map the dynamic rows of a manual JV interface to the lines array:
$entry = FinCore::createJournalEntry([
    'date' => '2026-06-18',
    'reference' => 'JV-2026-0045',
    'type' => JvType::GENERAL->value,
    'description' => 'Manual adjustment for prepaid rent',
    'lines' => [
        [
            'account_id' => 12, // Prepaid Expense Account
            'type' => 'debit',
            'amount' => 15000.00, // Debit amount from Row 1
            'description' => 'Amortization of rent'
        ],
        [
            'account_id' => 25, // Rent Expense Account
            'type' => 'credit',
            'amount' => 15000.00, // Credit amount from Row 2
            'description' => 'Corresponding rent credit'
        ]
    ]
]);

// Post to ledger
$entry->post();

Parameter Details

  • type: Indicates the category of the journal entry. Developers should use the Nml\FinCore\Enums\JvType enum:
    • JvType::GENERAL->value ('general'): Normal business transactions (e.g., sales invoices, supplier purchases, payments).
    • JvType::ADJUSTMENT->value ('adjustment'): End-of-period adjustments (e.g., depreciation, accruals, prepayments).
    • JvType::CLOSING->value ('closing'): Year-end closing entries to reset revenue/expense balances to the retained earnings account.
  • sbu_code: Strategic Business Unit (Branch, Department, or Division) code. Tagging entries with this code enables department-wise or branch-wise filtering on all financial statements.
  • journalable_type & journalable_id: Polymorphic relationship columns that map this journal entry to the source document in the ERP (e.g., App\Models\Invoice or App\Models\Payment). This provides an audit trail back to the originating business transaction.

2. Generating a Trial Balance

Generate a Trial Balance report within a specific date range or filter by SBU.

use Nml\FinCore\Facades\FinCore;

$trialBalance = FinCore::getTrialBalance(
    startDate: '2026-01-01',
    endDate: '2026-06-30',
    sbuCode: 'COLOMBO_SBU' // Optional SBU filter
);

// Format of the returned array:
// [
//     'accounts' => [
//          ['account' => AccountModel, 'debit' => 50000.00, 'credit' => 0.00, 'balance' => 50000.00],
//          ...
//      ],
//     'total_debits' => 50000.00,
//     'total_credits' => 50000.00,
//     'is_balanced' => true
// ]

3. Generating an Income Statement (Profit & Loss)

Get the revenue, expense, and net income metrics for a specified period.

use Nml\FinCore\Facades\FinCore;

$incomeStatement = FinCore::getIncomeStatement(
    startDate: '2026-04-01',
    endDate: '2026-06-30'
);

$netIncome = $incomeStatement['net_income'];
$totalRevenue = $incomeStatement['revenue']['total'];
$totalExpenses = $incomeStatement['expenses']['total'];

4. Generating a Balance Sheet

Generate the statement of financial position as of a specific date.

use Nml\FinCore\Facades\FinCore;

$balanceSheet = FinCore::getBalanceSheet(
    date: '2026-06-18'
);

$assets = $balanceSheet['assets']['total'];
$liabilities = $balanceSheet['liabilities']['total'];
$equity = $balanceSheet['equity']['total'];
$currentYearEarnings = $balanceSheet['current_year_earnings']; // Net profit/loss dynamically rolled into equity if fiscal year is unclosed
$isBalanced = $balanceSheet['is_balanced']; // Assets = Liabilities + Equity

5. Generating a General Ledger (Account Ledger)

Generate a detailed General Ledger report for a specific account, supporting memory-efficient pagination for large datasets:

use Nml\FinCore\Facades\FinCore;

$ledger = FinCore::getGeneralLedger(
    accountId: 3, // Accounts Receivable
    startDate: '2026-01-01',
    endDate: '2026-06-30',
    sbuCode: 'COLOMBO_SBU',
    perPage: 50,  // Optional pagination limit
    page: 1       // Optional page number
);

foreach ($ledger['accounts'] as $accountReport) {
    $account = $accountReport['account']; // Account model instance
    $openingBalance = $accountReport['opening_balance'];
    $closingBalance = $accountReport['closing_balance'];
    
    // Pagination metadata (only returned if perPage is specified)
    $pagination = $accountReport['pagination'] ?? null;
    
    foreach ($accountReport['entries'] as $entry) {
        $entryNumber = $entry['entry_number'];
        $date = $entry['date'];
        $type = $entry['type']; // 'debit' or 'credit'
        $amount = $entry['amount'];
        $runningBalance = $entry['running_balance'];
    }
}

6. Generating a Cash Flow Statement

Generate a Cash Flow statement (indirect method) for a date range:

use Nml\FinCore\Facades\FinCore;

$cashFlow = FinCore::getCashFlowStatement(
    startDate: '2026-01-01',
    endDate: '2026-06-30'
);

$operatingTotal = $cashFlow['operating_activities']['total'];
$investingTotal = $cashFlow['investing_activities']['total'];
$financingTotal = $cashFlow['financing_activities']['total'];
$netIncreaseDecrease = $cashFlow['net_increase_decrease'];
$beginningCash = $cashFlow['beginning_cash'];
$endingCash = $cashFlow['ending_cash'];
$isReconciled = $cashFlow['is_reconciled']; // true if beginningCash + netIncreaseDecrease matches endingCash

7. Managing Entry Statuses

Transitions draft entries to submitted, and void or reject them:

// Submit for review
$entry->submit($userId);

// Reject and send back to draft
$entry->returnToDraft('Reason for rejection');

// Void posted journal entry
$entry->void();

8. Advanced: Tax/VAT & Multi-Currency

Registering a Tax Rate

Create a tax rate mapping it to a general ledger account for tax postings:

use Nml\FinCore\Models\Tax;

$vatTax = Tax::create([
    'name' => 'VAT 15%',
    'code' => 'VAT-15',
    'rate' => 15.00,
    'account_id' => 15, // Output VAT Payable account ID
    'is_active' => true
]);

Creating a Multi-Currency Entry with Automatic Tax Calculation

When creating a foreign currency transaction with tax:

  1. Provide the currency (e.g. 'USD') and exchange_rate (e.g. 300.000000).
  2. Provide fc_amount (Foreign Currency amount) on lines. The system automatically computes the functional currency amount = fc_amount * exchange_rate.
  3. Provide tax_id and tax_behavior ('inclusive' or 'exclusive'). The system calculates the tax and automatically creates the corresponding tax ledger entry.
use Nml\FinCore\Facades\FinCore;
use Nml\FinCore\Enums\JvType;

$entry = FinCore::createJournalEntry([
    'date' => '2026-06-18',
    'reference' => 'TX-USD-001',
    'type' => JvType::GENERAL->value,
    'description' => 'Foreign sale invoice with tax',
    'currency' => 'USD',
    'exchange_rate' => 300.000000,
    'lines' => [
        [
            'account_id' => 3, // Accounts Receivable (Asset)
            'type' => 'debit',
            'fc_amount' => 115.00, // Total USD including tax
            'description' => 'A/R for foreign sale'
        ],
        [
            'account_id' => 20, // Sales Revenue (Revenue)
            'type' => 'credit',
            'fc_amount' => 115.00,
            'tax_id' => $vatTax->id,
            'tax_behavior' => 'inclusive', // Tax amount (15 USD / 4500 LKR) is extracted and posted to Output VAT
            'description' => 'Sales revenue inclusive of VAT'
        ]
    ]
]);

// Post to ledger. Validates balancing in both USD (115.00) and LKR (34500.00).
$entry->post();

9. Fixed Assets & Depreciation

Registering a Fixed Asset

Register a fixed asset with purchase costs, depreciation parameters, and its target ledger accounts:

use Nml\FinCore\Models\FixedAsset;

$asset = FixedAsset::create([
    'name' => 'Delivery Van',
    'code' => 'FA-VAN-001',
    'purchase_date' => '2026-01-01',
    'purchase_cost' => 2400000.00,
    'salvage_value' => 400000.00,
    'useful_life_years' => 5,
    'depreciation_method' => 'straight_line', // 'straight_line' or 'reducing_balance'
    'depreciation_rate' => 20.00, // 20% annual rate
    'asset_account_id' => 5, // Motor Vehicles cost account
    'accumulated_depreciation_account_id' => 6, // Accumulated Depreciation account
    'depreciation_expense_account_id' => 25, // Depreciation Expense account
    'status' => 'active'
]);

Calculating and Posting Depreciation

Calculate monthly depreciation for a specific asset or all active assets, which automatically creates and posts the corresponding double-entry journal:

use Nml\FinCore\Facades\FinCore;

// 1. Calculate the monthly depreciation amount for a specific asset
$amount = FinCore::calculateMonthlyDepreciation($asset, '2026-06-30');

// 2. Post depreciation for a specific asset (Creates Debit Depreciation Expense, Credit Accumulated Depreciation)
$entry = FinCore::postDepreciationForAsset($asset->id, '2026-06-30');

// 3. Post monthly depreciation for all active fixed assets in bulk
$results = FinCore::postDepreciationForAllActiveAssets('2026-06-30');

10. Budgeting & Variance Reporting

Setting Monthly Budget Targets

Set a target monthly budget amount for a specific account and SBU:

use Nml\FinCore\Facades\FinCore;

$budget = FinCore::setMonthlyBudget(
    accountId: 25, // Depreciation Expense account
    year: 2026,
    month: 6,
    amount: 40000.00,
    sbuCode: 'COLOMBO_SBU' // Optional
);

Generating a Budget vs. Actual Variance Report

Fetch target budgets, actual ledger movements, and variance details for a specific month:

use Nml\FinCore\Facades\FinCore;

$report = FinCore::getBudgetVarianceReport(
    year: 2026,
    month: 6,
    sbuCode: 'COLOMBO_SBU' // Optional
);

foreach ($report as $row) {
    $accountName = $row['account_name'];
    $budgetAmount = $row['budget_amount'];
    $actualAmount = $row['actual_amount'];
    $variance = $row['variance']; // Positive is Favorable, Negative is Unfavorable
    $variancePercentage = $row['variance_percentage'];
    $status = $row['status']; // 'Favorable' or 'Unfavorable'
}

11. Bank Reconciliation Matcher

Starting a Bank Reconciliation Statement

Create a bank reconciliation session for a specific bank ledger account, date, opening and closing balance:

use Nml\FinCore\Facades\FinCore;

$reconciliation = FinCore::createReconciliation(
    accountId: 1, // Bank Account
    statementDate: '2026-06-30',
    openingBalance: 150000.00,
    closingBalance: 185000.00
);

Fetching Unreconciled Ledger Lines

Get all posted journal entry lines for this account that remain uncleared:

use Nml\FinCore\Facades\FinCore;

$unreconciled = FinCore::getUnreconciledLines(
    accountId: 1,
    endDate: '2026-06-30'
);

Running the Auto-Matcher

Match uploaded bank statement transactions against unreconciled ledger entries automatically. The engine matches based on exact amount, type (debit/credit), and transaction date differences:

use Nml\FinCore\Facades\FinCore;

$statementTransactions = [
    ['date' => '2026-06-10', 'amount' => 5000.00, 'type' => 'debit', 'reference' => 'TXN-1'],
    ['date' => '2026-06-15', 'amount' => 30000.00, 'type' => 'credit', 'reference' => 'TXN-2'],
];

$results = FinCore::autoMatchStatementTransactions(
    reconciliationId: $reconciliation->id,
    statementTransactions: $statementTransactions
);

$matched = $results['matched'];     // List of successfully matched items
$unmatched = $results['unmatched']; // List of bank statement items not found in ledger

Manual Clear and Finalization

Reconcile lines manually or finalize the statement. Finalization requires the cleared balance to match the statement closing balance perfectly:

use Nml\FinCore\Facades\FinCore;

// Manually clear a ledger line
FinCore::manuallyClearLine($reconciliation->id, $lineId = 42, '2026-06-25');

// Finalize and close the reconciliation statement
FinCore::finalizeReconciliation($reconciliation->id);

12. Accounts Receivable (AR) & Accounts Payable (AP) Ageing Reports

Recording Entries with Partner Metadata

To track receivables and payables per customer or vendor, pass the polymorphic partnerable object and optional due_date when creating double-entry records:

use Nml\FinCore\Facades\FinCore;

// Create sales invoice entry with partner tracking
$entry = FinCore::createJournalEntry([
    'date' => '2026-06-01',
    'reference' => 'INV-2026-001',
    'lines' => [
        [
            'account_id' => 3, // Accounts Receivable (Receivable type)
            'type' => 'debit',
            'amount' => 120000.00,
            'due_date' => '2026-06-30',
            'partnerable_type' => 'App\Models\Customer',
            'partnerable_id' => 105,
            'description' => 'Sales invoice for Customer #105'
        ],
        [
            'account_id' => 20, // Sales Revenue
            'type' => 'credit',
            'amount' => 120000.00
        ]
    ]
]);
$entry->post();

Generating Ageing Reports

Fetch the ageing analysis report for all partners. The engine allocates credits against debits using a FIFO (First In, First Out) algorithm and classifies outstanding amounts into age buckets based on the due_date:

By default, the engine tracks Accounts Receivable using code 1200 (or accounts with subtype receivable) and Accounts Payable using code 2000 (or accounts with subtype payable). You can customize these codes in config/accounting.php (ar_account_codes and ap_account_codes) or pass specific account IDs directly.

use Nml\FinCore\Facades\FinCore;

// 1. Get Accounts Receivable Ageing Report (Default configured AR accounts)
$arReport = FinCore::getReceivablesAgeingReport('2026-06-30');

// 2. Get Accounts Payable Ageing Report (Default configured AP accounts)
$apReport = FinCore::getPayablesAgeingReport('2026-06-30');

// 3. (Optional) Filter by specific Account IDs
$customArReport = FinCore::getReceivablesAgeingReport('2026-06-30', [$arAccountId]);
$customApReport = FinCore::getPayablesAgeingReport('2026-06-30', [$apAccountId]);

foreach ($arReport as $row) {
    $partnerName = $row['partner_name'];
    $totalOutstanding = $row['total_outstanding'];
    $current = $row['current'];       // Not due yet
    $bucket1 = $row['1_30'];          // 1 - 30 days overdue
    $bucket2 = $row['31_60'];         // 31 - 60 days overdue
    $bucket3 = $row['61_90'];         // 61 - 90 days overdue
    $bucket4 = $row['91_plus'];       // 91+ days overdue
}

13. Fiscal Year-End Closing Engine

Closing a Fiscal Year

At the end of a fiscal year, close the year by calling closeFiscalYear. The engine automatically:

  1. Calculates the net balance of all Revenue and Expense accounts for that year.
  2. Generates and posts a closing journal entry on the last day of the fiscal year, transferring the net profit or loss to the specified Retained Earnings account.
  3. Sets the balance of all Revenue and Expense accounts to zero relative to the next period.
  4. Marks the FiscalYear and all its monthly FiscalPeriods as closed, locking them against future postings or modifications.
use Nml\FinCore\Facades\FinCore;

// Close Fiscal Year ID 1, transferring net profit/loss to Retained Earnings Account ID 8
$closingEntry = FinCore::closeFiscalYear(
    fiscalYearId: 1,
    retainedEarningsAccountId: 8,
    userId: 1 // Optional user ID of the approver
);

14. Audit Trail / Activity Log

Retrieving Audit History

The engine automatically logs all creations, updates, state changes (submitted, posted, voided), and deletions of journal entries. This includes capturing the actor's user ID, IP address, user agent, action type, and a JSON diff of modified attributes (old_values vs new_values).

Retrieve the history for any journal entry:

use Nml\FinCore\Facades\FinCore;

$logs = FinCore::getAuditHistory($journalEntryId);

foreach ($logs as $log) {
    Log::info("Action: " . $log->action);
    Log::info("Performed by User: " . $log->user_id);
    Log::info("IP: " . $log->ip_address);
    Log::info("User Agent: " . $log->user_agent);
    Log::info("Old State: ", (array) $log->old_values);
    Log::info("New State: ", (array) $log->new_values);
}

15. Running Tests

Run the package test suite using PHPUnit:

./vendor/bin/phpunit