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 @@