Skip to content

Commit

Permalink
Merge pull request #164 from ekmungai/multiple-compound-line-items-pe…
Browse files Browse the repository at this point in the history
…r-account

Multiple compound line items per account
  • Loading branch information
ekmungai authored Mar 26, 2024
2 parents c90caf7 + 6e848dc commit 1a368b6
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 79 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ clover.*

# PHPSTORM
.idea
Dockerfile
Dockerfile
.vscode
ifrs.code-workspace
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 19 additions & 19 deletions src/Models/Ledger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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');
}
Expand All @@ -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);
}
}

Expand All @@ -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();

Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
}
}
Expand Down
47 changes: 35 additions & 12 deletions src/Models/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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];
}
}

Expand Down Expand Up @@ -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]);
}
}
}

Expand Down Expand Up @@ -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();
}

Expand Down
18 changes: 4 additions & 14 deletions src/Transactions/JournalEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
}
15 changes: 7 additions & 8 deletions tests/Feature/AccountScheduleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -498,7 +498,7 @@ public function testAccountScheduleCurrencyFilters()
])->id,
"quantity" => 1,
]);

$supplierPayment1->addLineItem($lineItem);

$supplierPayment1->post();
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);

}
}
9 changes: 4 additions & 5 deletions tests/Unit/AssignmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -1540,7 +1540,6 @@ public function testAssignmentCompoundTransaction()
$transaction->addLineItem($line);

$transaction->post();

$cleared = new JournalEntry([
"account_id" => $account->id,
"transaction_date" => Carbon::now(),
Expand Down
Loading

0 comments on commit 1a368b6

Please sign in to comment.