diff --git a/app/Crud/Split/Journal.php b/app/Crud/Split/Journal.php index 0a0664ac33..0f9b6dbc1a 100644 --- a/app/Crud/Split/Journal.php +++ b/app/Crud/Split/Journal.php @@ -9,6 +9,7 @@ namespace FireflyIII\Crud\Split; +use FireflyIII\Events\TransactionStored; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -132,6 +133,13 @@ class Journal implements JournalInterface $two->budgets()->save($budget); } + if ($transaction['piggy_bank_id'] > 0) { + // add some extra meta information to the transaction data + $transaction['transaction_journal_id'] = $journal->id; + $transaction['date'] = $journal->date->format('Y-m-d'); + event(new TransactionStored($transaction)); + } + return new Collection([$one, $two]); } diff --git a/app/Events/TransactionStored.php b/app/Events/TransactionStored.php new file mode 100644 index 0000000000..4da42ee0c8 --- /dev/null +++ b/app/Events/TransactionStored.php @@ -0,0 +1,46 @@ +transaction = $transaction; + } + +} diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index e12666657a..5a09b343ad 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -11,6 +11,7 @@ declare(strict_types = 1); namespace FireflyIII\Export\Entry; use FireflyIII\Models\TransactionJournal; +use Illuminate\Support\Collection; /** * To extend the exported object, in case of new features in Firefly III for example, @@ -45,8 +46,21 @@ class Entry public $description; /** @var EntryAccount */ public $destinationAccount; + /** @var Collection */ + public $destinationAccounts; /** @var EntryAccount */ public $sourceAccount; + /** @var Collection */ + public $sourceAccounts; + + /** + * Entry constructor. + */ + private function __construct() + { + $this->sourceAccounts = new Collection; + $this->destinationAccounts = new Collection; + } /** * @param TransactionJournal $journal @@ -66,10 +80,18 @@ class Entry $entry->bill = new EntryBill($journal->bill); $sources = TransactionJournal::sourceAccountList($journal); - $entry->sourceAccount = new EntryAccount($sources->first()); $destinations = TransactionJournal::destinationAccountList($journal); + $entry->sourceAccount = new EntryAccount($sources->first()); $entry->destinationAccount = new EntryAccount($destinations->first()); + foreach ($sources as $source) { + $entry->sourceAccounts->push(new EntryAccount($source)); + } + + foreach ($destinations as $destination) { + $entry->destinationAccounts->push(new EntryAccount($destination)); + } + return $entry; } diff --git a/app/Export/Exporter/CsvExporter.php b/app/Export/Exporter/CsvExporter.php index 066bc47dd5..c7256ccb47 100644 --- a/app/Export/Exporter/CsvExporter.php +++ b/app/Export/Exporter/CsvExporter.php @@ -11,6 +11,7 @@ declare(strict_types = 1); namespace FireflyIII\Export\Exporter; use FireflyIII\Export\Entry\Entry; +use FireflyIII\Export\Entry\EntryAccount; use FireflyIII\Models\ExportJob; use League\Csv\Writer; use SplFileObject; @@ -61,19 +62,84 @@ class CsvExporter extends BasicExporter implements ExporterInterface // all rows: $rows = []; + /* + * Count the maximum number of sources and destinations + * each entry has. May need to expand the number of export fields: + */ + $maxSourceAccounts = 1; + $maxDestinationAccounts = 1; + /** @var Entry $entry */ + foreach ($this->getEntries() as $entry) { + $sources = $entry->sourceAccounts->count(); + $destinations = $entry->destinationAccounts->count(); + $maxSourceAccounts = max($maxSourceAccounts, $sources); + $maxDestinationAccounts = max($maxDestinationAccounts, $destinations); + } + // add header: - $rows[] = array_keys(Entry::getFieldsAndTypes()); + $rows[] = array_keys($this->getFieldsAndTypes($maxSourceAccounts, $maxDestinationAccounts)); // then the rest: /** @var Entry $entry */ foreach ($this->getEntries() as $entry) { // order is defined in Entry::getFieldsAndTypes. - $rows[] = [ - $entry->description, $entry->amount, $entry->date, $entry->sourceAccount->accountId, $entry->sourceAccount->name, $entry->sourceAccount->iban, - $entry->sourceAccount->type, $entry->sourceAccount->number, $entry->destinationAccount->accountId, $entry->destinationAccount->name, - $entry->destinationAccount->iban, $entry->destinationAccount->type, $entry->destinationAccount->number, $entry->budget->budgetId, - $entry->budget->name, $entry->category->categoryId, $entry->category->name, $entry->bill->billId, $entry->bill->name, - ]; + $current = [ + $entry->description, + $entry->amount, + $entry->date]; + for ($i = 0; $i < $maxSourceAccounts; $i++) { + /** @var EntryAccount $source */ + $source = $entry->sourceAccounts->get($i); + $currentId = ''; + $currentName = ''; + $currentIban = ''; + $currentType = ''; + $currentNumber = ''; + if ($source) { + $currentId = $source->accountId; + $currentName = $source->name; + $currentIban = $source->iban; + $currentType = $source->type; + $currentNumber = $source->number; + } + $current[] = $currentId; + $current[] = $currentName; + $current[] = $currentIban; + $current[] = $currentType; + $current[] = $currentNumber; + } + unset($source); + for ($i = 0; $i < $maxDestinationAccounts; $i++) { + /** @var EntryAccount $destination */ + $destination = $entry->destinationAccounts->get($i); + $currentId = ''; + $currentName = ''; + $currentIban = ''; + $currentType = ''; + $currentNumber = ''; + if ($destination) { + $currentId = $destination->accountId; + $currentName = $destination->name; + $currentIban = $destination->iban; + $currentType = $destination->type; + $currentNumber = $destination->number; + } + $current[] = $currentId; + $current[] = $currentName; + $current[] = $currentIban; + $current[] = $currentType; + $current[] = $currentNumber; + } + unset($destination); + + + $current[] = $entry->budget->budgetId; + $current[] = $entry->budget->name; + $current[] = $entry->category->categoryId; + $current[] = $entry->category->name; + $current[] = $entry->bill->billId; + $current[] = $entry->bill->name; + $rows[] = $current; } $writer->insertAll($rows); @@ -81,6 +147,43 @@ class CsvExporter extends BasicExporter implements ExporterInterface return true; } + /** + * @return array + */ + private function getFieldsAndTypes(int $sources, int $destinations): array + { + // key = field name (see top of class) + // value = field type (see csv.php under 'roles') + $array = [ + 'description' => 'description', + 'amount' => 'amount', + 'date' => 'date-transaction', + ]; + for ($i = 0; $i < $sources; $i++) { + $array['source_account_' . $i . '_id'] = 'account-id'; + $array['source_account_' . $i . '_name'] = 'account-name'; + $array['source_account_' . $i . '_iban'] = 'account-iban'; + $array['source_account_' . $i . '_type'] = '_ignore'; + $array['source_account_' . $i . '_number'] = 'account-number'; + } + for ($i = 0; $i < $destinations; $i++) { + $array['destination_account_' . $i . '_id'] = 'account-id'; + $array['destination_account_' . $i . '_name'] = 'account-name'; + $array['destination_account_' . $i . '_iban'] = 'account-iban'; + $array['destination_account_' . $i . '_type'] = '_ignore'; + $array['destination_account_' . $i . '_number'] = 'account-number'; + } + + $array['budget_id'] = 'budget-id'; + $array['budget_name'] = 'budget-name'; + $array['category_id'] = 'category-id'; + $array['category_name'] = 'category-name'; + $array['bill_id'] = 'bill-id'; + $array['bill_name'] = 'bill-name'; + + return $array; + } + private function tempFile() { $this->fileName = $this->job->key . '-records.csv'; diff --git a/app/Handlers/Events/ConnectTransactionToPiggyBank.php b/app/Handlers/Events/ConnectTransactionToPiggyBank.php new file mode 100644 index 0000000000..55f362d67b --- /dev/null +++ b/app/Handlers/Events/ConnectTransactionToPiggyBank.php @@ -0,0 +1,68 @@ +'; + /** @var PiggyBankRepositoryInterface $repository */ + $repository = app(PiggyBankRepositoryInterface::class); + $transaction = $event->transaction; + $piggyBank = $repository->find($transaction['piggy_bank_id']); + + // valid piggy: + if (is_null($piggyBank->id)) { + return true; + } + $amount = strval($transaction['amount']); + // piggy bank account something with amount: + if ($transaction['source_account_id'] == $piggyBank->account_id) { + // if the source of this transaction is the same as the piggy bank, + // the money is being removed from the piggy bank. So the + // amount must be negative: + $amount = bcmul($amount, '-1'); + } + + $repetition = $piggyBank->currentRelevantRep(); + // add or remove the money from the piggy bank: + $newAmount = bcadd(strval($repetition->currentamount), $amount); + $repetition->currentamount = $newAmount; + $repetition->save(); + + // now generate a piggy bank event: + PiggyBankEvent::create(['piggy_bank_id' => $piggyBank->id, 'date' => $transaction['date'], 'amount' => $newAmount]); + + return true; + } + + +} diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index e5311019ab..1b8ec81140 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -53,11 +53,13 @@ class SplitController extends Controller $accountRepository = app('FireflyIII\Repositories\Account\AccountRepositoryInterface'); $currencyRepository = app('FireflyIII\Repositories\Currency\CurrencyRepositoryInterface'); $budgetRepository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); + $piggyRepository = app('FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface'); $assetAccounts = ExpandedForm::makeSelectList($accountRepository->getAccountsByType(['Default account', 'Asset account'])); $sessionData = session('journal-data', []); $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); $currencies = ExpandedForm::makeSelectList($currencyRepository->get()); $budgets = ExpandedForm::makeSelectListWithEmpty($budgetRepository->getActiveBudgets()); + $piggyBanks = ExpandedForm::makeSelectListWithEmpty($piggyRepository->getPiggyBanksWithAmount()); $subTitle = trans('form.add_new_' . $sessionData['what']); $subTitleIcon = 'fa-plus'; $preFilled = [ @@ -74,7 +76,8 @@ class SplitController extends Controller ]; return view( - 'split.journals.create', compact('journal', 'subTitle', 'subTitleIcon', 'preFilled', 'assetAccounts', 'currencies', 'budgets', 'uploadSize') + 'split.journals.create', + compact('journal', 'piggyBanks', 'subTitle', 'subTitleIcon', 'preFilled', 'assetAccounts', 'currencies', 'budgets', 'uploadSize') ); } @@ -125,7 +128,6 @@ class SplitController extends Controller public function store(JournalInterface $repository, SplitJournalFormRequest $request, TransactionJournal $journal) { $data = $request->getSplitData(); - foreach ($data['transactions'] as $transaction) { $repository->storeTransaction($journal, $transaction); } @@ -245,7 +247,7 @@ class SplitController extends Controller $destinationName = $request->old('destination_account_name')[$index] ?? $transaction->account->name; // any transfer not from the source: - if (($journal->isWithdrawal() || $journal->isDeposit()) && $transaction->account_id !== $sourceAccounts->first()->id) { + if ($transaction->account_id !== $sourceAccounts->first()->id) { $array['description'][] = $description; $array['destination_account_id'][] = $transaction->account_id; $array['destination_account_name'][] = $destinationName; diff --git a/app/Http/Requests/SplitJournalFormRequest.php b/app/Http/Requests/SplitJournalFormRequest.php index 09868f4316..4438c43ff0 100644 --- a/app/Http/Requests/SplitJournalFormRequest.php +++ b/app/Http/Requests/SplitJournalFormRequest.php @@ -59,6 +59,9 @@ class SplitJournalFormRequest extends Request 'category' => $this->get('category')[$index] ?? '', 'source_account_id' => intval($this->get('journal_source_account_id')), 'source_account_name' => $this->get('journal_source_account_name'), + 'piggy_bank_id' => isset($this->get('piggy_bank_id')[$index]) + ? intval($this->get('piggy_bank_id')[$index]) + : 0, 'destination_account_id' => isset($this->get('destination_account_id')[$index]) ? intval($this->get('destination_account_id')[$index]) : intval($this->get('journal_destination_account_id')), diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 3124a4a7f4..6b1b58f54f 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -30,12 +30,16 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\FireRulesForUpdate', ], + 'FireflyIII\Events\BudgetLimitStored' => [ 'FireflyIII\Handlers\Events\BudgetLimitEventHandler@store', ], 'FireflyIII\Events\BudgetLimitUpdated' => [ 'FireflyIII\Handlers\Events\BudgetLimitEventHandler@update', ], + 'FireflyIII\Events\TransactionStored' => [ + 'FireflyIII\Handlers\Events\ConnectTransactionToPiggyBank', + ], 'FireflyIII\Events\TransactionJournalStored' => [ 'FireflyIII\Handlers\Events\ScanForBillsAfterStore', 'FireflyIII\Handlers\Events\ConnectJournalToPiggyBank', diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index fca7a5dbac..fe61b30071 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -18,6 +18,7 @@ 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; @@ -167,6 +168,18 @@ class JournalRepository implements JournalRepositoryInterface 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); diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index a9d7950ac7..a2a48803f0 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -57,6 +57,21 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return true; } + /** + * @param int $piggyBankid + * + * @return PiggyBank + */ + public function find(int $piggyBankid): PiggyBank + { + $piggyBank = $this->user->piggyBanks()->where('piggy_banks.id', $piggyBankid)->first(['piggy_banks.*']); + if (!is_null($piggyBank)) { + return $piggyBank; + } + + return new PiggyBank(); + } + /** * @param PiggyBank $piggyBank * diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 7b1e892dc1..f45d132b2c 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -34,6 +34,13 @@ interface PiggyBankRepositoryInterface */ public function destroy(PiggyBank $piggyBank): bool; + /** + * @param int $piggyBankid + * + * @return PiggyBank + */ + public function find(int $piggyBankid): PiggyBank; + /** * Get all events. * diff --git a/resources/views/split/journals/create.twig b/resources/views/split/journals/create.twig index fabd897597..90acdc41f4 100644 --- a/resources/views/split/journals/create.twig +++ b/resources/views/split/journals/create.twig @@ -93,6 +93,9 @@ {{ trans('list.budget') }} {% endif %} {{ trans('list.category') }} + {% if preFilled.what == 'transfer' %} + {{ trans('list.piggy_bank') }} + {% endif %} @@ -139,6 +142,12 @@ + {% if preFilled.what == 'transfer' %} + + + {{ Form.select('piggy_bank_id[]',piggyBanks, preFilled.piggy_bank_id[index], {class: 'form-control'}) }} + + {% endif %} {% endfor %} diff --git a/storage/database/seed.split.json b/storage/database/seed.split.json new file mode 100644 index 0000000000..7a8df1ea23 --- /dev/null +++ b/storage/database/seed.split.json @@ -0,0 +1,301 @@ +{ + "users": [ + { + "email": "thegrumpydictator@gmail.com", + "password": "james" + } + ], + "roles": [ + { + "user_id": 1, + "role": 1 + } + ], + "accounts": [ + { + "user_id": 1, + "account_type_id": 3, + "name": "Checking Account", + "iban": "NL11XOLA6707795988" + }, + { + "user_id": 1, + "account_type_id": 3, + "name": "Alternate", + "iban": "NL40UKBK3619908726" + }, + { + "user_id": 1, + "account_type_id": 4, + "name": "SixtyFive" + }, + { + "user_id": 1, + "account_type_id": 4, + "name": "EightyFour" + }, + { + "user_id": 1, + "account_type_id": 4, + "name": "Fiftyone" + }, + { + "user_id": 1, + "account_type_id": 5, + "name": "Work SixtyFive" + }, + { + "user_id": 1, + "account_type_id": 5, + "name": "Work EightyFour" + }, + { + "user_id": 1, + "account_type_id": 5, + "name": "Work Fiftyone" + } + ], + "account-meta": [ + { + "account_id": 1, + "name": "accountRole", + "data": "\"defaultAsset\"" + }, + { + "account_id": 2, + "name": "accountRole", + "data": "\"defaultAsset\"" + } + ], + "bills": [], + "budgets": [ + { + "name": "Groceries", + "user_id": 1 + }, + { + "name": "Bills", + "user_id": 1 + }, + { + "name": "Car", + "user_id": 1 + } + ], + "budget-limits": [], + "monthly-limits": [ + { + "budget_id": 1, + "amount_min": 200, + "amount_max": 200 + }, + { + "budget_id": 2, + "amount_min": 1000, + "amount_max": 1000 + }, + { + "budget_id": 3, + "amount_min": 200, + "amount_max": 200 + } + ], + "categories": [ + { + "name": "Daily groceries", + "user_id": 1 + }, + { + "name": "Car", + "user_id": 1 + }, + { + "name": "Reimbursements", + "user_id": 1 + } + ], + "piggy-banks": [ + { + "account_id": 2, + "name": "New camera", + "targetamount": 1000, + "startdate": "2015-04-01", + "reminder_skip": 0, + "remind_me": 0, + "order": 1, + "currentamount": 0 + }, + { + "account_id": 2, + "name": "New phone", + "targetamount": 600, + "startdate": "2015-04-01", + "reminder_skip": 0, + "remind_me": 0, + "order": 2, + "currentamount": 0 + }, + { + "account_id": 2, + "name": "New couch", + "targetamount": 500, + "startdate": "2015-04-01", + "reminder_skip": 0, + "remind_me": 0, + "order": 3, + "currentamount": 0 + } + ], + "piggy-events": [], + "rule-groups": [], + "rules": [], + "rule-triggers": [], + "rule-actions": [], + "tags": [], + "monthly-deposits": [], + "monthly-transfers": [], + "monthly-withdrawals": [], + "attachments": [], + "multi-withdrawals": [ + { + "user_id": 1, + "date": "2016-03-12", + "description": "Even multi-withdrawal (50, 50)", + "destination_ids": [ + 3, + 4 + ], + "source_id": 1, + "amounts": [ + 50, + 50 + ], + "category_ids": [ + 1, + 2, + 3 + ], + "budget_ids": [ + 1, + 2, + 3 + ] + }, + { + "user_id": 1, + "date": "2016-05-12", + "description": "Uneven multi-withdrawal (15,34,51)", + "destination_ids": [ + 3, + 4, + 5 + ], + "source_id": 1, + "amounts": [ + 14, + 35, + 51 + ], + "category_ids": [ + 1, + 2, + 3 + ], + "budget_ids": [ + 1, + 2, + 3 + ] + } + ], + "multi-deposits": [ + { + "user_id": 1, + "date": "2016-03-02", + "description": "Even multi-deposit (50, 50)", + "source_ids": [ + 6, + 7 + ], + "destination_id": 1, + "amounts": [ + 50, + 50 + ], + "category_ids": [ + 1, + 2, + 3 + ] + }, + { + "user_id": 1, + "date": "2016-05-02", + "description": "Uneven multi-deposit (15,34,51)", + "source_ids": [ + 6, + 7, + 8 + ], + "destination_id": 1, + "amounts": [ + 14, + 35, + 51 + ], + "category_ids": [ + 1, + 2, + 3 + ] + } + ], + "multi-transfers": [ + { + "user_id": 1, + "date": "2016-01-18", + "description": "Even multi-transfer (50, 50)", + "source_ids": [ + 1, + 1 + ], + "destination_ids": [ + 2, + 2 + ], + "amounts": [ + 50, + 50 + ], + "category_ids": [ + 1, + 2 + ] + }, + { + "user_id": 1, + "date": "2016-03-28", + "description": "Uneven multi-transfer (15,34,51)", + "source_ids": [ + 1, + 1, + 1 + ], + "destination_ids": [ + 2, + 2, + 2 + ], + "amounts": [ + 14, + 35, + 51 + ], + "category_ids": [ + 1, + 2, + 3 + ] + } + ] +} \ No newline at end of file