From 9a3cd277008ba39fe8d6858d8382c93ab78bedb2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 21 Oct 2016 19:06:22 +0200 Subject: [PATCH] Many updates to get split transactions and normal transactions working side by side. --- app/Export/Processor.php | 7 +- app/Http/Controllers/JsonController.php | 8 +- .../Controllers/TransactionController.php | 178 +++++---- app/Http/Requests/JournalFormRequest.php | 174 ++++++--- ...ExecuteRuleGroupOnExistingTransactions.php | 8 +- app/Models/TransactionJournal.php | 3 + app/Providers/FireflyServiceProvider.php | 6 + .../Journal/JournalRepository.php | 349 +++++++++--------- .../Journal/JournalRepositoryInterface.php | 32 -- app/Repositories/Journal/JournalTasker.php | 89 ++++- .../Journal/JournalTaskerInterface.php | 31 ++ app/Rules/TransactionMatcher.php | 14 +- app/Support/ExpandedForm.php | 2 + app/Support/ExpandedMultiForm.php | 188 ++++++++++ app/Support/Facades/ExpandedMultiForm.php | 35 ++ app/Validation/FireflyValidator.php | 22 ++ config/app.php | 93 ++--- config/twigbridge.php | 5 + public/js/ff/firefly.js | 44 +++ resources/views/form/date.twig | 7 +- resources/views/form/multi/amount.twig | 30 ++ resources/views/form/multi/feedback.twig | 4 + resources/views/form/multi/select.twig | 10 + resources/views/form/multi/text.twig | 9 + resources/views/transactions/create.twig | 47 +-- 25 files changed, 960 insertions(+), 435 deletions(-) create mode 100644 app/Support/ExpandedMultiForm.php create mode 100644 app/Support/Facades/ExpandedMultiForm.php create mode 100644 resources/views/form/multi/amount.twig create mode 100644 resources/views/form/multi/feedback.twig create mode 100644 resources/views/form/multi/select.twig create mode 100644 resources/views/form/multi/text.twig diff --git a/app/Export/Processor.php b/app/Export/Processor.php index f5c69be451..bfb7076006 100644 --- a/app/Export/Processor.php +++ b/app/Export/Processor.php @@ -20,6 +20,7 @@ use FireflyIII\Export\Entry\Entry; use FireflyIII\Models\ExportJob; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalTaskerInterface; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Collection; use Storage; @@ -95,9 +96,9 @@ class Processor */ public function collectJournals(): bool { - /** @var JournalRepositoryInterface $repository */ - $repository = app(JournalRepositoryInterface::class); - $this->journals = $repository->getJournalsInRange($this->accounts, $this->settings['startDate'], $this->settings['endDate']); + /** @var JournalTaskerInterface $tasker */ + $tasker = app(JournalTaskerInterface::class); + $this->journals = $tasker->getJournalsInRange($this->accounts, $this->settings['startDate'], $this->settings['endDate']); return true; } diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index 65465a8e9f..5eea913966 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -20,7 +20,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface as CRI; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; use Input; @@ -270,17 +270,17 @@ class JsonController extends Controller } /** - * @param JournalRepositoryInterface $repository + * @param JournalTaskerInterface $tasker * @param $what * * @return \Symfony\Component\HttpFoundation\Response */ - public function transactionJournals(JournalRepositoryInterface $repository, $what) + public function transactionJournals(JournalTaskerInterface $tasker, $what) { $descriptions = []; $type = config('firefly.transactionTypesByWhat.' . $what); $types = [$type]; - $journals = $repository->getJournals($types, 1, 50); + $journals = $tasker->getJournals($types, 1, 50); foreach ($journals as $j) { $descriptions[] = $j->description; } diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index dc00364967..f2b30018df 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -23,9 +23,12 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Http\Request; +use Log; use Preferences; use Response; use Session; @@ -40,6 +43,14 @@ use View; */ class TransactionController extends Controller { + /** @var AccountRepositoryInterface */ + private $accounts; + private $attachments; + /** @var BudgetRepositoryInterface */ + private $budgets; + /** @var PiggyBankRepositoryInterface */ + private $piggyBanks; + /** * */ @@ -52,8 +63,21 @@ class TransactionController extends Controller $maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize')); $maxPostSize = Steam::phpBytes(ini_get('post_max_size')); $uploadSize = min($maxFileSize, $maxPostSize); - View::share('uploadSize', $uploadSize); + + // some useful repositories: + $this->middleware( + function ($request, $next) { + $this->accounts = app(AccountRepositoryInterface::class); + $this->budgets = app(BudgetRepositoryInterface::class); + $this->piggyBanks = app(PiggyBankRepositoryInterface::class); + $this->attachments = app(AttachmentHelperInterface::class); + + return $next($request); + } + ); + + } /** @@ -63,20 +87,16 @@ class TransactionController extends Controller */ public function create(string $what = TransactionType::DEPOSIT) { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $budgetRepository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); - $piggyRepository = app('FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface'); - $what = strtolower($what); - $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); - $assetAccounts = ExpandedForm::makeSelectList($accountRepository->getActiveAccountsByType(['Default account', 'Asset account'])); - $budgets = ExpandedForm::makeSelectListWithEmpty($budgetRepository->getActiveBudgets()); - $piggyBanks = $piggyRepository->getPiggyBanksWithAmount(); - $piggies = ExpandedForm::makeSelectListWithEmpty($piggyBanks); - $preFilled = Session::has('preFilled') ? session('preFilled') : []; - $subTitle = trans('form.add_new_' . $what); - $subTitleIcon = 'fa-plus'; - $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; + $what = strtolower($what); + $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); + $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getActiveAccountsByType(['Default account', 'Asset account'])); + $budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); + $piggyBanks = $this->piggyBanks->getPiggyBanksWithAmount(); + $piggies = ExpandedForm::makeSelectListWithEmpty($piggyBanks); + $preFilled = Session::has('preFilled') ? session('preFilled') : []; + $subTitle = trans('form.add_new_' . $what); + $subTitleIcon = 'fa-plus'; + $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; Session::put('preFilled', $preFilled); @@ -91,7 +111,6 @@ class TransactionController extends Controller asort($piggies); - return view('transactions.create', compact('assetAccounts', 'subTitleIcon', 'uploadSize', 'budgets', 'what', 'piggies', 'subTitle', 'optionalFields')); } @@ -145,19 +164,12 @@ class TransactionController extends Controller { $count = $journal->transactions()->count(); if ($count > 2) { - return redirect(route('split.journal.edit', [$journal->id])); + return redirect(route('journal.edit-split', [$journal->id])); } - // code to get list data: - $budgetRepository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); - $piggyRepository = app('FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface'); - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - - $assetAccounts = ExpandedForm::makeSelectList($repository->getAccountsByType(['Default account', 'Asset account'])); - $budgetList = ExpandedForm::makeSelectListWithEmpty($budgetRepository->getActiveBudgets()); - $piggyBankList = ExpandedForm::makeSelectListWithEmpty($piggyRepository->getPiggyBanks()); + $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType(['Default account', 'Asset account'])); + $budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); + $piggyBankList = ExpandedForm::makeSelectListWithEmpty($this->piggyBanks->getPiggyBanks()); // view related code $subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]); @@ -216,20 +228,20 @@ class TransactionController extends Controller } /** - * @param Request $request - * @param JournalRepositoryInterface $repository - * @param string $what + * @param Request $request + * @param JournalTaskerInterface $tasker + * @param string $what * * @return View */ - public function index(Request $request, JournalRepositoryInterface $repository, string $what) + public function index(Request $request, JournalTaskerInterface $tasker, string $what) { $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitleIcon = config('firefly.transactionIconsByWhat.' . $what); $types = config('firefly.transactionTypesByWhat.' . $what); $subTitle = trans('firefly.title_' . $what); $page = intval($request->get('page')); - $journals = $repository->getJournals($types, $page, $pageSize); + $journals = $tasker->getJournals($types, $page, $pageSize); $journals->setPath('transactions/' . $what); @@ -266,14 +278,14 @@ class TransactionController extends Controller } /** - * @param TransactionJournal $journal - * @param JournalRepositoryInterface $repository + * @param TransactionJournal $journal + * @param JournalTaskerInterface $tasker * * @return View */ - public function show(TransactionJournal $journal, JournalRepositoryInterface $repository, JournalTaskerInterface $tasker) + public function show(TransactionJournal $journal, JournalTaskerInterface $tasker) { - $events = $repository->getPiggyBankEvents($journal); + $events = $tasker->getPiggyBankEvents($journal); $transactions = $tasker->getTransactionsOverview($journal); $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; @@ -291,63 +303,47 @@ class TransactionController extends Controller */ public function store(JournalFormRequest $request, JournalRepositoryInterface $repository) { - $att = app('FireflyIII\Helpers\Attachments\AttachmentHelperInterface'); - $doSplit = intval($request->get('split_journal')) === 1; - $journalData = $request->getJournalData(); + $doSplit = intval($request->get('split_journal')) === 1; + $createAnother = intval($request->get('create_another')) === 1; + $data = $request->getJournalData(); + $journal = $repository->store($data); + if (is_null($journal->id)) { + // error! + Log::error('Could not store transaction journal: ', $journal->getErrors()->toArray()); + Session::flash('error', $journal->getErrors()->first()); + + return redirect(route('transactions.create', [$request->input('what')]))->withInput(); + } + + $this->attachments->saveAttachmentsForModel($journal); // store the journal only, flash the rest. - if ($doSplit) { - $journal = $repository->storeJournal($journalData); - $journal->completed = false; - $journal->save(); - - // store attachments: - $att->saveAttachmentsForModel($journal); - - // flash errors - if (count($att->getErrors()->get('attachments')) > 0) { - Session::flash('error', $att->getErrors()->get('attachments')); - } - // flash messages - if (count($att->getMessages()->get('attachments')) > 0) { - Session::flash('info', $att->getMessages()->get('attachments')); - } - - Session::put('journal-data', $journalData); - - return redirect(route('split.journal.create', [$journal->id])); - } - - - // if not withdrawal, unset budgetid. - if ($journalData['what'] != strtolower(TransactionType::WITHDRAWAL)) { - $journalData['budget_id'] = 0; - } - - $journal = $repository->store($journalData); - $att->saveAttachmentsForModel($journal); - - // flash errors - if (count($att->getErrors()->get('attachments')) > 0) { - Session::flash('error', $att->getErrors()->get('attachments')); + if (count($this->attachments->getErrors()->get('attachments')) > 0) { + Session::flash('error', $this->attachments->getErrors()->get('attachments')); } // flash messages - if (count($att->getMessages()->get('attachments')) > 0) { - Session::flash('info', $att->getMessages()->get('attachments')); + if (count($this->attachments->getMessages()->get('attachments')) > 0) { + Session::flash('info', $this->attachments->getMessages()->get('attachments')); } - event(new TransactionJournalStored($journal, intval($journalData['piggy_bank_id']))); + event(new TransactionJournalStored($journal, $data['piggy_bank_id'])); Session::flash('success', strval(trans('firefly.stored_journal', ['description' => e($journal->description)]))); Preferences::mark(); - if (intval($request->get('create_another')) === 1) { + if ($createAnother === true) { // set value so create routine will not overwrite URL: Session::put('transactions.create.fromStore', true); return redirect(route('transactions.create', [$request->input('what')]))->withInput(); } + if ($doSplit === true) { + // redirect to edit screen: + return redirect(route('transactions.edit', [$journal->id])); + } + + // redirect to previous URL. return redirect(session('transactions.create.url')); @@ -355,35 +351,31 @@ class TransactionController extends Controller /** - * @param JournalFormRequest $request - * @param JournalRepositoryInterface $repository - * @param AttachmentHelperInterface $att - * @param TransactionJournal $journal + * @param JournalFormRequest $request + * @param TransactionJournal $journal * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function update(JournalFormRequest $request, JournalRepositoryInterface $repository, AttachmentHelperInterface $att, TransactionJournal $journal) + public function update(JournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { - $journalData = $request->getJournalData(); - $repository->update($journal, $journalData); - - // save attachments: - $att->saveAttachmentsForModel($journal); + $data = $request->getJournalData(); + $journal = $repository->update($journal, $data); + $this->attachments->saveAttachmentsForModel($journal); // flash errors - if (count($att->getErrors()->get('attachments')) > 0) { - Session::flash('error', $att->getErrors()->get('attachments')); + if (count($this->attachments->getErrors()->get('attachments')) > 0) { + Session::flash('error', $this->attachments->getErrors()->get('attachments')); } // flash messages - if (count($att->getMessages()->get('attachments')) > 0) { - Session::flash('info', $att->getMessages()->get('attachments')); + if (count($this->attachments->getMessages()->get('attachments')) > 0) { + Session::flash('info', $this->attachments->getMessages()->get('attachments')); } event(new TransactionJournalUpdated($journal)); // update, get events by date and sort DESC - $type = strtolower($journal->transaction_type_type ?? TransactionJournal::transactionTypeStr($journal)); - Session::flash('success', strval(trans('firefly.updated_' . $type, ['description' => e($journalData['description'])]))); + $type = strtolower(TransactionJournal::transactionTypeStr($journal)); + Session::flash('success', strval(trans('firefly.updated_' . $type, ['description' => e($data['description'])]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { diff --git a/app/Http/Requests/JournalFormRequest.php b/app/Http/Requests/JournalFormRequest.php index 9f1a5fcdf5..0076f5e341 100644 --- a/app/Http/Requests/JournalFormRequest.php +++ b/app/Http/Requests/JournalFormRequest.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; use Carbon\Carbon; -use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionType; use Input; @@ -37,86 +36,110 @@ class JournalFormRequest extends Request } /** + * Returns and validates the data required to store a new journal. Can handle both single transaction journals and split journals. + * * @return array */ public function getJournalData() { - $tags = $this->getFieldOrEmptyString('tags'); + $data = [ + 'what' => $this->get('what'), // type. can be 'deposit', 'withdrawal' or 'transfer' + 'user' => auth()->user()->id, + 'date' => new Carbon($this->get('date')), + 'tags' => explode(',', $this->getFieldOrEmptyString('tags')), + 'currency_id' => intval($this->get('amount_currency_id_amount')), - return [ - 'what' => $this->get('what'), - 'description' => trim($this->get('description')), - 'source_account_id' => intval($this->get('source_account_id')), - 'source_account_name' => trim($this->getFieldOrEmptyString('source_account_name')), - 'destination_account_id' => intval($this->get('destination_account_id')), - 'destination_account_name' => trim($this->getFieldOrEmptyString('destination_account_name')), - 'amount' => round($this->get('amount'), 2), - 'user' => auth()->user()->id, - 'amount_currency_id_amount' => intval($this->get('amount_currency_id_amount')), - 'date' => new Carbon($this->get('date')), - 'interest_date' => $this->getDateOrNull('interest_date'), - 'book_date' => $this->getDateOrNull('book_date'), - 'process_date' => $this->getDateOrNull('process_date'), - 'budget_id' => intval($this->get('budget_id')), - 'category' => trim($this->getFieldOrEmptyString('category')), - 'tags' => explode(',', $tags), - 'piggy_bank_id' => intval($this->get('piggy_bank_id')), + // all custom fields: + 'interest_date' => $this->getDateOrNull('interest_date'), + 'book_date' => $this->getDateOrNull('book_date'), + 'process_date' => $this->getDateOrNull('process_date'), + 'due_date' => $this->getDateOrNull('due_date'), + 'payment_date' => $this->getDateOrNull('payment_date'), + 'invoice_date' => $this->getDateOrNull('invoice_date'), + 'internal_reference' => trim(strval($this->get('internal_reference'))), + 'notes' => trim(strval($this->get('notes'))), - // new custom fields here: - 'due_date' => $this->getDateOrNull('due_date'), - 'payment_date' => $this->getDateOrNull('payment_date'), - 'invoice_date' => $this->getDateOrNull('invoice_date'), - 'internal_reference' => trim(strval($this->get('internal_reference'))), - 'notes' => trim(strval($this->get('notes'))), + // transaction / journal data: + 'description' => $this->getFieldOrEmptyString('description'), + 'amount' => round($this->get('amount'), 2), + 'budget_id' => intval($this->get('budget_id')), + 'category' => $this->getFieldOrEmptyString('category'), + 'source_account_id' => intval($this->get('source_account_id')), + 'source_account_name' => $this->getFieldOrEmptyString('source_account_name'), + 'destination_account_id' => $this->getFieldOrEmptyString('destination_account_id'), + 'destination_account_name' => $this->getFieldOrEmptyString('destination_account_name'), + 'piggy_bank_id' => intval($this->get('piggy_bank_id')), ]; + + return $data; } /** * @return array - * @throws Exception */ public function rules() { $what = Input::get('what'); $rules = [ - 'description' => 'required|min:1,max:255', - 'what' => 'required|in:withdrawal,deposit,transfer', - 'amount' => 'numeric|required|min:0.01', - 'date' => 'required|date', - 'process_date' => 'date', - 'book_date' => 'date', - 'interest_date' => 'date', - 'category' => 'between:1,255', - 'amount_currency_id_amount' => 'required|exists:transaction_currencies,id', - 'piggy_bank_id' => 'numeric', + 'what' => 'required|in:withdrawal,deposit,transfer', + 'date' => 'required|date', - // new custom fields here: - 'due_date' => 'date', - 'payment_date' => 'date', - 'internal_reference' => 'min:1,max:255', - 'notes' => 'min:1,max:65536', + // then, custom fields: + 'interest_date' => 'date', + 'book_date' => 'date', + 'process_date' => 'date', + 'due_date' => 'date', + 'payment_date' => 'date', + 'invoice_date' => 'date', + 'internal_reference' => 'min:1,max:255', + 'notes' => 'min:1,max:50000', + // and then transaction rules: + 'description' => 'required|between:1,255', + 'amount' => 'numeric|required|min:0.01', + 'budget_id' => 'mustExist:budgets,id|belongsToUser:budgets,id', + 'category' => 'between:1,255', + 'source_account_id' => 'numeric|belongsToUser:accounts,id', + 'source_account_name' => 'between:1,255', + 'destination_account_id' => 'numeric|belongsToUser:accounts,id', + 'destination_account_name' => 'between:1,255', + 'piggy_bank_id' => 'between:1,255', ]; + // some rules get an upgrade depending on the type of data: + $rules = $this->enhanceRules($what, $rules); + + return $rules; + } + + /** + * Inspired by https://www.youtube.com/watch?v=WwnI0RS6J5A + * + * @param string $what + * @param array $rules + * + * @return array + * @throws FireflyException + */ + private function enhanceRules(string $what, array $rules): array + { switch ($what) { case strtolower(TransactionType::WITHDRAWAL): $rules['source_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts'; $rules['destination_account_name'] = 'between:1,255'; - if (intval(Input::get('budget_id')) != 0) { - $rules['budget_id'] = 'exists:budgets,id|belongsToUser:budgets'; - } break; case strtolower(TransactionType::DEPOSIT): $rules['source_account_name'] = 'between:1,255'; $rules['destination_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts'; break; case strtolower(TransactionType::TRANSFER): + // this may not work: $rules['source_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:destination_account_id'; $rules['destination_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:source_account_id'; break; default: - throw new FireflyException('Cannot handle transaction type of type ' . e($what) . '.'); + throw new FireflyException('Cannot handle transaction type of type ' . e($what) . ' . '); } return $rules; @@ -141,4 +164,63 @@ class JournalFormRequest extends Request { return $this->get($field) ?? ''; } + // + // /** + // * @param int $index + // * @param string $field + // * + // * @return int + // */ + // private function getIntFromArray(int $index, string $field): int + // { + // $array = $this->get($field); + // if (isset($array[$index])) { + // return intval($array[$index]); + // } + // + // return 0; + // } + // + // /** + // * @param int $index + // * @param string $field + // * + // * @return string + // */ + // private function getStringFromArray(int $index, string $field): string + // { + // $array = $this->get($field); + // if (isset($array[$index])) { + // return trim($array[$index]); + // } + // + // return ''; + // } + // + // /** + // * @return array + // */ + // private function getTransactionData(): array + // { + // $transactions = []; + // $array = $this->get('amount'); + // if (is_array($array) && count($array) > 0) { + // foreach ($array as $index => $amount) { + // $transaction = [ + // 'description' => $this->getStringFromArray($index, 'description'), + // 'amount' => round($amount, 2), + // 'budget_id' => $this->getIntFromArray($index, 'budget_id'), + // 'category' => $this->getStringFromArray($index, 'category'), + // 'source_account_id' => $this->getIntFromArray($index, 'source_account_id'), + // 'source_account_name' => $this->getStringFromArray($index, 'source_account_name'), + // 'destination_account_id' => $this->getIntFromArray($index, 'destination_account_id'), + // 'destination_account_name' => $this->getStringFromArray($index, 'destination_account_name'), + // 'piggy_bank_id' => $this->getIntFromArray($index, 'piggy_bank_id'), + // ]; + // $transactions[] = $transaction; + // } + // } + // + // return $transactions; + // } } diff --git a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php index e6437ec74d..8048fe9cd7 100644 --- a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php +++ b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php @@ -15,7 +15,7 @@ namespace FireflyIII\Jobs; use Carbon\Carbon; use FireflyIII\Models\RuleGroup; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Rules\Processor; use FireflyIII\User; use Illuminate\Contracts\Queue\ShouldQueue; @@ -155,10 +155,10 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue */ protected function collectJournals() { - /** @var JournalRepositoryInterface $repository */ - $repository = app(JournalRepositoryInterface::class); + /** @var JournalTaskerInterface $tasker */ + $tasker = app(JournalTaskerInterface::class); - return $repository->getJournalsInRange($this->accounts, $this->startDate, $this->endDate); + return $tasker->getJournalsInRange($this->accounts, $this->startDate, $this->endDate); } /** diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index e4e7020d0f..f6f17b9429 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -434,6 +434,9 @@ class TransactionJournal extends TransactionJournalSupport return new TransactionJournalMeta(); } + if (is_string($value) && strlen($value) === 0) { + return new TransactionJournalMeta(); + } if ($value instanceof Carbon) { $value = $value->toW3cString(); diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index b1530b08ca..e8a9d8c592 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -15,6 +15,7 @@ namespace FireflyIII\Providers; use FireflyIII\Support\Amount; use FireflyIII\Support\ExpandedForm; +use FireflyIII\Support\ExpandedMultiForm; use FireflyIII\Support\FireflyConfig; use FireflyIII\Support\Navigation; use FireflyIII\Support\Preferences; @@ -92,6 +93,11 @@ class FireflyServiceProvider extends ServiceProvider return new ExpandedForm; } ); + $this->app->bind( + 'expandedmultiform', function () { + return new ExpandedMultiForm; + } + ); $this->app->bind('FireflyIII\Repositories\Currency\CurrencyRepositoryInterface', 'FireflyIII\Repositories\Currency\CurrencyRepository'); $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index c92b27c681..d4cfd695d7 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -13,24 +13,18 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; -use Carbon\Carbon; use DB; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; -use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\User; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\JoinClause; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; use Log; /** @@ -100,88 +94,6 @@ class JournalRepository implements JournalRepositoryInterface return $entry; } - /** - * @param array $types - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator - { - $offset = ($page - 1) * $pageSize; - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - $query->where('transaction_journals.completed', 1); - if (count($types) > 0) { - $query->transactionTypes($types); - } - $count = $this->user->transactionJournals()->transactionTypes($types)->count(); - $set = $query->take($pageSize)->offset($offset)->get(TransactionJournal::queryFields()); - $journals = new LengthAwarePaginator($set, $count, $pageSize, $page); - - return $journals; - } - - /** - * Returns a collection of ALL journals, given a specific account and a date range. - * - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection - { - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - $query->where('transaction_journals.completed', 1); - $query->before($end); - $query->after($start); - - if ($accounts->count() > 0) { - $ids = $accounts->pluck('id')->toArray(); - // join source and destination: - $query->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); - } - ); - $query->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); - } - ); - - $query->where( - function (Builder $q) use ($ids) { - $q->whereIn('destination.account_id', $ids); - $q->orWhereIn('source.account_id', $ids); - } - ); - } - - $set = $query->get(TransactionJournal::queryFields()); - - return $set; - } - - /** - * @param TransactionJournal $journal - * - * @return Collection - */ - public function getPiggyBankEvents(TransactionJournal $journal): Collection - { - /** @var Collection $set */ - $events = $journal->piggyBankEvents()->get(); - $events->each( - function (PiggyBankEvent $event) { - $event->piggyBank = $event->piggyBank()->withTrashed()->first(); - } - ); - - return $events; - } /** * @param array $data @@ -192,56 +104,47 @@ class JournalRepository implements JournalRepositoryInterface { // find transaction type. $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - - // store actual journal. - $journal = new TransactionJournal( + $journal = new TransactionJournal( [ 'user_id' => $data['user'], 'transaction_type_id' => $transactionType->id, - 'transaction_currency_id' => $data['amount_currency_id_amount'], + 'transaction_currency_id' => $data['currency_id'], 'description' => $data['description'], 'completed' => 0, 'date' => $data['date'], - 'interest_date' => $data['interest_date'], - 'book_date' => $data['book_date'], - 'process_date' => $data['process_date'], ] ); $journal->save(); - // store or get category - if (strlen($data['category']) > 0) { - $category = Category::firstOrCreateEncrypted(['name' => $data['category'], 'user_id' => $data['user']]); - $journal->categories()->save($category); - } + // store stuff: + $this->storeCategoryWithJournal($journal, $data['category']); + $this->storeBudgetWithJournal($journal, $data['budget_id']); + $accounts = $this->storeAccounts($transactionType, $data); - // store or get budget - if (intval($data['budget_id']) > 0 && $transactionType->type !== TransactionType::TRANSFER) { - /** @var \FireflyIII\Models\Budget $budget */ - $budget = Budget::find($data['budget_id']); - $journal->budgets()->save($budget); - } + // store two transactions: + $one = [ + 'journal' => $journal, + 'account' => $accounts['source'], + 'amount' => bcmul(strval($data['amount']), '-1'), + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, + ]; + $this->storeTransaction($one); - // store accounts (depends on type) - list($sourceAccount, $destinationAccount) = $this->storeAccounts($transactionType, $data); + $two = [ + 'journal' => $journal, + 'account' => $accounts['destination'], + 'amount' => $data['amount'], + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, + ]; + + $this->storeTransaction($two); - // store accompanying transactions. - Transaction::create( // first transaction. - [ - 'account_id' => $sourceAccount->id, - 'transaction_journal_id' => $journal->id, - 'amount' => $data['amount'] * -1, - ] - ); - Transaction::create( // second transaction. - [ - 'account_id' => $destinationAccount->id, - 'transaction_journal_id' => $journal->id, - 'amount' => $data['amount'], - ] - ); - $journal->completed = 1; - $journal->save(); // store tags if (isset($data['tags']) && is_array($data['tags'])) { @@ -256,6 +159,9 @@ class JournalRepository implements JournalRepositoryInterface Log::debug(sprintf('Could not store meta field "%s" with value "%s" for journal #%d', json_encode($key), json_encode($value), $journal->id)); } + $journal->completed = 1; + $journal->save(); + return $journal; } @@ -302,45 +208,24 @@ class JournalRepository implements JournalRepositoryInterface */ public function update(TransactionJournal $journal, array $data): TransactionJournal { - // update actual journal. - $journal->transaction_currency_id = $data['amount_currency_id_amount']; + // update actual journal: + $journal->transaction_currency_id = $data['currency_id']; $journal->description = $data['description']; $journal->date = $data['date']; // unlink all categories, recreate them: $journal->categories()->detach(); - if (strlen($data['category']) > 0) { - $category = Category::firstOrCreateEncrypted(['name' => $data['category'], 'user_id' => $data['user']]); - $journal->categories()->save($category); - } - - // unlink all budgets and recreate them: $journal->budgets()->detach(); - if (intval($data['budget_id']) > 0 && $journal->transactionType->type !== TransactionType::TRANSFER) { - /** @var \FireflyIII\Models\Budget $budget */ - $budget = Budget::where('user_id', $this->user->id)->where('id', $data['budget_id'])->first(); - $journal->budgets()->save($budget); - } - // store accounts (depends on type) - list($fromAccount, $toAccount) = $this->storeAccounts($journal->transactionType, $data); + $this->storeCategoryWithJournal($journal, $data['category']); + $this->storeBudgetWithJournal($journal, $data['budget_id']); + $accounts = $this->storeAccounts($journal->transactionType, $data); - // update the from and to transaction. - /** @var Transaction $transaction */ - foreach ($journal->transactions()->get() as $transaction) { - if ($transaction->amount < 0) { - // this is the from transaction, negative amount: - $transaction->amount = $data['amount'] * -1; - $transaction->account_id = $fromAccount->id; - $transaction->save(); - } - if ($transaction->amount > 0) { - $transaction->amount = $data['amount']; - $transaction->account_id = $toAccount->id; - $transaction->save(); - } - } + $sourceAmount = bcmul(strval($data['amount']), '-1'); + $this->updateSourceTransaction($journal, $accounts['source'], $sourceAmount); // negative because source loses money. + $amount = strval($data['amount']); + $this->updateDestinationTransaction($journal, $accounts['destination'], $amount); // positive because destination gets money. $journal->save(); @@ -402,38 +287,66 @@ class JournalRepository implements JournalRepositoryInterface */ private function storeAccounts(TransactionType $type, array $data): array { - $sourceAccount = null; - $destinationAccount = null; + $accounts = [ + 'source' => null, + 'destination' => null, + ]; switch ($type->type) { case TransactionType::WITHDRAWAL: - list($sourceAccount, $destinationAccount) = $this->storeWithdrawalAccounts($data); + $accounts = $this->storeWithdrawalAccounts($data); break; case TransactionType::DEPOSIT: - list($sourceAccount, $destinationAccount) = $this->storeDepositAccounts($data); + $accounts = $this->storeDepositAccounts($data); break; case TransactionType::TRANSFER: - $sourceAccount = Account::where('user_id', $this->user->id)->where('id', $data['source_account_id'])->first(); - $destinationAccount = Account::where('user_id', $this->user->id)->where('id', $data['destination_account_id'])->first(); + + $accounts['source'] = Account::where('user_id', $this->user->id)->where('id', $data['source_account_id'])->first(); + $accounts['destination'] = Account::where('user_id', $this->user->id)->where('id', $data['destination_account_id'])->first(); break; default: - throw new FireflyException('Did not recognise transaction type.'); + throw new FireflyException(sprintf('Did not recognise transaction type "%s".', $type->type)); } - if (is_null($destinationAccount)) { + if (is_null($accounts['source'])) { Log::error('"destination"-account is null, so we cannot continue!', ['data' => $data]); throw new FireflyException('"destination"-account is null, so we cannot continue!'); } - if (is_null($sourceAccount)) { + if (is_null($accounts['destination'])) { Log::error('"source"-account is null, so we cannot continue!', ['data' => $data]); throw new FireflyException('"source"-account is null, so we cannot continue!'); } - return [$sourceAccount, $destinationAccount]; + return $accounts; + } + + /** + * @param TransactionJournal $journal + * @param int $budgetId + */ + private function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId) + { + if (intval($budgetId) > 0 && $journal->transactionType->type !== TransactionType::TRANSFER) { + /** @var \FireflyIII\Models\Budget $budget */ + $budget = Budget::find($budgetId); + $journal->budgets()->save($budget); + } + } + + /** + * @param TransactionJournal $journal + * @param string $category + */ + private function storeCategoryWithJournal(TransactionJournal $journal, string $category) + { + if (strlen($category) > 0) { + $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]); + $journal->categories()->save($category); + } } /** @@ -446,19 +359,50 @@ class JournalRepository implements JournalRepositoryInterface $destinationAccount = Account::where('user_id', $this->user->id)->where('id', $data['destination_account_id'])->first(['accounts.*']); if (strlen($data['source_account_name']) > 0) { - $fromType = AccountType::where('type', 'Revenue account')->first(); - $fromAccount = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $fromType->id, 'name' => $data['source_account_name'], 'active' => 1] + $sourceType = AccountType::where('type', 'Revenue account')->first(); + $sourceAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $sourceType->id, 'name' => $data['source_account_name'], 'active' => 1] ); - return [$fromAccount, $destinationAccount]; + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; } - $fromType = AccountType::where('type', 'Cash account')->first(); - $fromAccount = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $fromType->id, 'name' => 'Cash account', 'active' => 1] + $sourceType = AccountType::where('type', 'Cash account')->first(); + $sourceAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $sourceType->id, 'name' => 'Cash account', 'active' => 1] ); - return [$fromAccount, $destinationAccount]; + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + } + + + private function storeTransaction(array $data): Transaction + { + /** @var Transaction $transaction */ + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $data['journal']->id, + 'account_id' => $data['account']->id, + 'amount' => $data['amount'], + 'description' => $data['description'], + 'identifier' => $data['identifier'], + ] + ); + if (!is_null($data['category'])) { + $transaction->categories()->save($data['category']); + } + + if (!is_null($data['budget'])) { + $transaction->categories()->save($data['budget']); + } + + return $transaction; + } /** @@ -481,14 +425,69 @@ class JournalRepository implements JournalRepositoryInterface ] ); - return [$sourceAccount, $destinationAccount]; + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; } $destinationType = AccountType::where('type', 'Cash account')->first(); $destinationAccount = Account::firstOrCreateEncrypted( ['user_id' => $data['user'], 'account_type_id' => $destinationType->id, 'name' => 'Cash account', 'active' => 1] ); - return [$sourceAccount, $destinationAccount]; + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + + + } + + /** + * @param TransactionJournal $journal + * @param Account $account + * @param string $amount + * + * @throws FireflyException + */ + private function updateDestinationTransaction(TransactionJournal $journal, Account $account, string $amount) + { + // should be one: + $set = $journal->transactions()->where('amount', '>', 0)->get(); + if ($set->count() != 1) { + throw new FireflyException( + sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount more than zero.', $journal->id, $set->count()) + ); + } + /** @var Transaction $transaction */ + $transaction = $set->first(); + $transaction->amount = $amount; + $transaction->account_id = $account->id; + $transaction->save(); + + } + + /** + * @param TransactionJournal $journal + * @param Account $account + * @param string $amount + * + * @throws FireflyException + */ + private function updateSourceTransaction(TransactionJournal $journal, Account $account, string $amount) + { + // should be one: + $set = $journal->transactions()->where('amount', '<', 0)->get(); + if ($set->count() != 1) { + throw new FireflyException( + sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount less than zero.', $journal->id, $set->count()) + ); + } + /** @var Transaction $transaction */ + $transaction = $set->first(); + $transaction->amount = $amount; + $transaction->account_id = $account->id; + $transaction->save(); } diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index e12acda50a..48090345c6 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -13,11 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; -use Carbon\Carbon; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; /** * Interface JournalRepositoryInterface @@ -52,34 +48,6 @@ interface JournalRepositoryInterface */ public function first(): TransactionJournal; - /** - * Returns a page of a specific type(s) of journal. - * - * @param array $types - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator; - - /** - * Returns a collection of ALL journals, given a specific account and a date range. - * - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection; - - /** - * @param TransactionJournal $journal - * - * @return Collection - */ - public function getPiggyBankEvents(TransactionJournal $journal): Collection; /** * @param array $data diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index 122e4527df..669bcda926 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; +use Carbon\Carbon; use Crypt; use DB; use FireflyIII\Models\Transaction; @@ -20,6 +21,8 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; /** * Class JournalTasker @@ -42,6 +45,91 @@ class JournalTasker implements JournalTaskerInterface $this->user = $user; } + /** + * Returns a page of a specific type(s) of journal. + * + * @param array $types + * @param int $page + * @param int $pageSize + * + * @return LengthAwarePaginator + */ + public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator + { + $offset = ($page - 1) * $pageSize; + $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); + $query->where('transaction_journals.completed', 1); + if (count($types) > 0) { + $query->transactionTypes($types); + } + $count = $this->user->transactionJournals()->transactionTypes($types)->count(); + $set = $query->take($pageSize)->offset($offset)->get(TransactionJournal::queryFields()); + $journals = new LengthAwarePaginator($set, $count, $pageSize, $page); + + return $journals; + } + + /** + * Returns a collection of ALL journals, given a specific account and a date range. + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection + { + $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); + $query->where('transaction_journals.completed', 1); + $query->before($end); + $query->after($start); + + if ($accounts->count() > 0) { + $ids = $accounts->pluck('id')->toArray(); + // join source and destination: + $query->leftJoin( + 'transactions as source', function (JoinClause $join) { + $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); + } + ); + $query->leftJoin( + 'transactions as destination', function (JoinClause $join) { + $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); + } + ); + + $query->where( + function (Builder $q) use ($ids) { + $q->whereIn('destination.account_id', $ids); + $q->orWhereIn('source.account_id', $ids); + } + ); + } + + $set = $query->get(TransactionJournal::queryFields()); + + return $set; + } + + /** + * @param TransactionJournal $journal + * + * @return Collection + */ + public function getPiggyBankEvents(TransactionJournal $journal): Collection + { + /** @var Collection $set */ + $events = $journal->piggyBankEvents()->get(); + $events->each( + function (PiggyBankEvent $event) { + $event->piggyBank = $event->piggyBank()->withTrashed()->first(); + } + ); + + return $events; + } + /** * Get an overview of the transactions of a journal, tailored to the view * that shows a transaction (transaction/show/xx). @@ -138,7 +226,6 @@ class JournalTasker implements JournalTaskerInterface return $transactions; } - /** * Collect the balance of an account before the given transaction has hit. This is tricky, because * the balance does not depend on the transaction itself but the journal it's part of. And of course diff --git a/app/Repositories/Journal/JournalTaskerInterface.php b/app/Repositories/Journal/JournalTaskerInterface.php index e84fa09c33..feada074f5 100644 --- a/app/Repositories/Journal/JournalTaskerInterface.php +++ b/app/Repositories/Journal/JournalTaskerInterface.php @@ -14,7 +14,10 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; +use Carbon\Carbon; use FireflyIII\Models\TransactionJournal; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; /** * Interface JournalTaskerInterface @@ -23,6 +26,34 @@ use FireflyIII\Models\TransactionJournal; */ interface JournalTaskerInterface { + /** + * Returns a page of a specific type(s) of journal. + * + * @param array $types + * @param int $page + * @param int $pageSize + * + * @return LengthAwarePaginator + */ + public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator; + + /** + * Returns a collection of ALL journals, given a specific account and a date range. + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection; + + /** + * @param TransactionJournal $journal + * + * @return Collection + */ + public function getPiggyBankEvents(TransactionJournal $journal): Collection; /** * Get an overview of the transactions of a journal, tailored to the view diff --git a/app/Rules/TransactionMatcher.php b/app/Rules/TransactionMatcher.php index 82b5429777..7491bbbb88 100644 --- a/app/Rules/TransactionMatcher.php +++ b/app/Rules/TransactionMatcher.php @@ -15,7 +15,7 @@ namespace FireflyIII\Rules; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalTaskerInterface; use Illuminate\Support\Collection; use Log; @@ -31,8 +31,8 @@ class TransactionMatcher private $limit = 10; /** @var int Maximum number of transaction to search in (for performance reasons) * */ private $range = 200; - /** @var JournalRepositoryInterface */ - private $repository; + /** @var JournalTaskerInterface */ + private $tasker; /** @var array */ private $transactionTypes = [TransactionType::DEPOSIT, TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; /** @var array List of triggers to match */ @@ -41,11 +41,11 @@ class TransactionMatcher /** * TransactionMatcher constructor. Typehint the repository. * - * @param JournalRepositoryInterface $repository + * @param JournalTaskerInterface $tasker */ - public function __construct(JournalRepositoryInterface $repository) + public function __construct(JournalTaskerInterface $tasker) { - $this->repository = $repository; + $this->tasker = $tasker; } @@ -76,7 +76,7 @@ class TransactionMatcher // - the maximum number of transactions to search in have been searched do { // Fetch a batch of transactions from the database - $paginator = $this->repository->getJournals($this->transactionTypes, $page, $pagesize); + $paginator = $this->tasker->getJournals($this->transactionTypes, $page, $pagesize); $set = $paginator->getCollection(); diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 36682352d3..495fd2d75c 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -427,6 +427,7 @@ class ExpandedForm */ protected function expandOptionArray(string $name, $label, array $options): array { + $name = str_replace('[]', '', $name); $options['class'] = 'form-control'; $options['id'] = 'ffInput_' . $name; $options['autocomplete'] = 'off'; @@ -494,6 +495,7 @@ class ExpandedForm if (isset($options['label'])) { return $options['label']; } + $name = str_replace('[]', '', $name); return strval(trans('form.' . $name)); diff --git a/app/Support/ExpandedMultiForm.php b/app/Support/ExpandedMultiForm.php new file mode 100644 index 0000000000..e8340c7b9b --- /dev/null +++ b/app/Support/ExpandedMultiForm.php @@ -0,0 +1,188 @@ +label($name, $options); + $options = $this->expandOptionArray($name, $index, $label, $options); + $classes = $this->getHolderClasses($name, $index); + $value = $this->fillFieldValue($name, $index, $value); + $options['step'] = 'any'; + $options['min'] = '0.01'; + $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); + $currencies = Amt::getAllCurrencies(); + $options['data-hiddenfield'] = 'amount_currency_id_' . $name . '_' . $index; + unset($options['currency']); + unset($options['placeholder']); + $html = view('form.multi.amount', compact('defaultCurrency', 'index', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + + } + + /** + * @param string $name + * @param int $index + * @param array $list + * @param null $selected + * @param array $options + * + * @return string + */ + public function select(string $name, int $index, array $list = [], $selected = null, array $options = []): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $index, $label, $options); + $classes = $this->getHolderClasses($name, $index); + $selected = $this->fillFieldValue($name, $index, $selected); + unset($options['autocomplete']); + unset($options['placeholder']); + $html = view('form.multi.select', compact('classes', 'index', 'name', 'label', 'selected', 'options', 'list'))->render(); + + return $html; + } + + /** + * @param string $name + * @param int $index + * @param null $value + * @param array $options + * + * @return string + */ + public function text(string $name, int $index, $value = null, array $options = []): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $index, $label, $options); + $classes = $this->getHolderClasses($name, $index); + $value = $this->fillFieldValue($name, $index, $value); + $html = view('form.multi.text', compact('classes', 'name', 'index', 'label', 'value', 'options'))->render(); + + return $html; + + } + + /** + * @param string $name + * @param int $index + * @param string $label + * @param array $options + * + * @return array + */ + protected function expandOptionArray(string $name, int $index, string $label, array $options): array + { + $options['class'] = 'form-control'; + $options['id'] = 'ffInput_' . $name . '_' . $index; + $options['autocomplete'] = 'off'; + $options['placeholder'] = ucfirst($label); + + return $options; + } + + /** + * @param string $name + * @param int $index + * @param $value + * + * @return mixed + */ + protected function fillFieldValue(string $name, int $index, $value) + { + if (Session::has('preFilled')) { + $preFilled = session('preFilled'); + $value = isset($preFilled[$name][$index]) && is_null($value) ? $preFilled[$name][$index] : $value; + } + try { + if (!is_null(Input::old($name)[$index])) { + $value = Input::old($name)[$index]; + } + } catch (RuntimeException $e) { + // don't care about session errors. + } + if ($value instanceof Carbon) { + $value = $value->format('Y-m-d'); + } + + + return $value; + } + + /** + * @param string $name + * @param int $index + * + * @return string + */ + protected function getHolderClasses(string $name, int $index): string + { + /* + * Get errors from session: + */ + /** @var MessageBag $errors */ + $errors = session('errors'); + $classes = 'form-group'; + $set = []; + + if (!is_null($errors)) { + $set = $errors->get($name . '.' . $index); + } + + if (!is_null($errors) && count($set) > 0) { + $classes = 'form-group has-error has-feedback'; + } + + return $classes; + } + + /** + * @param string $name + * @param array $options + * + * @return string + */ + protected function label(string $name, array $options): string + { + if (isset($options['label'])) { + return $options['label']; + } + + return strval(trans('form.' . $name)); + + } +} \ No newline at end of file diff --git a/app/Support/Facades/ExpandedMultiForm.php b/app/Support/Facades/ExpandedMultiForm.php new file mode 100644 index 0000000000..064f3bb305 --- /dev/null +++ b/app/Support/Facades/ExpandedMultiForm.php @@ -0,0 +1,35 @@ +where($field, $value)->count(); + if ($count === 1) { + return true; + } + + return false; + } + /** * @param $attribute * diff --git a/config/app.php b/config/app.php index 0dfd97b9f8..d60998fa86 100755 --- a/config/app.php +++ b/config/app.php @@ -188,8 +188,8 @@ return [ // own stuff: - //Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, - //Barryvdh\Debugbar\ServiceProvider::class, + Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, + Barryvdh\Debugbar\ServiceProvider::class, DaveJamesMiller\Breadcrumbs\ServiceProvider::class, TwigBridge\ServiceProvider::class, 'PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider', @@ -226,50 +226,51 @@ return [ 'aliases' => [ - 'App' => Illuminate\Support\Facades\App::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Lang' => Illuminate\Support\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, - 'Notification' => Illuminate\Support\Facades\Notification::class, - 'Password' => Illuminate\Support\Facades\Password::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, - 'View' => Illuminate\Support\Facades\View::class, - 'Twig' => 'TwigBridge\Facade\Twig', - 'Form' => Collective\Html\FormFacade::class, - 'Html' => Collective\Html\HtmlFacade::class, - 'Breadcrumbs' => 'DaveJamesMiller\Breadcrumbs\Facade', - 'Preferences' => 'FireflyIII\Support\Facades\Preferences', - 'FireflyConfig' => 'FireflyIII\Support\Facades\FireflyConfig', - 'Navigation' => 'FireflyIII\Support\Facades\Navigation', - 'Amount' => 'FireflyIII\Support\Facades\Amount', - 'Steam' => 'FireflyIII\Support\Facades\Steam', - 'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm', - 'Entrust' => 'Zizaco\Entrust\EntrustFacade', - 'Input' => 'Illuminate\Support\Facades\Input', - 'Google2FA' => 'PragmaRX\Google2FA\Vendor\Laravel\Facade', + 'App' => Illuminate\Support\Facades\App::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + 'Twig' => 'TwigBridge\Facade\Twig', + 'Form' => Collective\Html\FormFacade::class, + 'Html' => Collective\Html\HtmlFacade::class, + 'Breadcrumbs' => 'DaveJamesMiller\Breadcrumbs\Facade', + 'Preferences' => 'FireflyIII\Support\Facades\Preferences', + 'FireflyConfig' => 'FireflyIII\Support\Facades\FireflyConfig', + 'Navigation' => 'FireflyIII\Support\Facades\Navigation', + 'Amount' => 'FireflyIII\Support\Facades\Amount', + 'Steam' => 'FireflyIII\Support\Facades\Steam', + 'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm', + 'ExpandedMultiForm' => 'FireflyIII\Support\Facades\ExpandedMultiForm', + 'Entrust' => 'Zizaco\Entrust\EntrustFacade', + 'Input' => 'Illuminate\Support\Facades\Input', + 'Google2FA' => 'PragmaRX\Google2FA\Vendor\Laravel\Facade', ], diff --git a/config/twigbridge.php b/config/twigbridge.php index 886f44d6e1..5a4822e22f 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -162,6 +162,11 @@ return [ 'multiRadio', 'file', 'multiCheckbox', 'staticText', 'amountSmall', ], ], + 'ExpandedMultiForm' => [ + 'is_safe' => [ + 'text','select','amount' + ], + ], 'Form' => [ 'is_safe' => [ 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea', 'file', diff --git a/public/js/ff/firefly.js b/public/js/ff/firefly.js index d63eddbcc3..b3e4b7bf93 100644 --- a/public/js/ff/firefly.js +++ b/public/js/ff/firefly.js @@ -5,6 +5,9 @@ $(function () { // when you click on a currency, this happens: $('.currency-option').click(currencySelect); + // when you click on a multi currency, this happens: + $('.multi-currency-option').click(multiCurrencySelect); + var ranges = {}; ranges[dateRangeConfig.currentPeriod] = [moment(dateRangeConfig.ranges.current[0]), moment(dateRangeConfig.ranges.current[1])]; ranges[dateRangeConfig.previousPeriod] = [moment(dateRangeConfig.ranges.previous[0]), moment(dateRangeConfig.ranges.previous[1])]; @@ -57,6 +60,47 @@ $(function () { }); +function multiCurrencySelect(e) { + "use strict"; + // clicked on + var target = $(e.target); // target is the tag. + + // name of the field in question: + var name = target.data('name'); + + // index of the field in question: + var index = target.data('index'); + console.log('name is ' + name + ':' + index); + + // id of menu button (used later on): + var menuID = 'currency_dropdown_' + name + '_' + index; + + // the hidden input with the actual value of the selected currency: + var hiddenInputName = 'amount_currency_id_' + name + '_' + index; + console.log('Looking for hidden input: ' + hiddenInputName); + + // span with the current selection (next to the caret): + var spanId = 'currency_select_symbol_' + name + '_' + index; + + // the selected currency symbol: + var symbol = target.data('symbol'); + + // id of the selected currency. + var id = target.data('id'); + + // update the hidden input: + $('input[name="' + hiddenInputName + '"]').val(id); + + // update the symbol: + $('#' + spanId).text(symbol); + + // close the menu (hack hack) + $('#' + menuID).click(); + + + return false; +} + function currencySelect(e) { "use strict"; // clicked on diff --git a/resources/views/form/date.twig b/resources/views/form/date.twig index f14289d1fe..3f08f20e3b 100644 --- a/resources/views/form/date.twig +++ b/resources/views/form/date.twig @@ -2,7 +2,12 @@
- {{ Form.input('date', name, value, options) }} +
+
+ +
+ {{ Form.input('date', name, value, options) }} +
{% include 'form/help.twig' %} {% include 'form/feedback.twig' %}
diff --git a/resources/views/form/multi/amount.twig b/resources/views/form/multi/amount.twig new file mode 100644 index 0000000000..4e6fc1590f --- /dev/null +++ b/resources/views/form/multi/amount.twig @@ -0,0 +1,30 @@ +
+ +
+
+ + {{ Form.input('number', name~'['~index~']', value, options) }} +
+ {% include 'form.multi.feedback.twig' %} +
+ + +
diff --git a/resources/views/form/multi/feedback.twig b/resources/views/form/multi/feedback.twig new file mode 100644 index 0000000000..92c00d26d9 --- /dev/null +++ b/resources/views/form/multi/feedback.twig @@ -0,0 +1,4 @@ +{% if errors.has(name~'.'~index) %} + +

{{ errors.first(name~'.'~index) }}

+{% endif %} diff --git a/resources/views/form/multi/select.twig b/resources/views/form/multi/select.twig new file mode 100644 index 0000000000..1c6d81b31a --- /dev/null +++ b/resources/views/form/multi/select.twig @@ -0,0 +1,10 @@ +
+ + +
+ {{ Form.select(name~'['~index~']', list, selected , options ) }} + {% include 'form.help.twig' %} + {% include 'form.multi.feedback.twig' %} + +
+
diff --git a/resources/views/form/multi/text.twig b/resources/views/form/multi/text.twig new file mode 100644 index 0000000000..5f0b154fbd --- /dev/null +++ b/resources/views/form/multi/text.twig @@ -0,0 +1,9 @@ +
+ + +
+ {{ Form.input('text', name~'['~index~']', value, options) }} + {% include 'form/help.twig' %} + {% include 'form.multi.feedback.twig' %} +
+
diff --git a/resources/views/transactions/create.twig b/resources/views/transactions/create.twig index afa0b4b78c..fc1dabfcab 100644 --- a/resources/views/transactions/create.twig +++ b/resources/views/transactions/create.twig @@ -29,23 +29,22 @@ - + {# DESCRIPTION IS ALWAYS AVAILABLE #} {{ ExpandedForm.text('description') }} - - {{ ExpandedForm.select('source_account_id',assetAccounts, null, {label: trans('form.asset_source_account')}) }} + {# SELECTABLE SOURCE ACCOUNT ONLY FOR WITHDRAWALS AND TRANSFERS #} + {{ ExpandedForm.select('source_account_id', assetAccounts, null, {label: trans('form.asset_source_account')}) }} - - {{ ExpandedForm.text('source_account_name',null, {label: trans('form.revenue_account')}) }} + {# FREE FORMAT SOURCE ACCOUNT ONLY FOR DEPOSITS #} + {{ ExpandedForm.text('source_account_name', null, {label: trans('form.revenue_account')}) }} - - {{ ExpandedForm.text('destination_account_name',null, {label: trans('form.expense_account')}) }} + {# FREE FORMAT DESTINATION ACCOUNT ONLY FOR EXPENSES #} + {{ ExpandedForm.text('destination_account_name', null, {label: trans('form.expense_account')}) }} - - {{ ExpandedForm.select('destination_account_id',assetAccounts, null, {label: trans('form.asset_destination_account')} ) }} + {# SELECTABLE DESTINATION ACCOUNT ONLY FOR TRANSFERS AND DEPOSITS #} + {{ ExpandedForm.select('destination_account_id', assetAccounts, null, {label: trans('form.asset_destination_account')} ) }} - - + {# ALWAYS SHOW AMOUNT #} {{ ExpandedForm.amount('amount') }} @@ -66,9 +65,9 @@
{% if budgets|length > 1 %} - {{ ExpandedForm.select('budget_id',budgets,0) }} + {{ ExpandedForm.select('budget_id', budgets, 0) }} {% else %} - {{ ExpandedForm.select('budget_id',budgets,0, {helpText: trans('firefly.no_budget_pointer')}) }} + {{ ExpandedForm.select('budget_id', budgets, 0, {helpText: trans('firefly.no_budget_pointer')}) }} {% endif %} @@ -78,7 +77,7 @@ {{ ExpandedForm.text('tags') }} - {{ ExpandedForm.select('piggy_bank_id',piggies) }} + {{ ExpandedForm.select('piggy_bank_id', piggies) }}
@@ -108,33 +107,33 @@
+ {# INTEREST DATE #} {% if optionalFields.interest_date %} - {{ ExpandedForm.date('interest_date') }} {% endif %} + {# BOOK DATE #} {% if optionalFields.book_date %} - {{ ExpandedForm.date('book_date') }} {% endif %} + {# PROCESSING DATE #} {% if optionalFields.process_date %} - {{ ExpandedForm.date('process_date') }} {% endif %} + {# DUE DATE #} {% if optionalFields.due_date %} - {{ ExpandedForm.date('due_date') }} {% endif %} + {# PAYMENT DATE #} {% if optionalFields.payment_date %} - {{ ExpandedForm.date('payment_date') }} {% endif %} + {# INVOICE DATE #} {% if optionalFields.invoice_date %} - {{ ExpandedForm.date('invoice_date') }} {% endif %} @@ -149,13 +148,14 @@

{{ 'optional_field_meta_business'|_ }}

+ + {# REFERENCE #} {% if optionalFields.internal_reference %} - {{ ExpandedForm.text('internal_reference') }} {% endif %} + {# NOTES #} {% if optionalFields.notes %} - {{ ExpandedForm.textarea('notes') }} {% endif %} @@ -170,8 +170,8 @@

{{ 'optional_field_attachments'|_ }}

+ {# ATTACHMENTS #} {% if optionalFields.attachments %} - {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {% endif %}
@@ -193,6 +193,7 @@ + #}