mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-25 21:16:47 +00:00
Multi account piggy banks.
This commit is contained in:
@@ -26,12 +26,14 @@ namespace FireflyIII\Http\Controllers\PiggyBank;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
@@ -51,7 +53,7 @@ class AmountController extends Controller
|
||||
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
app('view')->share('title', (string)trans('firefly.piggyBanks'));
|
||||
app('view')->share('title', (string) trans('firefly.piggyBanks'));
|
||||
app('view')->share('mainTitleIcon', 'fa-bullseye');
|
||||
|
||||
$this->piggyRepos = app(PiggyBankRepositoryInterface::class);
|
||||
@@ -69,16 +71,26 @@ class AmountController extends Controller
|
||||
*/
|
||||
public function add(PiggyBank $piggyBank)
|
||||
{
|
||||
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, today(config('app.timezone')));
|
||||
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank);
|
||||
$maxAmount = $leftOnAccount;
|
||||
if (0 !== bccomp($piggyBank->target_amount, '0')) {
|
||||
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
|
||||
$maxAmount = min($leftOnAccount, $leftToSave);
|
||||
$accounts = [];
|
||||
$total = '0';
|
||||
$totalSaved = $this->piggyRepos->getCurrentAmount($piggyBank);
|
||||
$leftToSave = bcsub($piggyBank->target_amount, $totalSaved);
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, today(config('app.timezone'))->endOfDay());
|
||||
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account);
|
||||
$maxAmount = 0 === bccomp($piggyBank->target_amount, '0') ? $leftToSave : min($leftOnAccount, $leftToSave);
|
||||
$accounts[] = [
|
||||
'account' => $account,
|
||||
'left_on_account' => $leftOnAccount,
|
||||
'saved_so_far' => $savedSoFar,
|
||||
'left_to_save' => $leftToSave,
|
||||
'max_amount' => $maxAmount,
|
||||
];
|
||||
$total = bcadd($total, $leftOnAccount);
|
||||
}
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
$total = (float) $total; // intentional float.
|
||||
|
||||
return view('piggy-banks.add', compact('piggyBank', 'maxAmount', 'currency'));
|
||||
return view('piggy-banks.add', compact('piggyBank', 'accounts', 'total'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,18 +101,24 @@ class AmountController extends Controller
|
||||
public function addMobile(PiggyBank $piggyBank)
|
||||
{
|
||||
/** @var Carbon $date */
|
||||
$date = session('end', today(config('app.timezone')));
|
||||
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $date);
|
||||
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank);
|
||||
$maxAmount = $leftOnAccount;
|
||||
|
||||
if (0 !== bccomp($piggyBank->target_amount, '0')) {
|
||||
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
|
||||
$maxAmount = min($leftOnAccount, $leftToSave);
|
||||
$date = session('end', today(config('app.timezone')));
|
||||
$accounts = [];
|
||||
$total = '0';
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, $date);
|
||||
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account);
|
||||
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
|
||||
$accounts[] = [
|
||||
'account' => $account,
|
||||
'left_on_account' => $leftOnAccount,
|
||||
'saved_so_far' => $savedSoFar,
|
||||
'left_to_save' => $leftToSave,
|
||||
'max_amount' => 0 === bccomp($piggyBank->target_amount, '0') ? $leftOnAccount : min($leftOnAccount, $leftToSave),
|
||||
];
|
||||
$total = bcadd($total, $leftOnAccount);
|
||||
}
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
|
||||
return view('piggy-banks.add-mobile', compact('piggyBank', 'maxAmount', 'currency'));
|
||||
return view('piggy-banks.add-mobile', compact('piggyBank', 'total', 'accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,32 +126,47 @@ class AmountController extends Controller
|
||||
*/
|
||||
public function postAdd(Request $request, PiggyBank $piggyBank): RedirectResponse
|
||||
{
|
||||
$amount = $request->get('amount') ?? '0';
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
// if amount is negative, make positive and continue:
|
||||
if (-1 === bccomp($amount, '0')) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
$data = $request->all();
|
||||
$amounts = $data['amount'] ?? [];
|
||||
$total = '0';
|
||||
Log::debug('Start with loop.');
|
||||
/** @var Account $account */
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$amount = (string) ($amounts[$account->id] ?? '0');
|
||||
if ('' === $amount || 0 === bccomp($amount, '0')) {
|
||||
continue;
|
||||
}
|
||||
if (-1 === bccomp($amount, '0')) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
}
|
||||
|
||||
// small check to see if the $amount is not more than the total "left to save" value
|
||||
$currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank);
|
||||
$leftToSave = 0 === bccomp($piggyBank->target_amount, '0') ? '0' : bcsub($piggyBank->target_amount, $currentAmount);
|
||||
if (bccomp($amount, $leftToSave) > 0 && 0 !== bccomp($leftToSave, '0')) {
|
||||
Log::debug(sprintf('Amount "%s" is more than left to save "%s". Using left to save.', $amount, $leftToSave));
|
||||
$amount = $leftToSave;
|
||||
}
|
||||
|
||||
$canAddAmount = $this->piggyRepos->canAddAmount($piggyBank, $account, $amount);
|
||||
if ($canAddAmount) {
|
||||
$this->piggyRepos->addAmount($piggyBank, $account, $amount);
|
||||
$total = bcadd($total, $amount);
|
||||
}
|
||||
$piggyBank->refresh();
|
||||
}
|
||||
if ($this->piggyRepos->canAddAmount($piggyBank, $amount)) {
|
||||
$this->piggyRepos->addAmount($piggyBank, $amount);
|
||||
session()->flash(
|
||||
'success',
|
||||
(string)trans(
|
||||
'firefly.added_amount_to_piggy',
|
||||
['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => $piggyBank->name]
|
||||
)
|
||||
);
|
||||
if (0 !== bccomp($total, '0')) {
|
||||
session()->flash('success', (string) trans('firefly.added_amount_to_piggy', ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => $piggyBank->name]));
|
||||
app('preferences')->mark();
|
||||
|
||||
return redirect(route('piggy-banks.index'));
|
||||
}
|
||||
|
||||
app('log')->error('Cannot add '.$amount.' because canAddAmount returned false.');
|
||||
app('log')->error(sprintf('Cannot add %s because canAddAmount returned false.', $total));
|
||||
session()->flash(
|
||||
'error',
|
||||
(string)trans(
|
||||
(string) trans(
|
||||
'firefly.cannot_add_amount_piggy',
|
||||
['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]
|
||||
['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => e($piggyBank->name)]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -145,32 +178,43 @@ class AmountController extends Controller
|
||||
*/
|
||||
public function postRemove(Request $request, PiggyBank $piggyBank): RedirectResponse
|
||||
{
|
||||
$amount = $request->get('amount') ?? '0';
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
// if amount is negative, make positive and continue:
|
||||
if (-1 === bccomp($amount, '0')) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
$amounts = $request->get('amount') ?? [];
|
||||
if (!is_array($amounts)) {
|
||||
$amounts = [];
|
||||
}
|
||||
if ($this->piggyRepos->canRemoveAmount($piggyBank, $amount)) {
|
||||
$this->piggyRepos->removeAmount($piggyBank, $amount);
|
||||
$total = '0';
|
||||
/** @var Account $account */
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$amount = (string) ($amounts[$account->id] ?? '0');
|
||||
if ('' === $amount || 0 === bccomp($amount, '0')) {
|
||||
continue;
|
||||
}
|
||||
if (-1 === bccomp($amount, '0')) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
}
|
||||
if ($this->piggyRepos->canRemoveAmount($piggyBank, $account, $amount)) {
|
||||
$this->piggyRepos->removeAmount($piggyBank, $account, $amount);
|
||||
$total = bcadd($total, $amount);
|
||||
}
|
||||
}
|
||||
if (0 !== bccomp($total, '0')) {
|
||||
session()->flash(
|
||||
'success',
|
||||
(string)trans(
|
||||
(string) trans(
|
||||
'firefly.removed_amount_from_piggy',
|
||||
['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => $piggyBank->name]
|
||||
['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => $piggyBank->name]
|
||||
)
|
||||
);
|
||||
app('preferences')->mark();
|
||||
|
||||
return redirect(route('piggy-banks.index'));
|
||||
}
|
||||
$amount = (string)$request->get('amount');
|
||||
|
||||
session()->flash(
|
||||
'error',
|
||||
(string)trans(
|
||||
(string) trans(
|
||||
'firefly.cannot_remove_from_piggy',
|
||||
['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]
|
||||
['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => e($piggyBank->name)]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -184,10 +228,14 @@ class AmountController extends Controller
|
||||
*/
|
||||
public function remove(PiggyBank $piggyBank)
|
||||
{
|
||||
$repetition = $this->piggyRepos->getRepetition($piggyBank);
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
|
||||
return view('piggy-banks.remove', compact('piggyBank', 'repetition', 'currency'));
|
||||
$accounts = [];
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$accounts[] = [
|
||||
'account' => $account,
|
||||
'saved_so_far' => $this->piggyRepos->getCurrentAmount($piggyBank, $account),
|
||||
];
|
||||
}
|
||||
return view('piggy-banks.remove', compact('piggyBank', 'accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,9 +245,14 @@ class AmountController extends Controller
|
||||
*/
|
||||
public function removeMobile(PiggyBank $piggyBank)
|
||||
{
|
||||
$repetition = $this->piggyRepos->getRepetition($piggyBank);
|
||||
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency();
|
||||
$accounts = [];
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$accounts[] = [
|
||||
'account' => $account,
|
||||
'saved_so_far' => $this->piggyRepos->getCurrentAmount($piggyBank, $account),
|
||||
];
|
||||
}
|
||||
|
||||
return view('piggy-banks.remove-mobile', compact('piggyBank', 'repetition', 'currency'));
|
||||
return view('piggy-banks.remove-mobile', compact('piggyBank', 'accounts'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,22 +79,21 @@ class EditController extends Controller
|
||||
// Flash some data to fill the form.
|
||||
$targetDate = $piggyBank->target_date?->format('Y-m-d');
|
||||
$startDate = $piggyBank->start_date?->format('Y-m-d');
|
||||
$currency = $this->accountRepository->getAccountCurrency($piggyBank->account);
|
||||
if (null === $currency) {
|
||||
$currency = app('amount')->getDefaultCurrency();
|
||||
}
|
||||
|
||||
$preFilled = [
|
||||
'name' => $piggyBank->name,
|
||||
'account_id' => $piggyBank->account_id,
|
||||
'targetamount' => app('steam')->bcround($piggyBank->target_amount, $currency->decimal_places),
|
||||
'targetdate' => $targetDate,
|
||||
'startdate' => $startDate,
|
||||
'target_amount' => app('steam')->bcround($piggyBank->target_amount, $piggyBank->transactionCurrency->decimal_places),
|
||||
'target_date' => $targetDate,
|
||||
'start_date' => $startDate,
|
||||
'accounts' => [],
|
||||
'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '',
|
||||
'notes' => null === $note ? '' : $note->text,
|
||||
];
|
||||
foreach($piggyBank->accounts as $account) {
|
||||
$preFilled['accounts'][] = $account->id;
|
||||
}
|
||||
if (0 === bccomp($piggyBank->target_amount, '0')) {
|
||||
$preFilled['targetamount'] = '';
|
||||
$preFilled['target_amount'] = '';
|
||||
}
|
||||
session()->flash('preFilled', $preFilled);
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ class ShowController extends Controller
|
||||
$subTitle = $piggyBank->name;
|
||||
$attachments = $this->piggyRepos->getAttachments($piggyBank);
|
||||
|
||||
|
||||
return view('piggy-banks.show', compact('piggyBank', 'events', 'subTitle', 'piggy', 'attachments'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Rules\IsValidPositiveAmount;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
@@ -44,15 +46,22 @@ class PiggyBankUpdateRequest extends FormRequest
|
||||
*/
|
||||
public function getPiggyBankData(): array
|
||||
{
|
||||
return [
|
||||
$accounts = $this->get('accounts');
|
||||
$data = [
|
||||
'name' => $this->convertString('name'),
|
||||
'startdate' => $this->getCarbonDate('startdate'),
|
||||
'account_id' => $this->convertInteger('account_id'),
|
||||
'targetamount' => trim($this->convertString('targetamount')),
|
||||
'targetdate' => $this->getCarbonDate('targetdate'),
|
||||
'start_date' => $this->getCarbonDate('start_date'),
|
||||
'target_amount' => trim($this->convertString('target_amount')),
|
||||
'target_date' => $this->getCarbonDate('target_date'),
|
||||
'notes' => $this->stringWithNewlines('notes'),
|
||||
'object_group_title' => $this->convertString('object_group'),
|
||||
];
|
||||
if (!is_array($accounts)) {
|
||||
$accounts = [];
|
||||
}
|
||||
foreach ($accounts as $item) {
|
||||
$data['accounts'][] = ['account_id' => (int) $item];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,21 +73,62 @@ class PiggyBankUpdateRequest extends FormRequest
|
||||
$piggy = $this->route()->parameter('piggyBank');
|
||||
|
||||
return [
|
||||
'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id),
|
||||
'account_id' => 'required|belongsToUser:accounts',
|
||||
'targetamount' => ['nullable', new IsValidPositiveAmount()],
|
||||
'startdate' => 'date',
|
||||
'targetdate' => 'date|nullable',
|
||||
'order' => 'integer|max:32768|min:1',
|
||||
'object_group' => 'min:0|max:255',
|
||||
'notes' => 'min:1|max:32768|nullable',
|
||||
'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id),
|
||||
'accounts' => 'required|array',
|
||||
'accounts.*' => 'required|belongsToUser:accounts',
|
||||
'target_amount' => ['nullable', new IsValidPositiveAmount()],
|
||||
'start_date' => 'date',
|
||||
'target_date' => 'date|nullable',
|
||||
'order' => 'integer|max:32768|min:1',
|
||||
'object_group' => 'min:0|max:255',
|
||||
'notes' => 'min:1|max:32768|nullable',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
{ // need to have more than one account.
|
||||
// accounts need to have the same currency or be multi-currency(?).
|
||||
$validator->after(
|
||||
function (Validator $validator): void {
|
||||
// validate start before end only if both are there.
|
||||
$data = $validator->getData();
|
||||
$currency = $this->getCurrencyFromData($data);
|
||||
if (array_key_exists('accounts', $data) && is_array($data['accounts'])) {
|
||||
$repository = app(AccountRepositoryInterface::class);
|
||||
$types = config('firefly.piggy_bank_account_types');
|
||||
foreach ($data['accounts'] as $value) {
|
||||
$accountId = (int) $value;
|
||||
$account = $repository->find($accountId);
|
||||
if (null !== $account) {
|
||||
// check currency here.
|
||||
$accountCurrency = $repository->getAccountCurrency($account);
|
||||
$isMultiCurrency = $repository->getMetaValue($account, 'is_multi_currency');
|
||||
if ($accountCurrency->id !== $currency->id && 'true' !== $isMultiCurrency) {
|
||||
$validator->errors()->add('accounts', trans('validation.invalid_account_currency'));
|
||||
}
|
||||
$type = $account->accountType->type;
|
||||
if (!in_array($type, $types, true)) {
|
||||
$validator->errors()->add('accounts', trans('validation.invalid_account_type'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if ($validator->fails()) {
|
||||
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
|
||||
}
|
||||
}
|
||||
private function getCurrencyFromData(array $data): TransactionCurrency
|
||||
{
|
||||
$currencyId = (int) ($data['transaction_currency_id'] ?? 0);
|
||||
$currency = TransactionCurrency::find($currencyId);
|
||||
if (null === $currency) {
|
||||
return app('amount')->getDefaultCurrency();
|
||||
}
|
||||
|
||||
return $currency;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user