mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-10-31 02:36:28 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			295 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			295 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * BalanceReportHelper.php
 | |
|  * Copyright (C) 2016 thegrumpydictator@gmail.com
 | |
|  *
 | |
|  * This software may be modified and distributed under the terms of the
 | |
|  * Creative Commons Attribution-ShareAlike 4.0 International License.
 | |
|  *
 | |
|  * See the LICENSE file for details.
 | |
|  */
 | |
| 
 | |
| declare(strict_types = 1);
 | |
| 
 | |
| namespace FireflyIII\Helpers\Report;
 | |
| 
 | |
| use Carbon\Carbon;
 | |
| use DB;
 | |
| use FireflyIII\Helpers\Collection\Balance;
 | |
| use FireflyIII\Helpers\Collection\BalanceEntry;
 | |
| use FireflyIII\Helpers\Collection\BalanceHeader;
 | |
| use FireflyIII\Helpers\Collection\BalanceLine;
 | |
| use FireflyIII\Models\BudgetLimit;
 | |
| use FireflyIII\Models\Tag;
 | |
| use FireflyIII\Models\TransactionType;
 | |
| use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
 | |
| use Illuminate\Database\Query\JoinClause;
 | |
| use Illuminate\Support\Collection;
 | |
| use Log;
 | |
| 
 | |
| /**
 | |
|  * Class BalanceReportHelper
 | |
|  *
 | |
|  * @SuppressWarnings(PHPMD.CouplingBetweenObjects) // I can't really help it.
 | |
|  * @package FireflyIII\Helpers\Report
 | |
|  */
 | |
| class BalanceReportHelper implements BalanceReportHelperInterface
 | |
