2

There is a really serious issue about Double Entry Accounting systems with pagination, I think it is common but I still didn't find any solution for my problem yet.

You can use this link to read about the simple Double Entry Accounting systems just like the one I made with Laravel and AngularJS.

In this system, the expected result (for example) is something like this:

ID      In       Out    Balance
1      100.00    0.00   100.00
2       10.00    0.00   110.00
3        0.00   70.00    40.00
4        5.00    0.00    45.00
5        0.00   60.00   -15.00
6       20.00    0.00     5.00

It is very easy to track the balance inside a cumulative function if you were showing all the transactions in one page, the balance in the last transaction is your current balance at the end of the day.

For example, for a specific range of dates $fromDate->$toDate, we do like:

$balanceYesterday = DB::table('journal')->where('date', '<', $fromDate)
        ->join('transactions','transactions.journal_id','journal.id')->where('transactions.type', "=", 0) /* 0 means the account of the company */
        ->select(DB::raw('SUM(amount) as total_balance'))
        ->first()->total_balance;

Now we have balance from yesterday, we depend on it to calculate the balance after that in a cumulative loop until the end of the process, reaching $toDate;

$currentBalance = $currentBalance + $currentTransaction->amount;
$currentTransactionBalance = $currentBalance;

Now the real problem starts when you have a big amount of transactions, and you need to paginate them $journal = $journal->paginate(100);, let's say 100 transactions per page, the system will work as expected for the first page, as we already can calculate the $balanceYesterday and depend on it to calculate the new balance after every transaction to the end of the 100 transactions in the first page.

Next page will have the problem that it doesn't know what was the last balance at the previous transaction in the first page, so it will start again from $balanceYesterday, making the whole table have wrong calculations.

What I did first to fix, was transferring the last transaction amount (in front-end) to the next page as a parameter, and use it as a starting amount to calculate again, and that was the best solution I had as I was using only << PREV and NEXT >> buttons, so it was very easy to fix it like that.

enter image description here

But I lately found out that this workaround will not work if I have a pagination with page numbers, as the user would like to go through pages to explore the journal, now it is impossible to know the last balance at a specific page, and the system will show wrong calculations.

enter image description here

What I am trying to do is finding a way to calculate the balance at a specific transaction, weather it was a credit or debit, I'm looking for a way to know how much the balance was after a specific transaction is done in a specific date, I DON'T WANT TO ADD A NEW BALANCE COLUMN AND SAVE THE BALANCE INSIDE IT, THE USER IS DOING A LOT OF MODIFICATIONS AND EDITS TO THE TRANSACTIONS FROM TIME TO TIME AND THAT WILL BREAK EVERYTHING AS A SMALL AMOUNT MODIFICATION WILL AFFECT ALL THE BALANCES AFTER IT, I CAN NOT depend on IDs of transactions in any method because transactions might have different random dates, so there will be no ordering by ID but there might be ordering by other fields like date or account owner or type or whatever..

I've been scratching my head on this for about 4 months, I searched online and found no solutions, I hope after this long explanation that my problem is clear, and I hope somebody can help me with a solution, please..

Thank you.

tinyCoder
  • 469
  • 9
  • 29
  • You should never rely on the order of ID values, those ID values are only useful to identify a single record, not to sort etc.. The sort order in your case is by the date, however I wonder what you do for same date entries as their order is undefined. – hakre Apr 14 '18 at 23:00
  • I never rely on them, I first order by date, and then order by ID to save the correct order of the transactions that are made at the same day, thank GOD we are not using hours or minutes, only date :| – tinyCoder Apr 14 '18 at 23:04
  • But when you order by date, all entries with the same date are of undefined order, so you can't order by date. And as written: You can not rely on the ID for an order, it is in undefined order. – hakre Apr 14 '18 at 23:07
  • I didn't get your point why I can't order by date, check this screenshot about the situation, we don't care about the time of the transaction in a specific day, transactions made in the same day are ordered in their default order. https://image.ibb.co/kW90U7/Screen_Shot_2018_04_15_at_02_09_59.png – tinyCoder Apr 14 '18 at 23:15
  • A result set of a database has only an undefined order as default order. So if you order by date, all records with the same date are of undefined order. Anyway that is perhaps a bit too theoretic, and in your case you order by date and then ID and it should do it practically as there are no duplicate IDs. – hakre Apr 14 '18 at 23:20

2 Answers2

2

