diff --git a/.gitignore b/.gitignore index 8b593aa..b028df3 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,6 @@ clover.* # PHPSTORM .idea -Dockerfile \ No newline at end of file +Dockerfile +.vscode +ifrs.code-workspace diff --git a/README.md b/README.md index 2b28deb..68b2d6e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ This Package enables any Laravel application to generate [International Financia The package supports multiple Entities (Companies), Account Categorization, Transaction assignment, Start of Year Opening Balances and accounting for VAT Transactions. Transactions are also protected against tampering via direct database changes ensuring the integrity of the Ledger. Outstanding amounts for clients and suppliers can also be displayed according to how long they have been outstanding using configurable time periods (Current, 31 - 60 days, 61 - 90 days etc). Finally, the package supports the automated posting of forex difference transactions both within the reporting period as well as translating foreign denominated account balances at a set closing rate. -This package's functionality is now available as a REST API Service. More details can be found [here](https://microbooks.io) +This package is a community initiative of [microbooks.io](https://microbooks.io). + ## Table of contents - [Eloquent IFRS](#eloquent-ifrs) - [Table of contents](#table-of-contents) diff --git a/src/Models/Ledger.php b/src/Models/Ledger.php index ffd8ddb..32992f1 100644 --- a/src/Models/Ledger.php +++ b/src/Models/Ledger.php @@ -89,7 +89,7 @@ private static function getLedgers(Transaction $transaction): array private static function postVat($appliedVats, $transaction, $lineItem): void { $rate = $transaction->exchangeRate->rate; - foreach($appliedVats as $appliedVat){ + foreach ($appliedVats as $appliedVat) { list($post, $folio) = Ledger::getLedgers($transaction); // identical double entry data @@ -104,7 +104,7 @@ private static function postVat($appliedVats, $transaction, $lineItem): void // different double entry data $post->post_account = $folio->folio_account = $lineItem->vat_inclusive ? $lineItem->account_id : $transaction->account_id; $post->folio_account = $folio->post_account = $appliedVat->vat->account_id; - + $post->save(); $folio->save(); } @@ -139,12 +139,12 @@ private static function postBasic(Transaction $transaction): void $post->save(); $folio->save(); - + if (count($lineItem->appliedVats) > 0) { Ledger::postVat($lineItem->appliedVats, $transaction, $lineItem); } } - + // reload ledgers to reflect changes $transaction->load('ledgers'); } @@ -161,13 +161,11 @@ private static function postBasic(Transaction $transaction): void */ private static function makeCompountEntryLedgers(array $posts, array $folios, Transaction $transaction, $entryType): bool { - if(count($posts) == 0){ + if (count($posts) == 0) { return true; } else { - $postAccount = array_key_first($posts); - $amount = $posts[$postAccount]; - - return Ledger::allocateAmount($postAccount, $amount, $posts, $folios, $transaction, $entryType); + $key = array_key_first($posts); + return Ledger::allocateAmount($posts[$key]['id'], $posts[$key]['amount'], $posts, $folios, $transaction, $entryType); } } @@ -185,12 +183,14 @@ private static function makeCompountEntryLedgers(array $posts, array $folios, Tr */ private static function allocateAmount($postAccount, $amount, $posts, $folios, $transaction, $entryType): bool { - if($amount == 0){ - unset($posts[$postAccount]); + if ($amount == 0) { + $key = array_key_first($posts); + unset($posts[$key]); return Ledger::makeCompountEntryLedgers($posts, $folios, $transaction, $entryType); } else { - $folioAccount = array_key_first($folios); + $key = array_key_first($folios); + $folioAccount = $folios[$key]['id']; $ledger = new Ledger(); @@ -202,18 +202,18 @@ private static function allocateAmount($postAccount, $amount, $posts, $folios, $ $ledger->post_account = $postAccount; $ledger->folio_account = $folioAccount; - if($folios[$folioAccount] > $amount){ + if ($folios[$key]['amount'] > $amount) { $ledger->amount = $amount; $ledger->save(); - $folios[$folioAccount] -= $ledger->amount; + $folios[$key]['amount'] -= $ledger->amount; $amount = 0; } else { - $debitAmount = $folios[$folioAccount]; + $debitAmount = $folios[$key]['amount']; $ledger->amount = $debitAmount; $ledger->save(); - - unset($folios[$folioAccount]); + + unset($folios[$key]); $amount -= $ledger->amount; } @@ -252,9 +252,9 @@ public static function post(Transaction $transaction): void //Remove current ledgers if any prior to creating new ones (prevents bypassing Posted Transaction Exception) $transaction->ledgers()->delete(); - if($transaction->compound){ + if ($transaction->compound) { Ledger::postCompound($transaction); - }else{ + } else { Ledger::postBasic($transaction); } } diff --git a/src/Models/Transaction.php b/src/Models/Transaction.php index 5b34ead..a1ee5b2 100644 --- a/src/Models/Transaction.php +++ b/src/Models/Transaction.php @@ -141,10 +141,7 @@ class Transaction extends Model implements Segregatable, Recyclable, Clearable, * @var array $compoundEntries */ - protected $compoundEntries = [ - Balance::CREDIT => [], - Balance::DEBIT => [] - ]; + protected $compoundEntries = []; /** * Check if LineItem already exists. @@ -188,6 +185,22 @@ private static function getCompoundEntrytype(bool $credited): string return $credited ? Balance::CREDIT : Balance::DEBIT; } + /** + * Get the sum of the amounts on the given side of the compound entries + * + * @param string entryType + * @return float + */ + private function entriesSum(string $entryType): float + { + $sum = 0; + foreach ($this->compoundEntries[$entryType] as $entry) { + $sum += $entry['amount']; + } + return $sum; + } + + /** * Add Compound Entry to Transaction CompoundEntries. * @@ -196,7 +209,7 @@ private static function getCompoundEntrytype(bool $credited): string */ protected function addCompoundEntry(array $compoundEntry, bool $credited): void { - $this->compoundEntries[Transaction::getCompoundEntrytype($credited)][$compoundEntry['id']] = $compoundEntry['amount']; + $this->compoundEntries[Transaction::getCompoundEntrytype($credited)][] = $compoundEntry; } /** @@ -427,10 +440,16 @@ public function getAmountAttribute(): float public function getCompoundEntries() { if ($this->compound) { - $this->compoundEntries[Transaction::getCompoundEntrytype($this->credited)][$this->account_id] = floatval($this->main_account_amount); - foreach ($this->lineItems as $lineItem) { - $this->compoundEntries[Transaction::getCompoundEntrytype($lineItem->credited)][$lineItem->account_id] = $lineItem->amount * $lineItem->quantity; + $this->compoundEntries = [ + Balance::CREDIT => [], + Balance::DEBIT => [] + ]; + + $this->compoundEntries[Transaction::getCompoundEntrytype($this->credited)][] = ['id' => $this->account_id, 'amount' => floatval($this->main_account_amount)]; + + foreach ($this->getLineItems() as $lineItem) { + $this->compoundEntries[Transaction::getCompoundEntrytype($lineItem->credited)][] = ['id' => $lineItem->account_id, 'amount' => $lineItem->amount * $lineItem->quantity]; } } @@ -562,8 +581,11 @@ public function removeLineItem(LineItem $lineItem): void if ($this->compound) { $entryType = Transaction::getCompoundEntrytype($lineItem->credited); - if (array_key_exists($lineItem->account_id, $this->compoundEntries[$entryType])) { - unset($this->compoundEntries[$entryType][$lineItem->account_id]); + + foreach ($this->compoundEntries[$entryType] as $index => $entry) { + if ($lineItem->account_id == $entry['id']) { + unset($this->compoundEntries[$entryType][$index]); + } } } @@ -661,8 +683,9 @@ public function post(): void $this->save(); - extract($this->getCompoundEntries()); - if ($this->compound && array_sum($C) != array_sum($D)) { + $this->getCompoundEntries(); + // dd($this->getCompoundEntries()); + if ($this->compound && $this->entriesSum(Balance::CREDIT) != $this->entriesSum(Balance::DEBIT)) { throw new UnbalancedTransaction(); } diff --git a/src/Transactions/JournalEntry.php b/src/Transactions/JournalEntry.php index 0ae4d82..5ff6899 100644 --- a/src/Transactions/JournalEntry.php +++ b/src/Transactions/JournalEntry.php @@ -78,13 +78,13 @@ public function __construct($attributes = []) */ public function addLineItem(LineItem $lineItem): bool { - if ($this->compound && count($lineItem->vat) > 1) { + if ($this->compound && count($lineItem->getVats()) > 0) { throw new MultipleVatError('Compound Journal Entries cannot have Vat'); } $success = parent::addLineItem($lineItem); - - if($success && $this->compound){ + + if ($success && $this->compound) { parent::addCompoundEntry(['id' => $lineItem->account_id, 'amount' => $lineItem->amount * $lineItem->quantity], $lineItem->credited); } return $success; @@ -96,19 +96,9 @@ public function addLineItem(LineItem $lineItem): bool public function save(array $options = []): bool { - if($this->compound && (is_null($this->main_account_amount) || $this->main_account_amount == 0)){ + if ($this->compound && (is_null($this->main_account_amount) || $this->main_account_amount == 0)) { throw new MissingMainAccountAmount(); } - - if($this->compound){ - parent::addCompoundEntry([ - 'id' => $this->account_id, - 'amount' => $this->main_account_amount - ], - $this->credited - ); - } - return parent::save(); } } diff --git a/tests/Feature/AccountScheduleTest.php b/tests/Feature/AccountScheduleTest.php index 18c88de..f32b3be 100644 --- a/tests/Feature/AccountScheduleTest.php +++ b/tests/Feature/AccountScheduleTest.php @@ -256,7 +256,7 @@ public function testClientAccountScheduleTest() $this->assertEquals($schedule->balances["originalAmount"], 241); $this->assertEquals($schedule->balances["amountCleared"], 95); $this->assertEquals($schedule->balances["unclearedAmount"], 146); - $this->assertEquals($schedule->balances["totalAge"], 365); + $this->assertEquals($schedule->balances["totalAge"], Carbon::now()->isLeapYear() ? 366 : 365); $this->assertEquals($schedule->balances["averageAge"], 122); } @@ -447,7 +447,7 @@ public function testSupplierAccountAccountSchedule() $this->assertEquals($schedule->balances['originalAmount'], 686.4); $this->assertEquals($schedule->balances['amountCleared'], 221.8); $this->assertEqualsWithDelta($schedule->balances['unclearedAmount'], 464.6, 0.1); - $this->assertEquals($schedule->balances['totalAge'], 365); + $this->assertEquals($schedule->balances['totalAge'], Carbon::now()->isLeapYear() ? 366 : 365); $this->assertEquals($schedule->balances['averageAge'], 122.0); } @@ -498,7 +498,7 @@ public function testAccountScheduleCurrencyFilters() ])->id, "quantity" => 1, ]); - + $supplierPayment1->addLineItem($lineItem); $supplierPayment1->post(); @@ -509,7 +509,7 @@ public function testAccountScheduleCurrencyFilters() 'cleared_type' => "IFRS\Models\Balance", "amount" => 24, ]); - + // Foreign currency opening balances $balance2 = factory(Balance::class)->create([ "account_id" => $account->id, @@ -700,7 +700,7 @@ public function testAccountScheduleCurrencyFilters() $this->assertEquals($schedule->balances['originalAmount'], 43248.0); $this->assertEquals($schedule->balances['amountCleared'], 11554); $this->assertEquals($schedule->balances['unclearedAmount'], 31694.0); - $this->assertEquals($schedule->balances['totalAge'], 730); + $this->assertEquals($schedule->balances['totalAge'], Carbon::now()->isLeapYear() ? 732 : 730); $this->assertEquals($schedule->balances['averageAge'], 183.0); // Base Currency transactions @@ -722,7 +722,7 @@ public function testAccountScheduleCurrencyFilters() $this->assertEquals($schedule->balances['originalAmount'], 408.0); $this->assertEquals($schedule->balances['amountCleared'], 109); $this->assertEquals($schedule->balances['unclearedAmount'], 299.0); - $this->assertEquals($schedule->balances['totalAge'], 365); + $this->assertEquals($schedule->balances['totalAge'], Carbon::now()->isLeapYear() ? 366 : 365); $this->assertEquals($schedule->balances['averageAge'], 183.0); // Foreign Currency transactions @@ -744,8 +744,7 @@ public function testAccountScheduleCurrencyFilters() $this->assertEquals($schedule->balances['originalAmount'], 408.0); $this->assertEquals($schedule->balances['amountCleared'], 109); $this->assertEquals($schedule->balances['unclearedAmount'], 299.0); - $this->assertEquals($schedule->balances['totalAge'], 365); + $this->assertEquals($schedule->balances['totalAge'], Carbon::now()->isLeapYear() ? 366 : 365); $this->assertEquals($schedule->balances['averageAge'], 183.0); - } } diff --git a/tests/Unit/AssignmentTest.php b/tests/Unit/AssignmentTest.php index 3d292c0..76f0d24 100644 --- a/tests/Unit/AssignmentTest.php +++ b/tests/Unit/AssignmentTest.php @@ -817,8 +817,8 @@ public function testUnassignableTransaction() $this->expectException(UnassignableTransaction::class); $this->expectExceptionMessage( "Client Invoice Transaction cannot have assignments. " - . "Assignment Transaction must be one of: " - . "Client Receipt, Supplier Payment, Credit Note, Debit Note, Journal Entry" + . "Assignment Transaction must be one of: " + . "Client Receipt, Supplier Payment, Credit Note, Debit Note, Journal Entry" ); $assignment = new Assignment([ @@ -883,8 +883,8 @@ public function testUnclearableTransaction() $this->expectException(UnclearableTransaction::class); $this->expectExceptionMessage( "Client Receipt Transaction cannot be cleared. " - . "Transaction to be cleared must be one of: " - . "Client Invoice, Supplier Bill, Journal Entry" + . "Transaction to be cleared must be one of: " + . "Client Invoice, Supplier Bill, Journal Entry" ); $assignment = new Assignment([ @@ -1540,7 +1540,6 @@ public function testAssignmentCompoundTransaction() $transaction->addLineItem($line); $transaction->post(); - $cleared = new JournalEntry([ "account_id" => $account->id, "transaction_date" => Carbon::now(), diff --git a/tests/Unit/JournalEntryTest.php b/tests/Unit/JournalEntryTest.php index 174b4cb..24485ca 100644 --- a/tests/Unit/JournalEntryTest.php +++ b/tests/Unit/JournalEntryTest.php @@ -186,11 +186,16 @@ public function testCompoundJournalEntryTransaction() ]); $journalEntry->save(); - + + $doubleAccount = factory(Account::class)->create([ + 'category_id' => null + ]); + $lineItem1 = factory(LineItem::class)->create([ "amount" => 30, "quantity" => 1, "credited" => true, + "account_id" => $doubleAccount->id ]); $lineItem2 = factory(LineItem::class)->create([ @@ -199,38 +204,48 @@ public function testCompoundJournalEntryTransaction() ]); $lineItem3 = factory(LineItem::class)->create([ - "amount" => 15, + "amount" => 20, "quantity" => 1, ]); + $lineItem4 = factory(LineItem::class)->create([ + "amount" => 5, + "quantity" => 1, + "credited" => true, + "account_id" => $doubleAccount->id + ]); + $journalEntry->addLineItem($lineItem1); $journalEntry->addLineItem($lineItem2); $journalEntry->addLineItem($lineItem3); + $journalEntry->addLineItem($lineItem4); - $this->assertEquals($journalEntry->amount, 40); + $this->assertEquals($journalEntry->amount, 45); $this->assertEquals($journalEntry->getCompoundEntries(), [ "C" => [ - $lineItem1->account_id => 30, - $journalEntry->account_id => 10.0 + ['id' => $journalEntry->account_id, 'amount' => 10.0], + ['id' => $lineItem1->account_id, 'amount' => 30], + ['id' => $lineItem4->account_id, 'amount' => 5] ], "D" => [ - $lineItem2->account_id => 25, - $lineItem3->account_id => 15 + ['id' => $lineItem2->account_id, 'amount' => 25], + ['id' => $lineItem3->account_id, 'amount' => 20] ] ]); $journalEntry->post(); - + $transaction = Transaction::find($journalEntry->id); - + $this->assertEquals($transaction->getCompoundEntries(), [ "C" => [ - $lineItem1->account_id => 30, - $journalEntry->account_id => 10.0 + ['id' => $journalEntry->account_id, 'amount' => 10.0], + ['id' => $lineItem1->account_id, 'amount' => 30], + ['id' => $lineItem4->account_id, 'amount' => 5], ], "D" => [ - $lineItem2->account_id => 25, - $lineItem3->account_id => 15 + ['id' => $lineItem2->account_id, 'amount' => 25], + ['id' => $lineItem3->account_id, 'amount' => 20] ] ]); @@ -238,15 +253,15 @@ public function testCompoundJournalEntryTransaction() $this->assertEquals(Ledger::contribution($transaction->account, $transaction->id), -10); // lineItem 1 - $this->assertEquals(Ledger::contribution($lineItem1->account, $transaction->id), -30); + $this->assertEquals(Ledger::contribution($lineItem1->account, $transaction->id), -35); // lineItem 2 $this->assertEquals(Ledger::contribution($lineItem2->account, $transaction->id), 25); - + // lineItem 3 - $this->assertEquals(Ledger::contribution($lineItem3->account, $transaction->id), 15); + $this->assertEquals(Ledger::contribution($lineItem3->account, $transaction->id), 20); - $this->assertEquals($transaction->amount, 40); + $this->assertEquals($transaction->amount, 45); } /** @@ -305,7 +320,7 @@ public function testInvalidVatRateException() $this->expectException(MultipleVatError::class); $this->expectExceptionMessage('Compound Journal Entries cannot have Vat '); - + $journalEntry->addLineItem($lineItem); } @@ -327,7 +342,7 @@ public function testMissingMainAccountAmountException() $this->expectException(MissingMainAccountAmount::class); $this->expectExceptionMessage('Compund Journal Entries must have a Main Account Amount '); - + $journalEntry->save(); } }