1. Go to this page and download the library: Download otim-otim/bookkeeper library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use OtimOtim\BookKeeper\Support\Money;
$a = Money::of('1500.50', 'USD'); // from a major-unit amount (string, int, float or Money)
$b = Money::ofMinor(150050, 'USD'); // from minor units
Money::ofRounded('10.005', 'USD'); // round excess decimals instead of rejecting them
Money::zero('UGX');
$a->plus($b)->minus('0.50'); // strings are major units, ints are minor units
$a->multipliedBy('1.5'); // rounded with the configured mode
$a->percent('18'); // 18% of the amount
$a->allocate([1, 1, 1]); // split without losing a cent
$a->convertTo('UGX', '3700'); // currency conversion
$a->amount(); // "1500.50" (decimal string; also __toString and JSON)
$a->minor(); // 150050
$a->isPositive(); $a->isZero(); $a->equals($b); $a->compareTo($b);
Money::sum($list, 'USD');
BookKeeper::settings()->setBaseCurrency('TZS'); // allowed until the first posting
BookKeeper::settings()->baseCurrency(); // "TZS"
BookKeeper::settings()->isBaseCurrencyLocked();
BookKeeper::balance('1125'); // base currency (UGX 560,000)
BookKeeper::account('1125')->currencyBalance(); // USD 150.00
BookKeeper::exchangeRates()->set('USD', '3700', '2026-03-01');
BookKeeper::exchangeRates()->convert(Money::of('100', 'USD'), 'KES'); // foreign → foreign via base
BookKeeper::exchangeRates()->crossRate('USD', 'KES');
class BankOfUgandaRates implements ExchangeRateProvider
{
public function rates(string $base, array $currencies, CarbonInterface $date): array
{
// return ['USD' => '3712.50', 'KES' => '28.61']; // base units per 1 unit
}
}
use OtimOtim\BookKeeper\Facades\BookKeeper;
use OtimOtim\BookKeeper\Enums\AccountType;
use OtimOtim\BookKeeper\Enums\AccountSubtype;
BookKeeper::createAccount('1000', 'Current assets', AccountType::Asset, ['is_group' => true]);
BookKeeper::createAccount('1010', 'Cash', AccountSubtype::Cash, ['parent' => '1000', 'role' => 'cash', 'tags' => ['liquid']]);
BookKeeper::createAccount('1200', 'Receivables', AccountSubtype::Receivable, ['role' => 'accounts_receivable', 'is_control' => true]);
BookKeeper::chart()->import('sacco-ug'); // import a template (safe to repeat)
BookKeeper::accounts()->tree(); // nested accounts
BookKeeper::account('@cash')->ancestors(); // path from the root
BookKeeper::balance('1000'); // groups roll up their children
// Record and post
$entry = BookKeeper::record([
['account' => '@cash', 'debit' => '1500.00', 'memo' => 'Cash received'],
['account' => '@sales', 'credit' => '1500.00'],
], [
'date' => '2026-03-01',
'memo' => 'Cash sale',
'reference' => 'INV-001',
'branch_id' => 3,
'source' => $order, // any Eloquent model, for drill-down
'idempotency_key' => "order:{$order->id}", // safe to retry
]);
$entry->number; // JE-2026-000001 (gap-free, assigned when posted)
BookKeeper::transfer('@bank', '@cash', 500); // one debit, one credit
BookKeeper::reverse($entry, ['memo' => 'Entered in error']); // the only way to correct
BookKeeper::draft($lines, $options); // save without posting
BookKeeper::ledger()->post($draft);
BookKeeper::openingBalances('2026-01-01', ['1010' => 5000, '2100' => 1200]);
// Accruals that reverse themselves (processed at end of day)
BookKeeper::record($lines, ['date' => '2026-03-31', 'reverse_on' => '2026-04-01']);
// Foreign currency: converted at the line rate or the stored rate for the date
BookKeeper::exchangeRates()->set('USD', '3700', '2026-03-01');
BookKeeper::record([
['account' => '@bank_usd', 'debit' => '100.00', 'currency' => 'USD'],
['account' => '@sales', 'credit' => 370000],
]);
BookKeeper::subledgers()->balance('@member_savings', $account); // one party
BookKeeper::subledgers()->listing('@member_savings'); // every party
BookKeeper::subledgers()->statement('@member_savings', $account, $from, $to); // running balance
BookKeeper::subledgers()->reconcile('@loan_portfolio'); // control = Σ parties?
BookKeeper::subledgers()->averageBalances('@share_capital', $from, $to); // day-weighted averages
BookKeeper::periods()->createFiscalYear(2027);
BookKeeper::periods()->close('2026-06-01'); // lock June; postings now throw PeriodClosedException
BookKeeper::periods()->reopen('2026-06-01');
BookKeeper::close()->closeDay('2026-06-30'); // tasks, scheduled reversals, balance snapshot
BookKeeper::close()->closeMonth('2026-06-30'); // tasks, FX revaluation, depreciation, lock the period
BookKeeper::close()->closeYear(2026); // close open months, closing entry → retained earnings, open next year
BookKeeper::close()->reopenYear(2026);
use OtimOtim\BookKeeper\Contracts\CloseTask;
use OtimOtim\BookKeeper\Close\CloseContext;
class AccrueSavingsInterest implements CloseTask
{
public function handle(CloseContext $context): void
{
// ... compute interest, then post it idempotently:
BookKeeper::post('savings.interest_accrued', [...], [...], [
'date' => $context->date,
'idempotency_key' => $context->key('savings-interest'),
]);
}
}
// config/bookkeeper.php
'close' => ['day' => [AccrueSavingsInterest::class], 'month' => [...], 'year' => [...]],