I believe the only thing you really need at this point is to calculate the sum of all transactions from the beginning of the paginated data set (all records, not just the current page's) until one before the first record displayed on the current page.

You can get this by finding the number of transactions that occurred between the start of your entire data set and the current page's transactions, retrieving them via LIMIT, and adding them up.

The first thing you'll want to have is the exact constraints of your pagination query. Since we want to grab another subset of paginated records besides the current page, you want to be sure the results of both queries are in the same order. Reusing the query builder object can help (adjust to match your actual pagination query):

$baseQuery = DB::table('journal')
    ->join('transactions', 'transactions.journal_id', 'journal.id')
    ->where('date', '>', $fromDate)
    ->where('date', '<', $toDate)
    ->where('transactions.type', "=", 0)
    ->orderBy('date', 'asc');
    // Note that we aren't fetching anything here yet.

Then, fetch the paginated result set. This will perform two queries: one for the total count of records, and a second for the specific page's transactions.

$paginatedTransactions = $baseQuery->paginate(100);

From here, we can determine what records we need to retrieve the previous balance for. The pagination object returned is an instance of LengthAwarePaginator, which knows how many records in total, the number of pages, what current page its on, etc.

Using that information, we just do some math to grab the number of records we need:

total records needed = (current page - 1) * records per page

Assuming the user is on page 5, they will see records 401 - 500, so we need to retrieve the previous 400 records.

// If we're on Page 1, or there are not enough records to
// paginate, we don't need to calculate anything.
if ($paginatedTransactions->onFirstPage() || ! $paginatedTransactions->hasPages()) {
    // Don't need to calculate a previous balance. Exit early here!
}

// Use helper methods from the Paginator to calculate
// the number of previous transactions.
$limit = ($paginatedTransactions->currentPage() - 1) * $paginatedTransactions->perPage();

Now that we have the number of transactions that occurred within our data set but before the current page, we can retrieve and calculate the sum by again utilizing the base query:

$previousBalance = $baseQuery->limit($limit)->sum('amount');

Adding a highlight here to explain that using your database to perform the SUM calculations will be a big performance benefit, rather than doing it in a loop in PHP. Take advantage of the DB as often as you can!

Add this balance to your original "yesterday" balance, and you should have an accurate beginning balance for the paginated transactions.

Note: everything pseudo-coded from theory, may need adjustments. Happy to revise if there are questions or issues.

Aken Roberts
  • 11,722
  • 3
  • 31
  • 39
  • Thank you, I couldn't hold my tears as it is almost a perfect solution, but theoretically, because when you call `$previousBalance = $baseQuery->limit($limit)->sum('amount');` it is calculating the amount of the items in the current page, it looks like when you call `->paginate` it is `publicating?` the value, even if you save the `$paginatedTransactions` in a temporary value and call the pagination method, it is affecting the $baseQuery result somehow! – tinyCoder Apr 15 '18 at 09:53
  • Man I love you, thank you very much for this amazing solution, i solved the issue by calculating the `$limit` using the `Request()->get('page')` and the custom `$perPage` value, and I ran the paginate function after calculating the `$previousBalance` so it will not affect the result, thank you :'( – tinyCoder Apr 15 '18 at 10:04
  • Looking at `Builder::paginate()`, it does look like it modifies the builder further. You could also use the `clone` keyword to duplicate the base query builder in order to prevent that. Glad I could help! – Aken Roberts Apr 15 '18 at 21:01
1

You should be able to formulate a truth statement for the balance for each record as long as you can tell what the order is to calculate the sum for the balance at each point within that ordered list.

For sure this come with a massive overhead as you need to query the whole table for each record you display, but first of all one must be able to do that. As you've shown in the example, you are as long as you do not paginate.

What you could do for pagination is to pre-calculate the balance for each record and store it in relation to the original record. This would de-normalize your data but with the benefit that creating the pagination is rather straight forward.

hakre
  • 178,314
  • 47
  • 389
  • 754
  • thanks, I was just editing my question when you posted, I was saying that I do NOT want to save the balance in a new column, I tried this before for some time at the result was very bad, the user is making changes to the transactions amount from time to time, that is breaking the whole balances after the modified one.. I was looking for a dynamic method. – tinyCoder Apr 14 '18 at 22:42
  • Them formulate the truth statement for each record and run it per each record. For the extra column: Recalculate those which need recalculation when a recordset is changed or added, see database triggers for that. Then your database will take care of that. – hakre Apr 14 '18 at 22:44
  • I already tried this, I used edit and insert triggers to modify the balances after one is changed or inserted in a prev. date, the performance result was bad for 50K journals (=100K transactions).. the system is also storing data from years before. I am very confused about this. – tinyCoder Apr 14 '18 at 22:47
  • Don't be confused. As there is a large amount of data, it takes time to process it. That's totally like it is. You can't get all these operations for free. However it makes no sense in bookkeeping to allow users to edit an existing record nor to add or remove records for time-spans which are already closed. So in theory you should never modify balances of the years closed - which is the past. So you can calculate the balances for the whole past w/o issues once only. If something becomes the past, you can calculate for that span as well. That should reduce the amount of caclulcations. – hakre Apr 14 '18 at 22:57
  • SURE! that was the main idea about this system, and in all similar systems, you should not modify, as if you made a mistake you should add a COUNTER transaction to fix the mistake. Even if we closed the old years and worked only in current year, we must allow the user to modify as the type of his business forces us to, and in one year only there are a lot to calculate, and the user needs what he calls a GOD-MODE active to modify anything anytime..WTH. – tinyCoder Apr 14 '18 at 23:02