. */ declare(strict_types=1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use ExpandedForm; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\AccountFormRequest; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Note; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Preferences; use Steam; use View; /** * Class AccountController. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountController extends Controller { /** @var CurrencyRepositoryInterface */ private $currencyRepos; /** @var JournalRepositoryInterface */ private $journalRepos; /** @var AccountRepositoryInterface */ private $repository; /** * */ public function __construct() { parent::__construct(); // translations: $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); app('view')->share('title', trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); $this->currencyRepos = app(CurrencyRepositoryInterface::class); $this->journalRepos = app(JournalRepositoryInterface::class); return $next($request); } ); } /** * @param Request $request * @param string $what * * @return View */ public function create(Request $request, string $what = 'asset') { $defaultCurrency = app('amount')->getDefaultCurrency(); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); $subTitle = trans('firefly.make_new_' . $what . '_account'); $roles = []; foreach (config('firefly.accountRoles') as $role) { $roles[$role] = (string)trans('firefly.account_role_' . $role); } // pre fill some data $request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id]); // put previous url in session if not redirect from store (not "create another"). if (true !== session('accounts.create.fromStore')) { $this->rememberPreviousUri('accounts.create.uri'); } $request->session()->forget('accounts.create.fromStore'); return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'roles')); } /** * @param Account $account * * @return View */ public function delete(Account $account) { $typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type); $subTitle = trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]); $accountList = ExpandedForm::makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type])); unset($accountList[$account->id]); // put previous url in session $this->rememberPreviousUri('accounts.delete.uri'); return view('accounts.delete', compact('account', 'subTitle', 'accountList')); } /** * @param Request $request * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function destroy(Request $request, Account $account) { $type = $account->accountType->type; $typeName = config('firefly.shortNamesByFullName.' . $type); $name = $account->name; $moveTo = $this->repository->findNull((int)$request->get('move_account_before_delete')); $this->repository->destroy($account, $moveTo); $request->session()->flash('success', (string)trans('firefly.' . $typeName . '_deleted', ['name' => $name])); Preferences::mark(); return redirect($this->getPreviousUri('accounts.delete.uri')); } /** * Edit an account. * * @param Request $request * @param Account $account * * @param AccountRepositoryInterface $repository * * @return View * * @SuppressWarnings(PHPMD.CyclomaticComplexity) // long and complex but not that excessively so. * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * */ public function edit(Request $request, Account $account, AccountRepositoryInterface $repository) { $what = config('firefly.shortNamesByFullName')[$account->accountType->type]; $subTitle = trans('firefly.edit_' . $what . '_account', ['name' => $account->name]); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); $roles = []; foreach (config('firefly.accountRoles') as $role) { $roles[$role] = (string)trans('firefly.account_role_' . $role); } // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('accounts.edit.fromUpdate')) { $this->rememberPreviousUri('accounts.edit.uri'); } $request->session()->forget('accounts.edit.fromUpdate'); // pre fill some useful values. // the opening balance is tricky: $openingBalanceAmount = (string)$repository->getOpeningBalanceAmount($account); $openingBalanceDate = $repository->getOpeningBalanceDate($account); $default = app('amount')->getDefaultCurrency(); $currency = $this->currencyRepos->findNull((int)$repository->getMetaValue($account, 'currency_id')); if (null === $currency) { $currency = $default; } $preFilled = [ 'accountNumber' => $repository->getMetaValue($account, 'accountNumber'), 'accountRole' => $repository->getMetaValue($account, 'accountRole'), 'ccType' => $repository->getMetaValue($account, 'ccType'), 'ccMonthlyPaymentDate' => $repository->getMetaValue($account, 'ccMonthlyPaymentDate'), 'BIC' => $repository->getMetaValue($account, 'BIC'), 'openingBalanceDate' => $openingBalanceDate, 'openingBalance' => $openingBalanceAmount, 'virtualBalance' => $account->virtual_balance, 'currency_id' => $currency->id, 'notes' => '', 'active' => $account->active, ]; /** @var Note $note */ $note = $this->repository->getNote($account); if (null !== $note) { $preFilled['notes'] = $note->text; } $request->session()->flash('preFilled', $preFilled); return view('accounts.edit', compact('account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles', 'preFilled')); } /** * @param Request $request * @param string $what * * @return View */ public function index(Request $request, string $what) { $what = $what ?? 'asset'; $subTitle = trans('firefly.' . $what . '_accounts'); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); $types = config('firefly.accountTypesByIdentifier.' . $what); $collection = $this->repository->getAccountsByType($types); $total = $collection->count(); $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); $pageSize = (int)Preferences::get('listPageSize', 50)->data; $accounts = $collection->slice(($page - 1) * $pageSize, $pageSize); unset($collection); /** @var Carbon $start */ $start = clone session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ $end = clone session('end', Carbon::now()->endOfMonth()); $start->subDay(); $ids = $accounts->pluck('id')->toArray(); $startBalances = Steam::balancesByAccounts($accounts, $start); $endBalances = Steam::balancesByAccounts($accounts, $end); $activities = Steam::getLastActivities($ids); $accounts->each( function (Account $account) use ($activities, $startBalances, $endBalances) { $account->lastActivityDate = $this->isInArray($activities, $account->id); $account->startBalance = $this->isInArray($startBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id); $account->difference = bcsub($account->endBalance, $account->startBalance); } ); // make paginator: $accounts = new LengthAwarePaginator($accounts, $total, $pageSize, $page); $accounts->setPath(route('accounts.index', [$what])); return view('accounts.index', compact('what', 'subTitleIcon', 'subTitle', 'page', 'accounts')); } /** * Show an account. * * @param Request $request * @param Account $account * @param Carbon|null $start * @param Carbon|null $end * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View * * @throws FireflyException * */ public function show(Request $request, Account $account, Carbon $start = null, Carbon $end = null) { if (AccountType::INITIAL_BALANCE === $account->accountType->type) { return $this->redirectToOriginalAccount($account); } if (null === $start) { $start = session('start'); } if (null === $end) { $end = session('end'); } if ($end < $start) { throw new FireflyException('End is after start!'); // @codeCoverageIgnore } $what = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type)); // used for menu $today = new Carbon; $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $account->accountType->type)); $page = (int)$request->get('page'); $pageSize = (int)Preferences::get('listPageSize', 50)->data; $currencyId = (int)$this->repository->getMetaValue($account, 'currency_id'); $currency = $this->currencyRepos->findNull($currencyId); if (0 === $currencyId) { $currency = app('amount')->getDefaultCurrency(); // @codeCoverageIgnore } $fStart = $start->formatLocalized($this->monthAndDayFormat); $fEnd = $end->formatLocalized($this->monthAndDayFormat); $subTitle = trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]); $chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); $periods = $this->getPeriodOverview($account, $end); $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); $collector->setRange($start, $end); $transactions = $collector->getPaginatedJournals(); $transactions->setPath(route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')])); $showAll = false; return view( 'accounts.show', compact('account', 'showAll', 'what', 'currency', 'today', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end', 'chartUri') ); } /** * Show an account. * * @param Request $request * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View * * @throws FireflyException * */ public function showAll(Request $request, Account $account) { if (AccountType::INITIAL_BALANCE === $account->accountType->type) { return $this->redirectToOriginalAccount($account); // @codeCoverageIgnore } $end = new Carbon; $today = new Carbon; $start = $this->repository->oldestJournalDate($account); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); $page = (int)$request->get('page'); $pageSize = (int)Preferences::get('listPageSize', 50)->data; $currencyId = (int)$this->repository->getMetaValue($account, 'currency_id'); $currency = $this->currencyRepos->findNull($currencyId); if (0 === $currencyId) { $currency = app('amount')->getDefaultCurrency(); // @codeCoverageIgnore } $subTitle = trans('firefly.all_journals_for_account', ['name' => $account->name]); $periods = new Collection; $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); $transactions = $collector->getPaginatedJournals(); $transactions->setPath(route('accounts.show.all', [$account->id])); $chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); $showAll = true; return view( 'accounts.show', compact('account', 'showAll', 'currency', 'today', 'chartUri', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end') ); } /** * @param AccountFormRequest $request * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function store(AccountFormRequest $request) { $data = $request->getAccountData(); $account = $this->repository->store($data); $request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name])); Preferences::mark(); // update preferences if necessary: $frontPage = Preferences::get('frontPageAccounts', [])->data; if (AccountType::ASSET === $account->accountType->type && \count($frontPage) > 0) { // @codeCoverageIgnoreStart $frontPage[] = $account->id; Preferences::set('frontPageAccounts', $frontPage); // @codeCoverageIgnoreEnd } if (1 === (int)$request->get('create_another')) { // set value so create routine will not overwrite URL: $request->session()->put('accounts.create.fromStore', true); return redirect(route('accounts.create', [$request->input('what')]))->withInput(); } // redirect to previous URL. return redirect($this->getPreviousUri('accounts.create.uri')); } /** * @param AccountFormRequest $request * @param Account $account * * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function update(AccountFormRequest $request, Account $account) { $data = $request->getAccountData(); $this->repository->update($account, $data); $request->session()->flash('success', (string)trans('firefly.updated_account', ['name' => $account->name])); Preferences::mark(); if (1 === (int)$request->get('return_to_edit')) { // set value so edit routine will not overwrite URL: $request->session()->put('accounts.edit.fromUpdate', true); return redirect(route('accounts.edit', [$account->id]))->withInput(['return_to_edit' => 1]); } // redirect to previous URL. return redirect($this->getPreviousUri('accounts.edit.uri')); } /** * @param array $array * @param int $entryId * * @return null|mixed */ protected function isInArray(array $array, int $entryId) { if (isset($array[$entryId])) { return $array[$entryId]; } return '0'; } /** * This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range) * and for each period, the amount of money spent and earned. This is a complex operation which is cached for * performance reasons. * * @param Account $account the account involved * * @param Carbon|null $date * * @return Collection * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function getPeriodOverview(Account $account, ?Carbon $date): Collection { $range = Preferences::get('viewRange', '1M')->data; $start = $this->repository->oldestJournalDate($account); $end = $date ?? new Carbon; if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } // properties for cache $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('account-show-period-entries'); $cache->addProperty($account->id); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; // loop dates foreach ($dates as $currentDate) { // try a collector for income: /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::DEPOSIT]) ->withOpposingAccount(); $earned = (string)$collector->getJournals()->sum('transaction_amount'); // try a collector for expenses: /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::WITHDRAWAL]) ->withOpposingAccount(); $spent = (string)$collector->getJournals()->sum('transaction_amount'); $dateName = app('navigation')->periodShow($currentDate['start'], $currentDate['period']); $entries->push( [ 'name' => $dateName, 'spent' => $spent, 'earned' => $earned, 'start' => $currentDate['start']->format('Y-m-d'), 'end' => $currentDate['end']->format('Y-m-d'), ] ); } $cache->store($entries); return $entries; } /** * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * * @throws FireflyException */ private function redirectToOriginalAccount(Account $account) { /** @var Transaction $transaction */ $transaction = $account->transactions()->first(); if (null === $transaction) { throw new FireflyException('Expected a transaction. This account has none. BEEP, error.'); } $journal = $transaction->transactionJournal; /** @var Transaction $opposingTransaction */ $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first(); if (null === $opposingTransaction) { throw new FireflyException('Expected an opposing transaction. This account has none. BEEP, error.'); // @codeCoverageIgnore } return redirect(route('accounts.show', [$opposingTransaction->account_id])); } }