| {
 | |
| 
 | |
|     /** @var  BudgetRepositoryInterface */
 | |
|     protected $budgetRepository;
 | |
| 
 | |
|     /**
 | |
|      * ReportHelper constructor.
 | |
|      *
 | |
|      *
 | |
|      * @param BudgetRepositoryInterface $budgetRepository
 | |
|      */
 | |
|     public function __construct(BudgetRepositoryInterface $budgetRepository)
 | |
|     {
 | |
|         $this->budgetRepository = $budgetRepository;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * @param Collection $accounts
 | |
|      * @param Carbon     $start
 | |
|      * @param Carbon     $end
 | |
|      *
 | |
|      * @return Balance
 | |
|      */
 | |
|     public function getBalanceReport(Collection $accounts, Carbon $start, Carbon $end): Balance
 | |
|     {
 | |
|         Log::debug('Start of balance report');
 | |
|         $balance      = new Balance;
 | |
|         $header       = new BalanceHeader;
 | |
|         $budgetLimits = $this->budgetRepository->getAllBudgetLimits($start, $end);
 | |
|         foreach ($accounts as $account) {
 | |
|             Log::debug(sprintf('Add account %s to headers.', $account->name));
 | |
|             $header->addAccount($account);
 | |
|         }
 | |
| 
 | |
|         /** @var BudgetLimit $budgetLimit */
 | |
|         foreach ($budgetLimits as $budgetLimit) {
 | |
|             $line = $this->createBalanceLine($budgetLimit, $accounts);
 | |
|             $balance->addBalanceLine($line);
 | |
|         }
 | |
|         Log::debug('Create rest of the things.');
 | |
|         $noBudgetLine       = $this->createNoBudgetLine($accounts, $start, $end);
 | |
|         $coveredByTagLine   = $this->createTagsBalanceLine($accounts, $start, $end);
 | |
|         $leftUnbalancedLine = $this->createLeftUnbalancedLine($noBudgetLine, $coveredByTagLine);
 | |
| 
 | |
|         $balance->addBalanceLine($noBudgetLine);
 | |
|         $balance->addBalanceLine($coveredByTagLine);
 | |
|         $balance->addBalanceLine($leftUnbalancedLine);
 | |
|         $balance->setBalanceHeader($header);
 | |
| 
 | |
|         Log::debug('Clear unused budgets.');
 | |
|         // remove budgets without expenses from balance lines:
 | |
|         $balance = $this->removeUnusedBudgets($balance);
 | |
| 
 | |
|         Log::debug('Return report.');
 | |
| 
 | |
|         return $balance;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method collects all transfers that are part of a "balancing act" tag
 | |
|      * and groups the amounts of those transfers by their destination account.
 | |
|      *
 | |
|      * This is used to indicate which expenses, usually outside of budgets, have been
 | |
|      * corrected by transfers from a savings account.
 | |
|      *
 | |
|      * @param Collection $accounts
 | |
|      * @param Carbon     $start
 | |
|      * @param Carbon     $end
 | |
|      *
 | |
|      * @return Collection
 | |
|      */
 | |
|     private function allCoveredByBalancingActs(Collection $accounts, Carbon $start, Carbon $end): Collection
 | |
|     {
 | |
|         $ids = $accounts->pluck('id')->toArray();
 | |
|         $set = auth()->user()->tags()
 | |
|                      ->leftJoin('tag_transaction_journal', 'tag_transaction_journal.tag_id', '=', 'tags.id')
 | |
|                      ->leftJoin('transaction_journals', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
 | |
|                      ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id')
 | |
|                      ->leftJoin(
 | |
|                          'transactions AS t_source', function (JoinClause $join) {
 | |
|                          $join->on('transaction_journals.id', '=', 't_source.transaction_journal_id')->where('t_source.amount', '<', 0);
 | |
|                      }
 | |
|                      )
 | |
|                      ->leftJoin(
 | |
|                          'transactions AS t_destination', function (JoinClause $join) {
 | |
|                          $join->on('transaction_journals.id', '=', 't_destination.transaction_journal_id')->where('t_destination.amount', '>', 0);
 | |
|                      }
 | |
|                      )
 | |
|                      ->where('tags.tagMode', 'balancingAct')
 | |
|                      ->where('transaction_types.type', TransactionType::TRANSFER)
 | |
|                      ->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
 | |
|                      ->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
 | |
|                      ->whereNull('transaction_journals.deleted_at')
 | |
|                      ->whereIn('t_source.account_id', $ids)
 | |
|                      ->whereIn('t_destination.account_id', $ids)
 | |
|                      ->groupBy('t_destination.account_id')
 | |
|                      ->get(
 | |
|                          [
 | |
|                              't_destination.account_id',
 | |
|                              DB::raw('SUM(t_destination.amount) AS sum'),
 | |
|                          ]
 | |
|                      );
 | |
| 
 | |
|         return $set;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * @param BudgetLimit $budgetLimit
 | |
|      * @param Collection  $accounts
 | |
|      *
 | |
|      * @return BalanceLine
 | |
|      */
 | |
|     private function createBalanceLine(BudgetLimit $budgetLimit, Collection $accounts): BalanceLine
 | |
|     {
 | |
|         $line = new BalanceLine;
 | |
|         $line->setBudget($budgetLimit->budget);
 | |
|         $line->setBudgetLimit($budgetLimit);
 | |
| 
 | |
|         // loop accounts:
 | |
|         foreach ($accounts as $account) {
 | |
|             $balanceEntry = new BalanceEntry;
 | |
|             $balanceEntry->setAccount($account);
 | |
|             $spent = $this->budgetRepository->spentInPeriod(new Collection([$budgetLimit->budget]), new Collection([$account]), $budgetLimit->start_date, $budgetLimit->end_date);
 | |
|             $balanceEntry->setSpent($spent);
 | |
|             $line->addBalanceEntry($balanceEntry);
 | |
|         }
 | |
| 
 | |
|         return $line;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param BalanceLine $noBudgetLine
 | |
|      * @param BalanceLine $coveredByTagLine
 | |
|      *
 | |
|      * @return BalanceLine
 | |
|      */
 | |
|     private function createLeftUnbalancedLine(BalanceLine $noBudgetLine, BalanceLine $coveredByTagLine): BalanceLine
 | |
|     {
 | |
|         $line = new BalanceLine;
 | |
|         $line->setRole(BalanceLine::ROLE_DIFFROLE);
 | |
|         $noBudgetEntries = $noBudgetLine->getBalanceEntries();
 | |
|         $tagEntries      = $coveredByTagLine->getBalanceEntries();
 | |
| 
 | |
|         foreach ($noBudgetEntries as $entry) {
 | |
|             $account  = $entry->getAccount();
 | |
|             $tagEntry = $tagEntries->filter(
 | |
|                 function (BalanceEntry $current) use ($account) {
 | |
|                     return $current->getAccount()->id === $account->id;
 | |
|                 }
 | |
|             );
 | |
|             if ($tagEntry->first()) {
 | |
|                 // found corresponding entry. As we should:
 | |
|                 $newEntry = new BalanceEntry;
 | |
|                 $newEntry->setAccount($account);
 | |
|                 $spent = bcadd($tagEntry->first()->getLeft(), $entry->getSpent());
 | |
|                 $newEntry->setSpent($spent);
 | |
|                 $line->addBalanceEntry($newEntry);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $line;
 | |
| 
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Collection $accounts
 | |
|      * @param Carbon     $start
 | |
|      * @param Carbon     $end
 | |
|      *
 | |
|      * @return BalanceLine
 | |
|      */
 | |
|     private function createNoBudgetLine(Collection $accounts, Carbon $start, Carbon $end): BalanceLine
 | |
|     {
 | |
|         $empty = new BalanceLine;
 | |
| 
 | |
|         foreach ($accounts as $account) {
 | |
|             $spent = $this->budgetRepository->spentInPeriodWoBudget(new Collection([$account]), $start, $end);
 | |
|             // budget
 | |
|             $budgetEntry = new BalanceEntry;
 | |
|             $budgetEntry->setAccount($account);
 | |
|             $budgetEntry->setSpent($spent);
 | |
|             $empty->addBalanceEntry($budgetEntry);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $empty;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Collection $accounts
 | |
|      * @param Carbon     $start
 | |
|      * @param Carbon     $end
 | |
|      *
 | |
|      * @return BalanceLine
 | |
|      */
 | |
|     private function createTagsBalanceLine(Collection $accounts, Carbon $start, Carbon $end): BalanceLine
 | |
|     {
 | |
|         $tags     = new BalanceLine;
 | |
|         $tagsLeft = $this->allCoveredByBalancingActs($accounts, $start, $end);
 | |
| 
 | |
|         $tags->setRole(BalanceLine::ROLE_TAGROLE);
 | |
| 
 | |
|         foreach ($accounts as $account) {
 | |
|             $leftEntry = $tagsLeft->filter(
 | |
|                 function (Tag $tag) use ($account) {
 | |
|                     return $tag->account_id == $account->id;
 | |
|                 }
 | |
|             );
 | |
|             $left      = '0';
 | |
|             if (!is_null($leftEntry->first())) {
 | |
|                 $left = $leftEntry->first()->sum;
 | |
|             }
 | |
| 
 | |
|             // balanced by tags
 | |
|             $tagEntry = new BalanceEntry;
 | |
|             $tagEntry->setAccount($account);
 | |
|             $tagEntry->setLeft($left);
 | |
|             $tags->addBalanceEntry($tagEntry);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $tags;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Balance $balance
 | |
|      * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly 5.
 | |
|      *
 | |
|      * @return Balance
 | |
|      */
 | |
|     private function removeUnusedBudgets(Balance $balance): Balance
 | |
|     {
 | |
|         $set    = $balance->getBalanceLines();
 | |
|         $newSet = new Collection;
 | |
|         foreach ($set as $entry) {
 | |
|             if (!is_null($entry->getBudget()->id)) {
 | |
|                 $sum = '0';
 | |
|                 foreach ($entry->getBalanceEntries() as $balanceEntry) {
 | |
|                     $sum = bcadd($sum, $balanceEntry->getSpent());
 | |
|                 }
 | |
|                 if (bccomp($sum, '0') === -1) {
 | |
|                     $newSet->push($entry);
 | |
|                 }
 | |
|                 continue;
 | |
|             }
 | |
|             $newSet->push($entry);
 | |
|         }
 | |
| 
 | |
|         $balance->setBalanceLines($newSet);
 | |
| 
 | |
|         return $balance;
 | |
| 
 | |
|     }
 | |
| 
 | |
| }
 |