diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index 639e56f1e8..8677cbc236 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -476,154 +476,4 @@ class BudgetController extends Controller } - /** - * Get the amount of money budgeted in a period. - * - * @param Budget $budget - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function getBudgetedInPeriod(Budget $budget, Carbon $start, Carbon $end): array // get data + augment with info - { - $key = app('navigation')->preferredCarbonFormat($start, $end); - $range = app('navigation')->preferredRangeFormat($start, $end); - $current = clone $start; - $budgeted = []; - while ($current < $end) { - /** @var Carbon $currentStart */ - $currentStart = app('navigation')->startOfPeriod($current, $range); - /** @var Carbon $currentEnd */ - $currentEnd = app('navigation')->endOfPeriod($current, $range); - $budgetLimits = $this->repository->getBudgetLimits($budget, $currentStart, $currentEnd); - $index = $currentStart->format($key); - $budgeted[$index] = $budgetLimits->sum('amount'); - $currentEnd->addDay(); - $current = clone $currentEnd; - } - - return $budgeted; - } - - - /** - * Get the expenses for a budget in a date range. - * - * @param Collection $limits - * @param Budget $budget - * @param Carbon $start - * @param Carbon $end - * - * @return array - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function getExpensesForBudget(Collection $limits, Budget $budget, Carbon $start, Carbon $end): array // get data + augment with info - { - $return = []; - if (0 === $limits->count()) { - $spent = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); - if (0 !== bccomp($spent, '0')) { - $return[$budget->name]['spent'] = bcmul($spent, '-1'); - $return[$budget->name]['left'] = 0; - $return[$budget->name]['overspent'] = 0; - } - - return $return; - } - - $rows = $this->spentInPeriodMulti($budget, $limits); - foreach ($rows as $name => $row) { - if (0 !== bccomp($row['spent'], '0') || 0 !== bccomp($row['left'], '0')) { - $return[$name] = $row; - } - } - unset($rows); - - return $return; - } - - /** - * - * Returns an array with the following values: - * 0 => - * 'name' => name of budget + repetition - * 'left' => left in budget repetition (always zero) - * 'overspent' => spent more than budget repetition? (always zero) - * 'spent' => actually spent in period for budget - * 1 => (etc) - * - * @param Budget $budget - * @param Collection $limits - * - * @return array - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * - */ - protected function spentInPeriodMulti(Budget $budget, Collection $limits): array // get data + augment with info - { - $return = []; - $format = (string)trans('config.month_and_day'); - $name = $budget->name; - /** @var BudgetLimit $budgetLimit */ - foreach ($limits as $budgetLimit) { - $expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); - $expenses = app('steam')->positive($expenses); - - if ($limits->count() > 1) { - $name = $budget->name . ' ' . trans( - 'firefly.between_dates', - [ - 'start' => $budgetLimit->start_date->formatLocalized($format), - 'end' => $budgetLimit->end_date->formatLocalized($format), - ] - ); - } - $amount = $budgetLimit->amount; - $leftInLimit = bcsub($amount, $expenses); - $hasOverspent = bccomp($leftInLimit, '0') === -1; - $left = $hasOverspent ? '0' : bcsub($amount, $expenses); - $spent = $hasOverspent ? $amount : $expenses; - $overspent = $hasOverspent ? app('steam')->positive($leftInLimit) : '0'; - - $return[$name] = [ - 'left' => $left, - 'overspent' => $overspent, - 'spent' => $spent, - ]; - } - - return $return; - } - - /** - * Returns an array with the following values: - * 'name' => "no budget" in local language - * 'repetition_left' => left in budget repetition (always zero) - * 'repetition_overspent' => spent more than budget repetition? (always zero) - * 'spent' => actually spent in period for budget. - * - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - protected function spentInPeriodWithout(Carbon $start, Carbon $end): string // get data + augment with info - { - // collector - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $types = [TransactionType::WITHDRAWAL]; - $collector->setAllAssetAccounts()->setTypes($types)->setRange($start, $end)->withoutBudget(); - $transactions = $collector->getTransactions(); - $sum = '0'; - /** @var Transaction $entry */ - foreach ($transactions as $entry) { - $sum = bcadd($entry->transaction_amount, $sum); - } - - return $sum; - } } diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index 01fe25a965..19dbce3dd2 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -33,6 +33,7 @@ use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Http\Controllers\AugumentData; +use FireflyIII\Support\Http\Controllers\ChartGeneration; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; @@ -43,7 +44,7 @@ use Log; */ class CategoryController extends Controller { - use DateCalculation, AugumentData; + use DateCalculation, AugumentData, ChartGeneration; /** @var GeneratorInterface Chart generation methods. */ protected $generator; @@ -393,91 +394,4 @@ class CategoryController extends Controller return response()->json($data); } - - /** - * Chart for a specific period (start and end). - * - * - * @param Category $category - * @param Carbon $start - * @param Carbon $end - * - * @return array - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function makePeriodChart(Category $category, Carbon $start, Carbon $end): array // chart helper method. - { - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($category->id); - $cache->addProperty('chart.category.period-chart'); - - - if ($cache->has()) { - //return $cache->get(); // @codeCoverageIgnore - } - - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - $repository = app(CategoryRepositoryInterface::class); - - // chart data - $chartData = [ - [ - 'label' => (string)trans('firefly.spent'), - 'entries' => [], - 'type' => 'bar', - 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red - ], - [ - 'label' => (string)trans('firefly.earned'), - 'entries' => [], - 'type' => 'bar', - 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green - ], - [ - 'label' => (string)trans('firefly.sum'), - 'entries' => [], - 'type' => 'line', - 'fill' => false, - ], - ]; - - $step = $this->calculateStep($start, $end); - - - while ($start <= $end) { - $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $start); - $earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $start); - $sum = bcadd($spent, $earned); - $label = trim(app('navigation')->periodShow($start, $step)); - $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12); - $chartData[1]['entries'][$label] = round($earned, 12); - $chartData[2]['entries'][$label] = round($sum, 12); - - switch ($step) { - default: - case '1D': - $start->addDay(); - break; - case '1W': - $start->addDays(7); - break; - case '1M': - $start->addMonth(); - break; - case '1Y': - $start->addYear(); - break; - } - } - - $data = $this->generator->multiSet($chartData); - $cache->store($data); - - return $data; - } } diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index b13125d2da..fec7cd9219 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -28,9 +28,9 @@ use FireflyIII\Helpers\Report\NetWorthInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Http\Controllers\BasicDataSupport; +use FireflyIII\Support\Http\Controllers\ChartGeneration; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; use Log; @@ -40,7 +40,7 @@ use Log; */ class ReportController extends Controller { - use BasicDataSupport; + use BasicDataSupport, ChartGeneration; /** @var GeneratorInterface Chart generation methods. */ protected $generator; @@ -84,7 +84,7 @@ class ReportController extends Controller // filter accounts on having the preference for being included. /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); - $filtered = $accounts->filter( + $filtered = $accounts->filter( function (Account $account) use ($accountRepository) { $includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth'); $result = null === $includeNetWorth ? true : '1' === $includeNetWorth; @@ -97,7 +97,6 @@ class ReportController extends Controller ); - while ($current < $end) { // get balances by date, grouped by currency. $result = $helper->getNetWorthByCurrency($filtered, $current); @@ -263,67 +262,4 @@ class ReportController extends Controller return response()->json($data); } - - /** - * Collects the incomes and expenses for the given periods, grouped per month. Will cache its results. - * - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return array - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function getChartData(Collection $accounts, Carbon $start, Carbon $end): array // chart helper function - { - $cache = new CacheProperties; - $cache->addProperty('chart.report.get-chart-data'); - $cache->addProperty($start); - $cache->addProperty($accounts); - $cache->addProperty($end); - if ($cache->has()) { - return $cache->get(); // @codeCoverageIgnore - } - - $currentStart = clone $start; - $spentArray = []; - $earnedArray = []; - - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); - - while ($currentStart <= $end) { - $currentEnd = app('navigation')->endOfPeriod($currentStart, '1M'); - $earned = (string)array_sum( - array_map( - function ($item) { - return $item['sum']; - }, - $tasker->getIncomeReport($currentStart, $currentEnd, $accounts) - ) - ); - - $spent = (string)array_sum( - array_map( - function ($item) { - return $item['sum']; - }, - $tasker->getExpenseReport($currentStart, $currentEnd, $accounts) - ) - ); - - $label = $currentStart->format('Y-m') . '-01'; - $spentArray[$label] = bcmul($spent, '-1'); - $earnedArray[$label] = $earned; - $currentStart = app('navigation')->addPeriod($currentStart, '1M', 0); - } - $result = [ - 'spent' => $spentArray, - 'earned' => $earnedArray, - ]; - $cache->store($result); - - return $result; - } } diff --git a/app/Http/Controllers/Report/ExpenseController.php b/app/Http/Controllers/Report/ExpenseController.php index 73de303fe2..d54486624b 100644 --- a/app/Http/Controllers/Report/ExpenseController.php +++ b/app/Http/Controllers/Report/ExpenseController.php @@ -340,294 +340,4 @@ class ExpenseController extends Controller } - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** - * Group by category (earnings). - * - * @param Collection $assets - * @param Collection $opposing - * @param Carbon $start - * @param Carbon $end - * - * @return array - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info - { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getTransactions(); - $sum = []; - // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $categoryName = $transaction->transaction_category_name; - $categoryId = (int)$transaction->transaction_category_id; - // if null, grab from journal: - if (0 === $categoryId) { - $categoryName = $transaction->transaction_journal_category_name; - $categoryId = (int)$transaction->transaction_journal_category_id; - } - if (0 !== $categoryId) { - $categoryName = app('steam')->tryDecrypt($categoryName); - } - - // if not set, set to zero: - if (!isset($sum[$categoryId][$currencyId])) { - $sum[$categoryId] = [ - 'grand_total' => '0', - 'name' => $categoryName, - 'per_currency' => [ - $currencyId => [ - 'sum' => '0', - 'category' => [ - 'id' => $categoryId, - 'name' => $categoryName, - ], - 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, - ], - ], - ], - ]; - } - - // add amount - $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount - ); - $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); - } - - return $sum; - } - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** - * Earned in period for accounts. - * - * @param Collection $assets - * @param Collection $opposing - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info - { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); - $collector->setOpposingAccounts($opposing); - $set = $collector->getTransactions(); - $sum = [ - 'grand_sum' => '0', - 'per_currency' => [], - ]; - // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - - // if not set, set to zero: - if (!isset($sum['per_currency'][$currencyId])) { - $sum['per_currency'][$currencyId] = [ - 'sum' => '0', - 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, - ], - ]; - } - - // add amount - $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); - $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); - } - - return $sum; - } - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** - * Spent by budget. - * - * @param Collection $assets - * @param Collection $opposing - * @param Carbon $start - * @param Carbon $end - * - * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info - { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withBudgetInformation(); - $set = $collector->getTransactions(); - $sum = []; - // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $budgetName = $transaction->transaction_budget_name; - $budgetId = (int)$transaction->transaction_budget_id; - // if null, grab from journal: - if (0 === $budgetId) { - $budgetName = $transaction->transaction_journal_budget_name; - $budgetId = (int)$transaction->transaction_journal_budget_id; - } - if (0 !== $budgetId) { - $budgetName = app('steam')->tryDecrypt($budgetName); - } - - // if not set, set to zero: - if (!isset($sum[$budgetId][$currencyId])) { - $sum[$budgetId] = [ - 'grand_total' => '0', - 'name' => $budgetName, - 'per_currency' => [ - $currencyId => [ - 'sum' => '0', - 'budget' => [ - 'id' => $budgetId, - 'name' => $budgetName, - ], - 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, - ], - ], - ], - ]; - } - - // add amount - $sum[$budgetId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$budgetId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount - ); - $sum[$budgetId]['grand_total'] = bcadd($sum[$budgetId]['grand_total'], $transaction->transaction_amount); - } - - return $sum; - } - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** - * Spent by category. - * - * @param Collection $assets - * @param Collection $opposing - * @param Carbon $start - * @param Carbon $end - * - * @return array - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info - { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getTransactions(); - $sum = []; - // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $categoryName = $transaction->transaction_category_name; - $categoryId = (int)$transaction->transaction_category_id; - // if null, grab from journal: - if (0 === $categoryId) { - $categoryName = $transaction->transaction_journal_category_name; - $categoryId = (int)$transaction->transaction_journal_category_id; - } - if (0 !== $categoryId) { - $categoryName = app('steam')->tryDecrypt($categoryName); - } - - // if not set, set to zero: - if (!isset($sum[$categoryId][$currencyId])) { - $sum[$categoryId] = [ - 'grand_total' => '0', - 'name' => $categoryName, - 'per_currency' => [ - $currencyId => [ - 'sum' => '0', - 'category' => [ - 'id' => $categoryId, - 'name' => $categoryName, - ], - 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, - ], - ], - ], - ]; - } - - // add amount - $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount - ); - $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); - } - - return $sum; - } - - /** @noinspection MoreThanThreeArgumentsInspection */ - /** - * Spent in a period. - * - * @param Collection $assets - * @param Collection $opposing - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info - { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing); - $set = $collector->getTransactions(); - $sum = [ - 'grand_sum' => '0', - 'per_currency' => [], - ]; - // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = (int)$transaction->transaction_currency_id; - - // if not set, set to zero: - if (!isset($sum['per_currency'][$currencyId])) { - $sum['per_currency'][$currencyId] = [ - 'sum' => '0', - 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, - ], - ]; - } - - // add amount - $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); - $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); - } - - return $sum; - } } diff --git a/app/Http/Controllers/Rule/CreateController.php b/app/Http/Controllers/Rule/CreateController.php index 3656586d6f..60c52e102f 100644 --- a/app/Http/Controllers/Rule/CreateController.php +++ b/app/Http/Controllers/Rule/CreateController.php @@ -29,6 +29,8 @@ use FireflyIII\Http\Requests\RuleFormRequest; use FireflyIII\Models\Bill; use FireflyIII\Models\RuleGroup; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; +use FireflyIII\Support\Http\Controllers\AugumentData; +use FireflyIII\Support\Http\Controllers\ModelInformation; use FireflyIII\Support\Http\Controllers\RuleManagement; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -40,7 +42,7 @@ use Throwable; */ class CreateController extends Controller { - use RuleManagement; + use RuleManagement, ModelInformation; /** @var RuleRepositoryInterface Rule repository */ private $ruleRepos; @@ -199,78 +201,4 @@ class CreateController extends Controller return $redirect; } - /** - * Get actions based on a bill. - * - * @param Bill $bill - * - * @return array - */ - protected function getActionsForBill(Bill $bill): array // get info and augument - { - try { - $result = view( - 'rules.partials.action', - [ - 'oldAction' => 'link_to_bill', - 'oldValue' => $bill->name, - 'oldChecked' => false, - 'count' => 1, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::error(sprintf('Throwable was thrown in getActionsForBill(): %s', $e->getMessage())); - Log::error($e->getTraceAsString()); - $result = 'Could not render view. See log files.'; - } - - // @codeCoverageIgnoreEnd - - return [$result]; - } - - /** - * Create fake triggers to match the bill's properties - * - * @param Bill $bill - * - * @return array - */ - protected function getTriggersForBill(Bill $bill): array // get info and augument - { - $result = []; - $triggers = ['currency_is', 'amount_more', 'amount_less', 'description_contains']; - $values = [ - $bill->transactionCurrency()->first()->name, - round((float)$bill->amount_min, 12), - round((float)$bill->amount_max, 12), - $bill->name, - ]; - foreach ($triggers as $index => $trigger) { - try { - $string = view( - 'rules.partials.trigger', - [ - 'oldTrigger' => $trigger, - 'oldValue' => $values[$index], - 'oldChecked' => false, - 'count' => $index + 1, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - - Log::debug(sprintf('Throwable was thrown in getTriggersForBill(): %s', $e->getMessage())); - Log::debug($e->getTraceAsString()); - $string = ''; - // @codeCoverageIgnoreEnd - } - if ('' !== $string) { - $result[] = $string; - } - } - - return $result; - } } diff --git a/app/Http/Controllers/Rule/EditController.php b/app/Http/Controllers/Rule/EditController.php index b4f000bb9d..53c2dd44dd 100644 --- a/app/Http/Controllers/Rule/EditController.php +++ b/app/Http/Controllers/Rule/EditController.php @@ -30,6 +30,8 @@ use FireflyIII\Models\Rule; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleTrigger; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; +use FireflyIII\Support\Http\Controllers\ModelInformation; +use FireflyIII\Support\Http\Controllers\RenderPartialViews; use FireflyIII\Support\Http\Controllers\RuleManagement; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -41,7 +43,7 @@ use Throwable; */ class EditController extends Controller { - use RuleManagement; + use RuleManagement, RenderPartialViews; /** @var RuleRepositoryInterface Rule repository */ private $ruleRepos; @@ -146,81 +148,4 @@ class EditController extends Controller return $redirect; } - - /** - * Get current (from system) rule actions. - * - * @param Rule $rule - * - * @return array - */ - protected function getCurrentActions(Rule $rule): array // get info from object and present. - { - $index = 0; - $actions = []; - $currentActions = $rule->ruleActions()->orderBy('order','ASC')->get(); - /** @var RuleAction $entry */ - foreach ($currentActions as $entry) { - $count = ($index + 1); - try { - $actions[] = view( - 'rules.partials.action', - [ - 'oldAction' => $entry->action_type, - 'oldValue' => $entry->action_value, - 'oldChecked' => $entry->stop_processing, - 'count' => $count, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::debug(sprintf('Throwable was thrown in getCurrentActions(): %s', $e->getMessage())); - Log::error($e->getTraceAsString()); - } - // @codeCoverageIgnoreEnd - ++$index; - } - - return $actions; - } - - /** - * Get current (from DB) rule triggers. - * - * @param Rule $rule - * - * @return array - * - */ - protected function getCurrentTriggers(Rule $rule): array // get info from object and present. - { - $index = 0; - $triggers = []; - $currentTriggers = $rule->ruleTriggers()->orderBy('order','ASC')->get(); - /** @var RuleTrigger $entry */ - foreach ($currentTriggers as $entry) { - if ('user_action' !== $entry->trigger_type) { - $count = ($index + 1); - try { - $triggers[] = view( - 'rules.partials.trigger', - [ - 'oldTrigger' => $entry->trigger_type, - 'oldValue' => $entry->trigger_value, - 'oldChecked' => $entry->stop_processing, - 'count' => $count, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::debug(sprintf('Throwable was thrown in getCurrentTriggers(): %s', $e->getMessage())); - Log::error($e->getTraceAsString()); - } - // @codeCoverageIgnoreEnd - ++$index; - } - } - - return $triggers; - } } diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index 4f30ed6385..c21be8db8c 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -25,10 +25,8 @@ namespace FireflyIII\Http\Controllers\Transaction; use FireflyIII\Events\UpdatedTransactionJournal; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\Http\Controllers\ModelInformation; use Illuminate\Http\Request; @@ -163,6 +161,7 @@ class ConvertController extends Controller if ($errors->count() > 0) { Log::error('Errors while converting: ', $errors->toArray()); + return redirect(route('transactions.convert.index', [strtolower($destinationType->type), $journal->id]))->withErrors($errors)->withInput(); } @@ -174,126 +173,4 @@ class ConvertController extends Controller return redirect(route('transactions.show', [$journal->id])); } - - - /** - * Get the destination account. Is complex. - * - * @param TransactionJournal $journal - * @param TransactionType $destinationType - * @param array $data - * - * @return Account - * - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function getDestinationAccount(TransactionJournal $journal, TransactionType $destinationType, array $data - ): Account // helper for conversion. Get info from obj. - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $sourceAccount = $this->repository->getJournalSourceAccounts($journal)->first(); - $destinationAccount = $this->repository->getJournalDestinationAccounts($journal)->first(); - $sourceType = $journal->transactionType; - $joined = $sourceType->type . '-' . $destinationType->type; - switch ($joined) { - default: - throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore - case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: - // one - $destination = $sourceAccount; - break; - case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: - // two - $destination = $accountRepository->findNull((int)$data['destination_account_asset']); - break; - case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: - case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: - // three and five - if ('' === $data['destination_account_expense'] || null === $data['destination_account_expense']) { - // destination is a cash account. - return $accountRepository->getCashAccount(); - } - $data = [ - 'name' => $data['destination_account_expense'], - 'accountType' => 'expense', - 'account_type_id' => null, - 'virtualBalance' => 0, - 'active' => true, - 'iban' => null, - ]; - $destination = $accountRepository->store($data); - break; - case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: - case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: - // four and six - $destination = $destinationAccount; - break; - } - - return $destination; - } - - - /** - * Get the source account. - * - * @param TransactionJournal $journal - * @param TransactionType $destinationType - * @param array $data - * - * @return Account - * - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function getSourceAccount(TransactionJournal $journal, TransactionType $destinationType, array $data - ): Account // helper for conversion. Get info from obj. - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $sourceAccount = $this->repository->getJournalSourceAccounts($journal)->first(); - $destinationAccount = $this->repository->getJournalDestinationAccounts($journal)->first(); - $sourceType = $journal->transactionType; - $joined = $sourceType->type . '-' . $destinationType->type; - switch ($joined) { - default: - throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore - case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: - case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: - - if ('' === $data['source_account_revenue'] || null === $data['source_account_revenue']) { - // destination is a cash account. - return $accountRepository->getCashAccount(); - } - - $data = [ - 'name' => $data['source_account_revenue'], - 'accountType' => 'revenue', - 'virtualBalance' => 0, - 'active' => true, - 'account_type_id' => null, - 'iban' => null, - ]; - $source = $accountRepository->store($data); - break; - case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: - case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: - $source = $sourceAccount; - break; - case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: - $source = $destinationAccount; - break; - case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: - $source = $accountRepository->findNull((int)$data['source_account_asset']); - break; - } - - return $source; - } } diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index 2f8ecf1b15..4ba15b5503 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -171,42 +171,4 @@ class SplitController extends Controller return redirect($this->getPreviousUri('transactions.edit-split.uri')); } - - /** - * Get info from old input. - * - * @param $array - * @param $old - * - * @return array - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function updateWithPrevious($array, $old): array // update object with new info - { - if (0 === \count($old) || !isset($old['transactions'])) { - return $array; - } - $old = $old['transactions']; - - foreach ($old as $index => $row) { - if (isset($array[$index])) { - /** @noinspection SlowArrayOperationsInLoopInspection */ - $array[$index] = array_merge($array[$index], $row); - continue; - } - // take some info from first transaction, that should at least exist. - $array[$index] = $row; - $array[$index]['currency_id'] = $array[0]['currency_id']; - $array[$index]['currency_code'] = $array[0]['currency_code'] ?? ''; - $array[$index]['currency_symbol'] = $array[0]['currency_symbol'] ?? ''; - $array[$index]['foreign_amount'] = round($array[0]['foreign_destination_amount'] ?? '0', 12); - $array[$index]['foreign_currency_id'] = $array[0]['foreign_currency_id']; - $array[$index]['foreign_currency_code'] = $array[0]['foreign_currency_code']; - $array[$index]['foreign_currency_symbol'] = $array[0]['foreign_currency_symbol']; - } - - return $array; - } } diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index 6776c8532c..2cbf4a19cd 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -24,17 +24,22 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Trait AugumentData @@ -42,6 +47,8 @@ use Illuminate\Support\Collection; */ trait AugumentData { + + /** * Searches for the opposing account. * @@ -69,6 +76,116 @@ trait AugumentData return $combined; } + /** + * Group by category (earnings). + * + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); + $collector->setOpposingAccounts($opposing)->withCategoryInformation(); + $set = $collector->getTransactions(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $categoryName = $transaction->transaction_category_name; + $categoryId = (int)$transaction->transaction_category_id; + // if null, grab from journal: + if (0 === $categoryId) { + $categoryName = $transaction->transaction_journal_category_name; + $categoryId = (int)$transaction->transaction_journal_category_id; + } + if (0 !== $categoryId) { + $categoryName = app('steam')->tryDecrypt($categoryName); + } + + // if not set, set to zero: + if (!isset($sum[$categoryId][$currencyId])) { + $sum[$categoryId] = [ + 'grand_total' => '0', + 'name' => $categoryName, + 'per_currency' => [ + $currencyId => [ + 'sum' => '0', + 'category' => [ + 'id' => $categoryId, + 'name' => $categoryName, + ], + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ], + ], + ]; + } + + // add amount + $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( + $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + ); + $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); + } + + return $sum; + } + + /** + * Earned in period for accounts. + * + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); + $collector->setOpposingAccounts($opposing); + $set = $collector->getTransactions(); + $sum = [ + 'grand_sum' => '0', + 'per_currency' => [], + ]; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + + // if not set, set to zero: + if (!isset($sum['per_currency'][$currencyId])) { + $sum['per_currency'][$currencyId] = [ + 'sum' => '0', + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); + $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); + } + + return $sum; + } + /** * Small helper function for the revenue and expense account charts. * @@ -180,6 +297,39 @@ trait AugumentData return $return; } + /** + * Get the amount of money budgeted in a period. + * + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + protected function getBudgetedInPeriod(Budget $budget, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + + $key = app('navigation')->preferredCarbonFormat($start, $end); + $range = app('navigation')->preferredRangeFormat($start, $end); + $current = clone $start; + $budgeted = []; + while ($current < $end) { + /** @var Carbon $currentStart */ + $currentStart = app('navigation')->startOfPeriod($current, $range); + /** @var Carbon $currentEnd */ + $currentEnd = app('navigation')->endOfPeriod($current, $range); + $budgetLimits = $repository->getBudgetLimits($budget, $currentStart, $currentEnd); + $index = $currentStart->format($key); + $budgeted[$index] = $budgetLimits->sum('amount'); + $currentEnd->addDay(); + $current = clone $currentEnd; + } + + return $budgeted; + } + /** * Get the category names from a set of category ID's. Small helper function for some of the charts. * @@ -204,6 +354,46 @@ trait AugumentData return $return; } + /** + * Get the expenses for a budget in a date range. + * + * @param Collection $limits + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function getExpensesForBudget(Collection $limits, Budget $budget, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + + $return = []; + if (0 === $limits->count()) { + $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); + if (0 !== bccomp($spent, '0')) { + $return[$budget->name]['spent'] = bcmul($spent, '-1'); + $return[$budget->name]['left'] = 0; + $return[$budget->name]['overspent'] = 0; + } + + return $return; + } + + $rows = $this->spentInPeriodMulti($budget, $limits); + foreach ($rows as $name => $row) { + if (0 !== bccomp($row['spent'], '0') || 0 !== bccomp($row['left'], '0')) { + $return[$name] = $row; + } + } + unset($rows); + + return $return; + } + /** * Gets all budget limits for a budget. * @@ -241,6 +431,7 @@ trait AugumentData return $set; } + /** * Helper function that groups expenses. * @@ -333,4 +524,277 @@ trait AugumentData return $grouped; } + + + + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * Spent by budget. + * + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); + $collector->setOpposingAccounts($opposing)->withBudgetInformation(); + $set = $collector->getTransactions(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $budgetName = $transaction->transaction_budget_name; + $budgetId = (int)$transaction->transaction_budget_id; + // if null, grab from journal: + if (0 === $budgetId) { + $budgetName = $transaction->transaction_journal_budget_name; + $budgetId = (int)$transaction->transaction_journal_budget_id; + } + if (0 !== $budgetId) { + $budgetName = app('steam')->tryDecrypt($budgetName); + } + + // if not set, set to zero: + if (!isset($sum[$budgetId][$currencyId])) { + $sum[$budgetId] = [ + 'grand_total' => '0', + 'name' => $budgetName, + 'per_currency' => [ + $currencyId => [ + 'sum' => '0', + 'budget' => [ + 'id' => $budgetId, + 'name' => $budgetName, + ], + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ], + ], + ]; + } + + // add amount + $sum[$budgetId]['per_currency'][$currencyId]['sum'] = bcadd( + $sum[$budgetId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + ); + $sum[$budgetId]['grand_total'] = bcadd($sum[$budgetId]['grand_total'], $transaction->transaction_amount); + } + + return $sum; + } + + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * Spent by category. + * + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); + $collector->setOpposingAccounts($opposing)->withCategoryInformation(); + $set = $collector->getTransactions(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $categoryName = $transaction->transaction_category_name; + $categoryId = (int)$transaction->transaction_category_id; + // if null, grab from journal: + if (0 === $categoryId) { + $categoryName = $transaction->transaction_journal_category_name; + $categoryId = (int)$transaction->transaction_journal_category_id; + } + if (0 !== $categoryId) { + $categoryName = app('steam')->tryDecrypt($categoryName); + } + + // if not set, set to zero: + if (!isset($sum[$categoryId][$currencyId])) { + $sum[$categoryId] = [ + 'grand_total' => '0', + 'name' => $categoryName, + 'per_currency' => [ + $currencyId => [ + 'sum' => '0', + 'category' => [ + 'id' => $categoryId, + 'name' => $categoryName, + ], + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ], + ], + ]; + } + + // add amount + $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( + $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + ); + $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); + } + + return $sum; + } + + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * Spent in a period. + * + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); + $collector->setOpposingAccounts($opposing); + $set = $collector->getTransactions(); + $sum = [ + 'grand_sum' => '0', + 'per_currency' => [], + ]; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = (int)$transaction->transaction_currency_id; + + // if not set, set to zero: + if (!isset($sum['per_currency'][$currencyId])) { + $sum['per_currency'][$currencyId] = [ + 'sum' => '0', + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); + $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); + } + + return $sum; + } + + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * + * Returns an array with the following values: + * 0 => + * 'name' => name of budget + repetition + * 'left' => left in budget repetition (always zero) + * 'overspent' => spent more than budget repetition? (always zero) + * 'spent' => actually spent in period for budget + * 1 => (etc) + * + * @param Budget $budget + * @param Collection $limits + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + */ + protected function spentInPeriodMulti(Budget $budget, Collection $limits): array // get data + augment with info + { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + + $return = []; + $format = (string)trans('config.month_and_day'); + $name = $budget->name; + /** @var BudgetLimit $budgetLimit */ + foreach ($limits as $budgetLimit) { + $expenses = $repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); + $expenses = app('steam')->positive($expenses); + + if ($limits->count() > 1) { + $name = $budget->name . ' ' . trans( + 'firefly.between_dates', + [ + 'start' => $budgetLimit->start_date->formatLocalized($format), + 'end' => $budgetLimit->end_date->formatLocalized($format), + ] + ); + } + $amount = $budgetLimit->amount; + $leftInLimit = bcsub($amount, $expenses); + $hasOverspent = bccomp($leftInLimit, '0') === -1; + $left = $hasOverspent ? '0' : bcsub($amount, $expenses); + $spent = $hasOverspent ? $amount : $expenses; + $overspent = $hasOverspent ? app('steam')->positive($leftInLimit) : '0'; + + $return[$name] = [ + 'left' => $left, + 'overspent' => $overspent, + 'spent' => $spent, + ]; + } + + return $return; + } + + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * Returns an array with the following values: + * 'name' => "no budget" in local language + * 'repetition_left' => left in budget repetition (always zero) + * 'repetition_overspent' => spent more than budget repetition? (always zero) + * 'spent' => actually spent in period for budget. + * + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + protected function spentInPeriodWithout(Carbon $start, Carbon $end): string // get data + augment with info + { + // collector + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $types = [TransactionType::WITHDRAWAL]; + $collector->setAllAssetAccounts()->setTypes($types)->setRange($start, $end)->withoutBudget(); + $transactions = $collector->getTransactions(); + $sum = '0'; + /** @var Transaction $entry */ + foreach ($transactions as $entry) { + $sum = bcadd($entry->transaction_amount, $sum); + } + + return $sum; + } } diff --git a/app/Support/Http/Controllers/BasicDataSupport.php b/app/Support/Http/Controllers/BasicDataSupport.php index cdfefd4d07..6d578e0780 100644 --- a/app/Support/Http/Controllers/BasicDataSupport.php +++ b/app/Support/Http/Controllers/BasicDataSupport.php @@ -90,4 +90,6 @@ trait BasicDataSupport return $result; } + + } diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index 06a9937351..ddf24454f9 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -26,7 +26,11 @@ namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Category; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Account\AccountTaskerInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; @@ -103,4 +107,156 @@ trait ChartGeneration return $data; } + /** + * Collects the incomes and expenses for the given periods, grouped per month. Will cache its results. + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function getChartData(Collection $accounts, Carbon $start, Carbon $end): array // chart helper function + { + $cache = new CacheProperties; + $cache->addProperty('chart.report.get-chart-data'); + $cache->addProperty($start); + $cache->addProperty($accounts); + $cache->addProperty($end); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + $currentStart = clone $start; + $spentArray = []; + $earnedArray = []; + + /** @var AccountTaskerInterface $tasker */ + $tasker = app(AccountTaskerInterface::class); + + while ($currentStart <= $end) { + $currentEnd = app('navigation')->endOfPeriod($currentStart, '1M'); + $earned = (string)array_sum( + array_map( + function ($item) { + return $item['sum']; + }, + $tasker->getIncomeReport($currentStart, $currentEnd, $accounts) + ) + ); + + $spent = (string)array_sum( + array_map( + function ($item) { + return $item['sum']; + }, + $tasker->getExpenseReport($currentStart, $currentEnd, $accounts) + ) + ); + + $label = $currentStart->format('Y-m') . '-01'; + $spentArray[$label] = bcmul($spent, '-1'); + $earnedArray[$label] = $earned; + $currentStart = app('navigation')->addPeriod($currentStart, '1M', 0); + } + $result = [ + 'spent' => $spentArray, + 'earned' => $earnedArray, + ]; + $cache->store($result); + + return $result; + } + + /** + * Chart for a specific period (start and end). + * + * + * @param Category $category + * @param Carbon $start + * @param Carbon $end + * + * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function makePeriodChart(Category $category, Carbon $start, Carbon $end): array // chart helper method. + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($category->id); + $cache->addProperty('chart.category.period-chart'); + + + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $repository = app(CategoryRepositoryInterface::class); + /** @var GeneratorInterface $generator */ + $generator = app(GeneratorInterface::class); + + // chart data + $chartData = [ + [ + 'label' => (string)trans('firefly.spent'), + 'entries' => [], + 'type' => 'bar', + 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red + ], + [ + 'label' => (string)trans('firefly.earned'), + 'entries' => [], + 'type' => 'bar', + 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green + ], + [ + 'label' => (string)trans('firefly.sum'), + 'entries' => [], + 'type' => 'line', + 'fill' => false, + ], + ]; + + $step = $this->calculateStep($start, $end); + + + while ($start <= $end) { + $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $start); + $earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $start); + $sum = bcadd($spent, $earned); + $label = trim(app('navigation')->periodShow($start, $step)); + $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12); + $chartData[1]['entries'][$label] = round($earned, 12); + $chartData[2]['entries'][$label] = round($sum, 12); + + switch ($step) { + default: + case '1D': + $start->addDay(); + break; + case '1W': + $start->addDays(7); + break; + case '1M': + $start->addMonth(); + break; + case '1Y': + $start->addYear(); + break; + } + } + + $data = $generator->multiSet($chartData); + $cache->store($data); + + return $data; + } + } \ No newline at end of file diff --git a/app/Support/Http/Controllers/ModelInformation.php b/app/Support/Http/Controllers/ModelInformation.php index 471d66dcd6..658c2b8da7 100644 --- a/app/Support/Http/Controllers/ModelInformation.php +++ b/app/Support/Http/Controllers/ModelInformation.php @@ -23,9 +23,15 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\Bill; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Log; +use Throwable; /** * Trait ModelInformation @@ -33,6 +39,205 @@ use FireflyIII\Repositories\Journal\JournalRepositoryInterface; */ trait ModelInformation { + /** + * Get actions based on a bill. + * + * @param Bill $bill + * + * @return array + */ + protected function getActionsForBill(Bill $bill): array // get info and augument + { + try { + $result = view( + 'rules.partials.action', + [ + 'oldAction' => 'link_to_bill', + 'oldValue' => $bill->name, + 'oldChecked' => false, + 'count' => 1, + ] + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::error(sprintf('Throwable was thrown in getActionsForBill(): %s', $e->getMessage())); + Log::error($e->getTraceAsString()); + $result = 'Could not render view. See log files.'; + } + + // @codeCoverageIgnoreEnd + + return [$result]; + } + + /** + * Get the destination account. Is complex. + * + * @param TransactionJournal $journal + * @param TransactionType $destinationType + * @param array $data + * + * @return Account + * + * @throws FireflyException + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function getDestinationAccount(TransactionJournal $journal, TransactionType $destinationType, array $data + ): Account // helper for conversion. Get info from obj. + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + /** @var JournalRepositoryInterface $journalRepos */ + $journalRepos = app(JournalRepositoryInterface::class); + $sourceAccount = $journalRepos->getJournalSourceAccounts($journal)->first(); + $destinationAccount = $journalRepos->getJournalDestinationAccounts($journal)->first(); + $sourceType = $journal->transactionType; + $joined = $sourceType->type . '-' . $destinationType->type; + switch ($joined) { + default: + throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: + // one + $destination = $sourceAccount; + break; + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: + // two + $destination = $accountRepository->findNull((int)$data['destination_account_asset']); + break; + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: + // three and five + if ('' === $data['destination_account_expense'] || null === $data['destination_account_expense']) { + // destination is a cash account. + return $accountRepository->getCashAccount(); + } + $data = [ + 'name' => $data['destination_account_expense'], + 'accountType' => 'expense', + 'account_type_id' => null, + 'virtualBalance' => 0, + 'active' => true, + 'iban' => null, + ]; + $destination = $accountRepository->store($data); + break; + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: + // four and six + $destination = $destinationAccount; + break; + } + + return $destination; + } + + /** + * Get the source account. + * + * @param TransactionJournal $journal + * @param TransactionType $destinationType + * @param array $data + * + * @return Account + * + * @throws FireflyException + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function getSourceAccount(TransactionJournal $journal, TransactionType $destinationType, array $data + ): Account // helper for conversion. Get info from obj. + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + /** @var JournalRepositoryInterface $journalRepos */ + $journalRepos = app(JournalRepositoryInterface::class); + $sourceAccount = $journalRepos->getJournalSourceAccounts($journal)->first(); + $destinationAccount = $journalRepos->getJournalDestinationAccounts($journal)->first(); + $sourceType = $journal->transactionType; + $joined = $sourceType->type . '-' . $destinationType->type; + switch ($joined) { + default: + throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: + + if ('' === $data['source_account_revenue'] || null === $data['source_account_revenue']) { + // destination is a cash account. + return $accountRepository->getCashAccount(); + } + + $data = [ + 'name' => $data['source_account_revenue'], + 'accountType' => 'revenue', + 'virtualBalance' => 0, + 'active' => true, + 'account_type_id' => null, + 'iban' => null, + ]; + $source = $accountRepository->store($data); + break; + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: + $source = $sourceAccount; + break; + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: + $source = $destinationAccount; + break; + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: + $source = $accountRepository->findNull((int)$data['source_account_asset']); + break; + } + + return $source; + } + + /** + * Create fake triggers to match the bill's properties + * + * @param Bill $bill + * + * @return array + */ + protected function getTriggersForBill(Bill $bill): array // get info and augument + { + $result = []; + $triggers = ['currency_is', 'amount_more', 'amount_less', 'description_contains']; + $values = [ + $bill->transactionCurrency()->first()->name, + round((float)$bill->amount_min, 12), + round((float)$bill->amount_max, 12), + $bill->name, + ]; + foreach ($triggers as $index => $trigger) { + try { + $string = view( + 'rules.partials.trigger', + [ + 'oldTrigger' => $trigger, + 'oldValue' => $values[$index], + 'oldChecked' => false, + 'count' => $index + 1, + ] + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + + Log::debug(sprintf('Throwable was thrown in getTriggersForBill(): %s', $e->getMessage())); + Log::debug($e->getTraceAsString()); + $string = ''; + // @codeCoverageIgnoreEnd + } + if ('' !== $string) { + $result[] = $string; + } + } + + return $result; + } + /** * Is transaction opening balance? * diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php index 6d1203c429..384116ece4 100644 --- a/app/Support/Http/Controllers/RenderPartialViews.php +++ b/app/Support/Http/Controllers/RenderPartialViews.php @@ -23,11 +23,13 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; - use FireflyIII\Helpers\Collection\BalanceLine; use FireflyIII\Helpers\Report\PopupReportInterface; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; +use FireflyIII\Models\Rule; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\RuleTrigger; use FireflyIII\Models\Tag; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; @@ -43,6 +45,7 @@ use Throwable; */ trait RenderPartialViews { + /** * Get options for account report. * @@ -103,7 +106,7 @@ trait RenderPartialViews break; case BalanceLine::ROLE_DEFAULTROLE === $role && null === $budget && null !== $account: // normal row without a budget: - $budget = new Budget; + $budget = new Budget; $journals = $popupHelper->balanceForNoBudget($account, $attributes); $budget->name = (string)trans('firefly.no_budget'); break; @@ -254,6 +257,85 @@ trait RenderPartialViews return $view; } + /** + * Get current (from system) rule actions. + * + * @param Rule $rule + * + * @return array + */ + protected function getCurrentActions(Rule $rule): array // get info from object and present. + { + $index = 0; + $actions = []; + // todo must be repos + $currentActions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); + /** @var RuleAction $entry */ + foreach ($currentActions as $entry) { + $count = ($index + 1); + try { + $actions[] = view( + 'rules.partials.action', + [ + 'oldAction' => $entry->action_type, + 'oldValue' => $entry->action_value, + 'oldChecked' => $entry->stop_processing, + 'count' => $count, + ] + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Throwable was thrown in getCurrentActions(): %s', $e->getMessage())); + Log::error($e->getTraceAsString()); + } + // @codeCoverageIgnoreEnd + ++$index; + } + + return $actions; + } + + /** + * Get current (from DB) rule triggers. + * + * @param Rule $rule + * + * @return array + * + */ + protected function getCurrentTriggers(Rule $rule): array // get info from object and present. + { + $index = 0; + $triggers = []; + // todo must be repos + $currentTriggers = $rule->ruleTriggers()->orderBy('order', 'ASC')->get(); + /** @var RuleTrigger $entry */ + foreach ($currentTriggers as $entry) { + if ('user_action' !== $entry->trigger_type) { + $count = ($index + 1); + try { + $triggers[] = view( + 'rules.partials.trigger', + [ + 'oldTrigger' => $entry->trigger_type, + 'oldValue' => $entry->trigger_value, + 'oldChecked' => $entry->stop_processing, + 'count' => $count, + ] + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Throwable was thrown in getCurrentTriggers(): %s', $e->getMessage())); + Log::error($e->getTraceAsString()); + } + // @codeCoverageIgnoreEnd + ++$index; + } + } + + return $triggers; + } + /** * Returns all the incomes that went to the given asset account. * diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 89117b3da3..b3c0d29cb1 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -54,6 +54,43 @@ use Symfony\Component\HttpFoundation\ParameterBag; */ trait RequestInformation { + /** + * Get info from old input. + * + * @param $array + * @param $old + * + * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function updateWithPrevious($array, $old): array // update object with new info + { + if (0 === \count($old) || !isset($old['transactions'])) { + return $array; + } + $old = $old['transactions']; + + foreach ($old as $index => $row) { + if (isset($array[$index])) { + /** @noinspection SlowArrayOperationsInLoopInspection */ + $array[$index] = array_merge($array[$index], $row); + continue; + } + // take some info from first transaction, that should at least exist. + $array[$index] = $row; + $array[$index]['currency_id'] = $array[0]['currency_id']; + $array[$index]['currency_code'] = $array[0]['currency_code'] ?? ''; + $array[$index]['currency_symbol'] = $array[0]['currency_symbol'] ?? ''; + $array[$index]['foreign_amount'] = round($array[0]['foreign_destination_amount'] ?? '0', 12); + $array[$index]['foreign_currency_id'] = $array[0]['foreign_currency_id']; + $array[$index]['foreign_currency_code'] = $array[0]['foreign_currency_code']; + $array[$index]['foreign_currency_symbol'] = $array[0]['foreign_currency_symbol']; + } + + return $array; + } /** * Create data-array from a journal. @@ -66,8 +103,10 @@ trait RequestInformation */ protected function arrayFromJournal(Request $request, TransactionJournal $journal): array // convert user input. { - $sourceAccounts = $this->repository->getJournalSourceAccounts($journal); - $destinationAccounts = $this->repository->getJournalDestinationAccounts($journal); + /** @var JournalRepositoryInterface $repository */ + $repository = app(JournalRepositoryInterface::class); + $sourceAccounts = $repository->getJournalSourceAccounts($journal); + $destinationAccounts = $repository->getJournalDestinationAccounts($journal); $array = [ 'journal_description' => $request->old('journal_description', $journal->description), 'journal_amount' => '0', @@ -82,14 +121,14 @@ trait RequestInformation 'tags' => implode(',', $journal->tags->pluck('tag')->toArray()), // all custom fields: - 'interest_date' => $request->old('interest_date', $this->repository->getMetaField($journal, 'interest_date')), - 'book_date' => $request->old('book_date', $this->repository->getMetaField($journal, 'book_date')), - 'process_date' => $request->old('process_date', $this->repository->getMetaField($journal, 'process_date')), - 'due_date' => $request->old('due_date', $this->repository->getMetaField($journal, 'due_date')), - 'payment_date' => $request->old('payment_date', $this->repository->getMetaField($journal, 'payment_date')), - 'invoice_date' => $request->old('invoice_date', $this->repository->getMetaField($journal, 'invoice_date')), - 'internal_reference' => $request->old('internal_reference', $this->repository->getMetaField($journal, 'internal_reference')), - 'notes' => $request->old('notes', $this->repository->getNoteText($journal)), + 'interest_date' => $request->old('interest_date', $repository->getMetaField($journal, 'interest_date')), + 'book_date' => $request->old('book_date', $repository->getMetaField($journal, 'book_date')), + 'process_date' => $request->old('process_date', $repository->getMetaField($journal, 'process_date')), + 'due_date' => $request->old('due_date', $repository->getMetaField($journal, 'due_date')), + 'payment_date' => $request->old('payment_date', $repository->getMetaField($journal, 'payment_date')), + 'invoice_date' => $request->old('invoice_date', $repository->getMetaField($journal, 'invoice_date')), + 'internal_reference' => $request->old('internal_reference', $repository->getMetaField($journal, 'internal_reference')), + 'notes' => $request->old('notes', $repository->getNoteText($journal)), // transactions. 'transactions' => $this->getTransactionDataFromJournal($journal),