Multi account piggy banks.

This commit is contained in:
James Cole 2024-12-14 17:32:03 +01:00
parent fb6c67fa04
commit 6a62f781e9
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
24 changed files with 572 additions and 404 deletions

View File

@ -72,6 +72,7 @@ class UpgradeDatabase extends Command
'firefly-iii:create-group-memberships', 'firefly-iii:create-group-memberships',
'firefly-iii:upgrade-group-information', 'firefly-iii:upgrade-group-information',
'firefly-iii:upgrade-currency-preferences', 'firefly-iii:upgrade-currency-preferences',
'firefly-iii:upgrade-multi-piggies',
'firefly-iii:correct-database', 'firefly-iii:correct-database',
]; ];
$args = []; $args = [];

View File

@ -93,7 +93,7 @@ class UpgradeMultiPiggyBanks extends Command
{ {
$this->repository->setUser($piggyBank->account->user); $this->repository->setUser($piggyBank->account->user);
$this->accountRepository->setUser($piggyBank->account->user); $this->accountRepository->setUser($piggyBank->account->user);
$repetition = $this->repository->getRepetition($piggyBank); $repetition = $this->repository->getRepetition($piggyBank, true);
$currency = $this->accountRepository->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrencyByUserGroup($piggyBank->account->user->userGroup); $currency = $this->accountRepository->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrencyByUserGroup($piggyBank->account->user->userGroup);
// update piggy bank to have a currency. // update piggy bank to have a currency.

View File

@ -42,12 +42,22 @@ class PiggyBankFactory
public User $user { public User $user {
set(User $value) { set(User $value) {
$this->user = $value; $this->user = $value;
$this->currencyRepository->setUser($value);
$this->accountRepository->setUser($value);
$this->piggyBankRepository->setUser($value);
} }
} }
private CurrencyRepositoryInterface $currencyRepository; private CurrencyRepositoryInterface $currencyRepository;
private AccountRepositoryInterface $accountRepository; private AccountRepositoryInterface $accountRepository;
private PiggyBankRepositoryInterface $piggyBankRepository; private PiggyBankRepositoryInterface $piggyBankRepository;
public function __construct()
{
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->piggyBankRepository = app(PiggyBankRepositoryInterface::class);
}
/** /**
* Store a piggy bank or come back with an exception. * Store a piggy bank or come back with an exception.
* *
@ -56,12 +66,7 @@ class PiggyBankFactory
* @return PiggyBank * @return PiggyBank
*/ */
public function store(array $data): PiggyBank { public function store(array $data): PiggyBank {
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->piggyBankRepository = app(PiggyBankRepositoryInterface::class);
$this->currencyRepository->setUser($this->user);
$this->accountRepository->setUser($this->user);
$this->piggyBankRepository->setUser($this->user);
$piggyBankData =$data; $piggyBankData =$data;
// unset some fields // unset some fields
@ -202,14 +207,23 @@ class PiggyBankFactory
} }
private function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void { public function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void {
$toBeLinked = [];
/** @var array $info */ /** @var array $info */
foreach($accounts as $info) { foreach($accounts as $info) {
$account = $this->accountRepository->find((int)($info['account_id'] ?? 0)); $account = $this->accountRepository->find((int)($info['account_id'] ?? 0));
if(null === $account) { if(null === $account) {
continue; continue;
} }
$piggyBank->accounts()->syncWithoutDetaching([$account->id => ['current_amount' => $info['current_amount'] ?? '0']]); if(array_key_exists('current_amount',$info)) {
$toBeLinked[$account->id] = ['current_amount' => $info['current_amount']];
//$piggyBank->accounts()->syncWithoutDetaching([$account->id => ['current_amount' => $info['current_amount'] ?? '0']]);
}
if(!array_key_exists('current_amount', $info)) {
$toBeLinked[$account->id] = [];
//$piggyBank->accounts()->syncWithoutDetaching([$account->id]);
}
} }
$piggyBank->accounts()->sync($toBeLinked);
} }
} }

View File

@ -26,12 +26,14 @@ namespace FireflyIII\Http\Controllers\PiggyBank;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View; use Illuminate\View\View;
/** /**
@ -51,7 +53,7 @@ class AmountController extends Controller
$this->middleware( $this->middleware(
function ($request, $next) { 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'); app('view')->share('mainTitleIcon', 'fa-bullseye');
$this->piggyRepos = app(PiggyBankRepositoryInterface::class); $this->piggyRepos = app(PiggyBankRepositoryInterface::class);
@ -69,16 +71,26 @@ class AmountController extends Controller
*/ */
public function add(PiggyBank $piggyBank) public function add(PiggyBank $piggyBank)
{ {
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, today(config('app.timezone'))); $accounts = [];
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank); $total = '0';
$maxAmount = $leftOnAccount; $totalSaved = $this->piggyRepos->getCurrentAmount($piggyBank);
if (0 !== bccomp($piggyBank->target_amount, '0')) { $leftToSave = bcsub($piggyBank->target_amount, $totalSaved);
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); foreach ($piggyBank->accounts as $account) {
$maxAmount = min($leftOnAccount, $leftToSave); $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) public function addMobile(PiggyBank $piggyBank)
{ {
/** @var Carbon $date */ /** @var Carbon $date */
$date = session('end', today(config('app.timezone'))); $date = session('end', today(config('app.timezone')));
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $date); $accounts = [];
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank); $total = '0';
$maxAmount = $leftOnAccount; foreach ($piggyBank->accounts as $account) {
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, $date);
if (0 !== bccomp($piggyBank->target_amount, '0')) { $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account);
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$maxAmount = min($leftOnAccount, $leftToSave); $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 public function postAdd(Request $request, PiggyBank $piggyBank): RedirectResponse
{ {
$amount = $request->get('amount') ?? '0'; $data = $request->all();
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); $amounts = $data['amount'] ?? [];
// if amount is negative, make positive and continue: $total = '0';
if (-1 === bccomp($amount, '0')) { Log::debug('Start with loop.');
$amount = bcmul($amount, '-1'); /** @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)) { if (0 !== bccomp($total, '0')) {
$this->piggyRepos->addAmount($piggyBank, $amount); session()->flash('success', (string) trans('firefly.added_amount_to_piggy', ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => $piggyBank->name]));
session()->flash(
'success',
(string)trans(
'firefly.added_amount_to_piggy',
['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => $piggyBank->name]
)
);
app('preferences')->mark(); app('preferences')->mark();
return redirect(route('piggy-banks.index')); return redirect(route('piggy-banks.index'));
} }
app('log')->error(sprintf('Cannot add %s because canAddAmount returned false.', $total));
app('log')->error('Cannot add '.$amount.' because canAddAmount returned false.');
session()->flash( session()->flash(
'error', 'error',
(string)trans( (string) trans(
'firefly.cannot_add_amount_piggy', '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 public function postRemove(Request $request, PiggyBank $piggyBank): RedirectResponse
{ {
$amount = $request->get('amount') ?? '0'; $amounts = $request->get('amount') ?? [];
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); if (!is_array($amounts)) {
// if amount is negative, make positive and continue: $amounts = [];
if (-1 === bccomp($amount, '0')) {
$amount = bcmul($amount, '-1');
} }
if ($this->piggyRepos->canRemoveAmount($piggyBank, $amount)) { $total = '0';
$this->piggyRepos->removeAmount($piggyBank, $amount); /** @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( session()->flash(
'success', 'success',
(string)trans( (string) trans(
'firefly.removed_amount_from_piggy', '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(); app('preferences')->mark();
return redirect(route('piggy-banks.index')); return redirect(route('piggy-banks.index'));
} }
$amount = (string)$request->get('amount');
session()->flash( session()->flash(
'error', 'error',
(string)trans( (string) trans(
'firefly.cannot_remove_from_piggy', '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) public function remove(PiggyBank $piggyBank)
{ {
$repetition = $this->piggyRepos->getRepetition($piggyBank); $accounts = [];
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); foreach ($piggyBank->accounts as $account) {
$accounts[] = [
return view('piggy-banks.remove', compact('piggyBank', 'repetition', 'currency')); '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) public function removeMobile(PiggyBank $piggyBank)
{ {
$repetition = $this->piggyRepos->getRepetition($piggyBank); $accounts = [];
$currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); 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'));
} }
} }

View File

@ -79,22 +79,21 @@ class EditController extends Controller
// Flash some data to fill the form. // Flash some data to fill the form.
$targetDate = $piggyBank->target_date?->format('Y-m-d'); $targetDate = $piggyBank->target_date?->format('Y-m-d');
$startDate = $piggyBank->start_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 = [ $preFilled = [
'name' => $piggyBank->name, 'name' => $piggyBank->name,
'account_id' => $piggyBank->account_id, 'target_amount' => app('steam')->bcround($piggyBank->target_amount, $piggyBank->transactionCurrency->decimal_places),
'targetamount' => app('steam')->bcround($piggyBank->target_amount, $currency->decimal_places), 'target_date' => $targetDate,
'targetdate' => $targetDate, 'start_date' => $startDate,
'startdate' => $startDate, 'accounts' => [],
'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '', 'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '',
'notes' => null === $note ? '' : $note->text, 'notes' => null === $note ? '' : $note->text,
]; ];
foreach($piggyBank->accounts as $account) {
$preFilled['accounts'][] = $account->id;
}
if (0 === bccomp($piggyBank->target_amount, '0')) { if (0 === bccomp($piggyBank->target_amount, '0')) {
$preFilled['targetamount'] = ''; $preFilled['target_amount'] = '';
} }
session()->flash('preFilled', $preFilled); session()->flash('preFilled', $preFilled);

View File

@ -83,6 +83,7 @@ class ShowController extends Controller
$subTitle = $piggyBank->name; $subTitle = $piggyBank->name;
$attachments = $this->piggyRepos->getAttachments($piggyBank); $attachments = $this->piggyRepos->getAttachments($piggyBank);
return view('piggy-banks.show', compact('piggyBank', 'events', 'subTitle', 'piggy', 'attachments')); return view('piggy-banks.show', compact('piggyBank', 'events', 'subTitle', 'piggy', 'attachments'));
} }
} }

View File

@ -24,6 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\Http\Requests; namespace FireflyIII\Http\Requests;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
@ -44,15 +46,22 @@ class PiggyBankUpdateRequest extends FormRequest
*/ */
public function getPiggyBankData(): array public function getPiggyBankData(): array
{ {
return [ $accounts = $this->get('accounts');
$data = [
'name' => $this->convertString('name'), 'name' => $this->convertString('name'),
'startdate' => $this->getCarbonDate('startdate'), 'start_date' => $this->getCarbonDate('start_date'),
'account_id' => $this->convertInteger('account_id'), 'target_amount' => trim($this->convertString('target_amount')),
'targetamount' => trim($this->convertString('targetamount')), 'target_date' => $this->getCarbonDate('target_date'),
'targetdate' => $this->getCarbonDate('targetdate'),
'notes' => $this->stringWithNewlines('notes'), 'notes' => $this->stringWithNewlines('notes'),
'object_group_title' => $this->convertString('object_group'), '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'); $piggy = $this->route()->parameter('piggyBank');
return [ return [
'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id), 'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id),
'account_id' => 'required|belongsToUser:accounts', 'accounts' => 'required|array',
'targetamount' => ['nullable', new IsValidPositiveAmount()], 'accounts.*' => 'required|belongsToUser:accounts',
'startdate' => 'date', 'target_amount' => ['nullable', new IsValidPositiveAmount()],
'targetdate' => 'date|nullable', 'start_date' => 'date',
'order' => 'integer|max:32768|min:1', 'target_date' => 'date|nullable',
'object_group' => 'min:0|max:255', 'order' => 'integer|max:32768|min:1',
'notes' => 'min:1|max:32768|nullable', 'object_group' => 'min:0|max:255',
'notes' => 'min:1|max:32768|nullable',
]; ];
} }
public function withValidator(Validator $validator): void 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()) { if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); 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;
}
} }

View File

@ -32,6 +32,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;
@ -159,9 +160,9 @@ class Account extends Model
return $this->morphToMany(ObjectGroup::class, 'object_groupable'); return $this->morphToMany(ObjectGroup::class, 'object_groupable');
} }
public function piggyBanks(): HasMany public function piggyBanks(): BelongsToMany
{ {
return $this->hasMany(PiggyBank::class); return $this->belongsToMany(PiggyBank::class);
} }
public function scopeAccountTypeIn(EloquentBuilder $query, array $types): void public function scopeAccountTypeIn(EloquentBuilder $query, array $types): void

View File

@ -27,12 +27,14 @@ namespace FireflyIII\Repositories\PiggyBank;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Factory\PiggyBankFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Note; use FireflyIII\Models\Note;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\PiggyBankRepetition;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use Illuminate\Support\Facades\Log;
/** /**
* Trait ModifiesPiggyBanks * Trait ModifiesPiggyBanks
@ -55,30 +57,42 @@ trait ModifiesPiggyBanks
} }
} }
public function removeAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool public function removeAmount(PiggyBank $piggyBank,Account $account, string $amount, ?TransactionJournal $journal = null): bool
{ {
$repetition = $this->getRepetition($piggyBank); $currentAmount = $this->getCurrentAmount($piggyBank, $account);
if (null === $repetition) { $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
return false; $pivot->current_amount = bcsub($currentAmount, $amount);
} $pivot->save();
$repetition->current_amount = bcsub($repetition->current_amount, $amount);
$repetition->save();
app('log')->debug('addAmount [a]: Trigger change for negative amount.'); app('log')->debug('removeAmount [a]: Trigger change for negative amount.');
event(new ChangedAmount($piggyBank, bcmul($amount, '-1'), $journal, null)); event(new ChangedAmount($piggyBank, bcmul($amount, '-1'), $journal, null));
return true; return true;
} }
public function addAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool public function removeAmountFromAll(PiggyBank $piggyBank, string $amount): void
{ {
$repetition = $this->getRepetition($piggyBank); foreach($piggyBank->accounts as $account) {
if (null === $repetition) { $current = $account->pivot->current_amount;
return false; // if this account contains more than the amount, remove the amount and return.
if (1 === bccomp($current, $amount)) {
$this->removeAmount($piggyBank, $account, $amount);
return;
}
// if this account contains less than the amount, remove the current amount, update the amount and continue.
if (bccomp($current, $amount) < 1) {
$this->removeAmount($piggyBank, $account, $current);
$amount = bcsub($amount, $current);
}
} }
$currentAmount = $repetition->current_amount ?? '0'; }
$repetition->current_amount = bcadd($currentAmount, $amount);
$repetition->save(); public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcadd($currentAmount, $amount);
$pivot->save();
app('log')->debug('addAmount [b]: Trigger change for positive amount.'); app('log')->debug('addAmount [b]: Trigger change for positive amount.');
event(new ChangedAmount($piggyBank, $amount, $journal, null)); event(new ChangedAmount($piggyBank, $amount, $journal, null));
@ -86,37 +100,36 @@ trait ModifiesPiggyBanks
return true; return true;
} }
public function canAddAmount(PiggyBank $piggyBank, string $amount): bool public function canAddAmount(PiggyBank $piggyBank, Account $account, string $amount): bool
{ {
$today = today(config('app.timezone')); Log::debug('Now in canAddAmount');
$leftOnAccount = $this->leftOnAccount($piggyBank, $today); $today = today(config('app.timezone'))->endOfDay();
$savedSoFar = $this->getRepetition($piggyBank)->current_amount; $leftOnAccount = $this->leftOnAccount($piggyBank, $account, $today);
$savedSoFar = $this->getCurrentAmount($piggyBank);
$maxAmount = $leftOnAccount; $maxAmount = $leftOnAccount;
$leftToSave = null;
app('log')->debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d H:i:s')));
app('log')->debug(sprintf('Saved so far: %s', $savedSoFar));
if (0 !== bccomp($piggyBank->target_amount, '0')) { if (0 !== bccomp($piggyBank->target_amount, '0')) {
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
app('log')->debug(sprintf('Left to save: %s', $leftToSave));
app('log')->debug(sprintf('Maximum amount: %s', $maxAmount));
} }
$compare = bccomp($amount, $maxAmount); $compare = bccomp($amount, $maxAmount);
$result = $compare <= 0; $result = $compare <= 0;
app('log')->debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d'))); app('log')->debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
app('log')->debug(sprintf('Saved so far: %s', $savedSoFar));
app('log')->debug(sprintf('Left to save: %s', $leftToSave));
app('log')->debug(sprintf('Maximum amount: %s', $maxAmount));
app('log')->debug(sprintf('Compare <= 0? %d, so %s', $compare, var_export($result, true)));
return $result; return $result;
} }
public function canRemoveAmount(PiggyBank $piggyBank, string $amount): bool public function canRemoveAmount(PiggyBank $piggyBank, Account $account, string $amount): bool
{ {
$repetition = $this->getRepetition($piggyBank); $savedSoFar = $this->getCurrentAmount($piggyBank, $account);
if (null === $repetition) {
return false;
}
$savedSoFar = $repetition->current_amount;
return bccomp($amount, $savedSoFar) <= 0; return bccomp($amount, $savedSoFar) <= 0;
} }
@ -244,17 +257,24 @@ trait ModifiesPiggyBanks
$this->setOrder($piggyBank, $newOrder); $this->setOrder($piggyBank, $newOrder);
} }
// update the accounts
$factory = new PiggyBankFactory();
$factory->user = $this->user;
$factory->linkToAccountIds($piggyBank, $data['accounts']);
// if the piggy bank is now smaller than the current relevant rep, // if the piggy bank is now smaller than the current relevant rep,
// remove money from the rep. // remove money from the rep.
$repetition = $this->getRepetition($piggyBank); $currentAmount = $this->getCurrentAmount($piggyBank);
if (null !== $repetition && $repetition->current_amount > $piggyBank->target_amount && 0 !== bccomp($piggyBank->target_amount, '0')) { if (1 === bccomp($currentAmount, '100') && 0 !== bccomp($piggyBank->target_amount, '0')) {
$difference = bcsub($piggyBank->target_amount, $repetition->current_amount); $difference = bcsub($piggyBank->target_amount, $currentAmount);
// an amount will be removed, create "negative" event: // an amount will be removed, create "negative" event:
event(new ChangedAmount($piggyBank, $difference, null, null)); event(new ChangedAmount($piggyBank, $difference, null, null));
$repetition->current_amount = $piggyBank->target_amount; // question is, from which account(s) to remove the difference?
$repetition->save(); // solution: just start from the top until there is no more money left to remove.
$this->removeAmountFromAll($piggyBank, app('steam')->positive($difference));
} }
// update using name: // update using name:
@ -295,22 +315,19 @@ trait ModifiesPiggyBanks
if (array_key_exists('name', $data) && '' !== $data['name']) { if (array_key_exists('name', $data) && '' !== $data['name']) {
$piggyBank->name = $data['name']; $piggyBank->name = $data['name'];
} }
if (array_key_exists('account_id', $data) && 0 !== $data['account_id']) { if (array_key_exists('target_amount', $data) && '' !== $data['target_amount']) {
$piggyBank->account_id = (int)$data['account_id']; $piggyBank->target_amount = $data['target_amount'];
} }
if (array_key_exists('targetamount', $data) && '' !== $data['targetamount']) { if (array_key_exists('target_amount', $data) && '' === $data['target_amount']) {
$piggyBank->target_amount = $data['targetamount'];
}
if (array_key_exists('targetamount', $data) && '' === $data['targetamount']) {
$piggyBank->target_amount = '0'; $piggyBank->target_amount = '0';
} }
if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) { if (array_key_exists('target_date', $data) && '' !== $data['target_date']) {
$piggyBank->target_date = $data['targetdate']; $piggyBank->target_date = $data['target_date'];
$piggyBank->target_date_tz = $data['targetdate']?->format('e'); $piggyBank->target_date_tz = $data['target_date']?->format('e');
} }
if (array_key_exists('startdate', $data)) { if (array_key_exists('start_date', $data)) {
$piggyBank->start_date = $data['startdate']; $piggyBank->start_date = $data['start_date'];
$piggyBank->start_date_tz = $data['targetdate']?->format('e'); $piggyBank->start_date_tz = $data['target_date']?->format('e');
} }
$piggyBank->save(); $piggyBank->save();

View File

@ -26,6 +26,7 @@ namespace FireflyIII\Repositories\PiggyBank;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Factory\PiggyBankFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Attachment; use FireflyIII\Models\Attachment;
use FireflyIII\Models\Note; use FireflyIII\Models\Note;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
@ -95,7 +96,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
public function getAttachments(PiggyBank $piggyBank): Collection public function getAttachments(PiggyBank $piggyBank): Collection
{ {
$set = $piggyBank->attachments()->get(); $set = $piggyBank->attachments()->get();
/** @var \Storage $disk */ /** @var \Storage $disk */
$disk = \Storage::disk('upload'); $disk = \Storage::disk('upload');
@ -114,22 +115,28 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
/** /**
* Get current amount saved in piggy bank. * Get current amount saved in piggy bank.
*/ */
public function getCurrentAmount(PiggyBank $piggyBank): string public function getCurrentAmount(PiggyBank $piggyBank, ?Account $account = null): string
{ {
$sum = '0'; $sum = '0';
foreach ($piggyBank->accounts as $account) { foreach ($piggyBank->accounts as $current) {
$amount = (string) $account->pivot->current_amount; if(null !== $account && $account->id !== $current->id) {
continue;
}
$amount = (string) $current->pivot->current_amount;
$amount = '' === $amount ? '0' : $amount; $amount = '' === $amount ? '0' : $amount;
$sum = bcadd($sum, $amount); $sum = bcadd($sum, $amount);
} }
Log::debug(sprintf('Current amount in piggy bank #%d ("%s") is %s', $piggyBank->id, $piggyBank->name, $sum));
return $sum; return $sum;
} }
public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition
{ {
throw new FireflyException('[b] Piggy bank repetitions are EOL.'); if (false === $overrule) {
throw new FireflyException('[b] Piggy bank repetitions are EOL.');
}
Log::warning('Piggy bank repetitions are EOL.');
return $piggyBank->piggyBankRepetitions()->first(); return $piggyBank->piggyBankRepetitions()->first();
} }
@ -148,15 +155,15 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
throw new FireflyException('[c] Piggy bank repetitions are EOL.'); throw new FireflyException('[c] Piggy bank repetitions are EOL.');
app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->id, $journal->id)); app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->id, $journal->id));
$operator = null; $operator = null;
$currency = null; $currency = null;
/** @var JournalRepositoryInterface $journalRepost */ /** @var JournalRepositoryInterface $journalRepost */
$journalRepost = app(JournalRepositoryInterface::class); $journalRepost = app(JournalRepositoryInterface::class);
$journalRepost->setUser($this->user); $journalRepost->setUser($this->user);
/** @var AccountRepositoryInterface $accountRepos */ /** @var AccountRepositoryInterface $accountRepos */
$accountRepos = app(AccountRepositoryInterface::class); $accountRepos = app(AccountRepositoryInterface::class);
$accountRepos->setUser($this->user); $accountRepos->setUser($this->user);
$defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup);
@ -165,10 +172,10 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->code)); app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->code));
/** @var Transaction $source */ /** @var Transaction $source */
$source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first();
/** @var Transaction $destination */ /** @var Transaction $destination */
$destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first(); $destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first();
// matches source, which means amount will be removed from piggy: // matches source, which means amount will be removed from piggy:
if ($source->account_id === $piggyBank->account_id) { if ($source->account_id === $piggyBank->account_id) {
@ -190,7 +197,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
} }
// currency of the account + the piggy bank currency are almost the same. // currency of the account + the piggy bank currency are almost the same.
// which amount from the transaction matches? // which amount from the transaction matches?
$amount = null; $amount = null;
if ((int) $source->transaction_currency_id === $currency->id) { if ((int) $source->transaction_currency_id === $currency->id) {
app('log')->debug('Use normal amount'); app('log')->debug('Use normal amount');
$amount = app('steam')->{$operator}($source->amount); // @phpstan-ignore-line $amount = app('steam')->{$operator}($source->amount); // @phpstan-ignore-line
@ -206,8 +213,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
} }
app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount)); app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount));
$room = bcsub($piggyBank->target_amount, $repetition->current_amount); $room = bcsub($piggyBank->target_amount, $repetition->current_amount);
$compare = bcmul($repetition->current_amount, '-1'); $compare = bcmul($repetition->current_amount, '-1');
if (0 === bccomp($piggyBank->target_amount, '0')) { if (0 === bccomp($piggyBank->target_amount, '0')) {
// amount is zero? then the "room" is positive amount of we wish to add or remove. // amount is zero? then the "room" is positive amount of we wish to add or remove.
@ -239,7 +246,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
return (string) $amount; return (string) $amount;
} }
public function setUser(null|Authenticatable|User $user): void public function setUser(null | Authenticatable | User $user): void
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;
@ -264,12 +271,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
{ {
$currency = app('amount')->getDefaultCurrency(); $currency = app('amount')->getDefaultCurrency();
$set = $this->getPiggyBanks(); $set = $this->getPiggyBanks();
/** @var PiggyBank $piggy */ /** @var PiggyBank $piggy */
foreach ($set as $piggy) { foreach ($set as $piggy) {
$currentAmount = $this->getRepetition($piggy)->current_amount ?? '0'; $currentAmount = $this->getRepetition($piggy)->current_amount ?? '0';
$piggy->name = $piggy->name.' ('.app('amount')->formatAnything($currency, $currentAmount, false).')'; $piggy->name = $piggy->name . ' (' . app('amount')->formatAnything($currency, $currentAmount, false) . ')';
} }
return $set; return $set;
@ -278,16 +285,15 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
public function getPiggyBanks(): Collection public function getPiggyBanks(): Collection
{ {
return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id')
->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id')
->where('accounts.user_id', auth()->user()->id) ->where('accounts.user_id', auth()->user()->id)
->with( ->with(
[ [
'account', 'account',
'objectGroups', 'objectGroups',
] ]
) )
->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']) ->orderBy('piggy_banks.order', 'ASC')->distinct()->get(['piggy_banks.*']);
;
} }
/** /**
@ -320,21 +326,22 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
/** /**
* Get for piggy account what is left to put in piggies. * Get for piggy account what is left to put in piggies.
*/ */
public function leftOnAccount(PiggyBank $piggyBank, Carbon $date): string public function leftOnAccount(PiggyBank $piggyBank, Account $account, Carbon $date): string
{ {
$balance = app('steam')->balanceIgnoreVirtual($piggyBank->account, $date); Log::debug(sprintf('leftOnAccount("%s","%s","%s")', $piggyBank->name, $account->name, $date->format('Y-m-d H:i:s')));
$balance = app('steam')->balanceConvertedIgnoreVirtual($account, $date, $piggyBank->transactionCurrency);
Log::debug(sprintf('Balance is: %s', $balance));
/** @var Collection $piggies */ /** @var Collection $piggies */
$piggies = $piggyBank->account->piggyBanks; $piggies = $account->piggyBanks;
/** @var PiggyBank $current */ /** @var PiggyBank $current */
foreach ($piggies as $current) { foreach ($piggies as $current) {
$repetition = $this->getRepetition($current); $amount = $this->getCurrentAmount($current, $account);
if (null !== $repetition) { $balance = bcsub($balance, $amount);
$balance = bcsub($balance, $repetition->current_amount); Log::debug(sprintf('Piggy bank: #%d with amount %s, balance is now %s', $current->id, $amount, $balance));
}
} }
Log::debug(sprintf('Final balance is: %s', $balance));
return $balance; return $balance;
} }
@ -345,8 +352,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
$search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query)); $search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query));
} }
$search->orderBy('piggy_banks.order', 'ASC') $search->orderBy('piggy_banks.order', 'ASC')
->orderBy('piggy_banks.name', 'ASC') ->orderBy('piggy_banks.name', 'ASC');
;
return $search->take($limit)->get(); return $search->take($limit)->get();
} }

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\PiggyBank;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\PiggyBankRepetition;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@ -37,13 +38,13 @@ use Illuminate\Support\Collection;
*/ */
interface PiggyBankRepositoryInterface interface PiggyBankRepositoryInterface
{ {
public function addAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool; public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool;
public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void; public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void;
public function canAddAmount(PiggyBank $piggyBank, string $amount): bool; public function canAddAmount(PiggyBank $piggyBank, Account $account, string $amount): bool;
public function canRemoveAmount(PiggyBank $piggyBank, string $amount): bool; public function canRemoveAmount(PiggyBank $piggyBank, Account $account, string $amount): bool;
/** /**
* Destroy piggy bank. * Destroy piggy bank.
@ -68,7 +69,10 @@ interface PiggyBankRepositoryInterface
/** /**
* Get current amount saved in piggy bank. * Get current amount saved in piggy bank.
*/ */
public function getCurrentAmount(PiggyBank $piggyBank): string; public function getCurrentAmount(PiggyBank $piggyBank, ?Account $account = null): string;
/**
* Get current amount saved in piggy bank.
*/
/** /**
* Get all events. * Get all events.
@ -97,7 +101,7 @@ interface PiggyBankRepositoryInterface
*/ */
public function getPiggyBanksWithAmount(): Collection; public function getPiggyBanksWithAmount(): Collection;
public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition; public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition;
/** /**
* Returns the suggested amount the user should save per month, or "". * Returns the suggested amount the user should save per month, or "".
@ -107,9 +111,10 @@ interface PiggyBankRepositoryInterface
/** /**
* Get for piggy account what is left to put in piggies. * Get for piggy account what is left to put in piggies.
*/ */
public function leftOnAccount(PiggyBank $piggyBank, Carbon $date): string; public function leftOnAccount(PiggyBank $piggyBank,Account $account, Carbon $date): string;
public function removeAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool; public function removeAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool;
public function removeAmountFromAll(PiggyBank $piggyBank, string $amount): void;
public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank; public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank;

View File

@ -44,34 +44,52 @@ class Steam
*/ */
public function balanceIgnoreVirtual(Account $account, Carbon $date): string public function balanceIgnoreVirtual(Account $account, Carbon $date): string
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); throw new FireflyException('Deprecated method balanceIgnoreVirtual.');
/** @var AccountRepositoryInterface $repository */ /** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class); $repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user); $repository->setUser($account->user);
$currencyId = (int) $repository->getMetaValue($account, 'currency_id'); $currencyId = (int) $repository->getMetaValue($account, 'currency_id');
$transactions = $account->transactions() $transactions = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', $currencyId) ->where('transactions.transaction_currency_id', $currencyId)
->get(['transactions.amount'])->toArray() ->get(['transactions.amount'])->toArray();
; $nativeBalance = $this->sumTransactions($transactions, 'amount');
$nativeBalance = $this->sumTransactions($transactions, 'amount');
// get all balances in foreign currency: // get all balances in foreign currency:
$transactions = $account->transactions() $transactions = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.foreign_currency_id', $currencyId) ->where('transactions.foreign_currency_id', $currencyId)
->where('transactions.transaction_currency_id', '!=', $currencyId) ->where('transactions.transaction_currency_id', '!=', $currencyId)
->get(['transactions.foreign_amount'])->toArray() ->get(['transactions.foreign_amount'])->toArray();
;
$foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount');
return bcadd($nativeBalance, $foreignBalance); return bcadd($nativeBalance, $foreignBalance);
} }
public function balanceConvertedIgnoreVirtual(Account $account, Carbon $date, TransactionCurrency $currency): string
{
$balance = $this->balanceConverted($account, $date, $currency);
$virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance;
// currency of account
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$accountCurrency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup);
if ($accountCurrency->id !== $currency->id && 0 !== bccomp($virtual, '0')) {
// convert amount to given currency.
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
$converter = new ExchangeRateConverter();
$virtual = $converter->convert($accountCurrency, $currency, $date, $virtual);
}
return bcsub($balance, $virtual);
}
public function sumTransactions(array $transactions, string $key): string public function sumTransactions(array $transactions, string $key): string
{ {
$sum = '0'; $sum = '0';
@ -96,7 +114,7 @@ class Steam
public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance-in-range'); $cache->addProperty('balance-in-range');
$cache->addProperty(null !== $currency ? $currency->id : 0); $cache->addProperty(null !== $currency ? $currency->id : 0);
@ -108,42 +126,41 @@ class Steam
$start->subDay(); $start->subDay();
$end->addDay(); $end->addDay();
$balances = []; $balances = [];
$formatted = $start->format('Y-m-d'); $formatted = $start->format('Y-m-d');
$startBalance = $this->balance($account, $start, $currency); $startBalance = $this->balance($account, $start, $currency);
$balances[$formatted] = $startBalance; $balances[$formatted] = $startBalance;
if (null === $currency) { if (null === $currency) {
$repository = app(AccountRepositoryInterface::class); $repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user); $repository->setUser($account->user);
$currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup);
} }
$currencyId = $currency->id; $currencyId = $currency->id;
$start->addDay(); $start->addDay();
// query! // query!
$set = $account->transactions() $set = $account->transactions()
->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59'))
->groupBy('transaction_journals.date') ->groupBy('transaction_journals.date')
->groupBy('transactions.transaction_currency_id') ->groupBy('transactions.transaction_currency_id')
->groupBy('transactions.foreign_currency_id') ->groupBy('transactions.foreign_currency_id')
->orderBy('transaction_journals.date', 'ASC') ->orderBy('transaction_journals.date', 'ASC')
->whereNull('transaction_journals.deleted_at') ->whereNull('transaction_journals.deleted_at')
->get( ->get(
[ // @phpstan-ignore-line [ // @phpstan-ignore-line
'transaction_journals.date', 'transaction_journals.date',
'transactions.transaction_currency_id', 'transactions.transaction_currency_id',
\DB::raw('SUM(transactions.amount) AS modified'), \DB::raw('SUM(transactions.amount) AS modified'),
'transactions.foreign_currency_id', 'transactions.foreign_currency_id',
\DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'),
] ]
) );
;
$currentBalance = $startBalance; $currentBalance = $startBalance;
/** @var Transaction $entry */ /** @var Transaction $entry */
foreach ($set as $entry) { foreach ($set as $entry) {
@ -173,7 +190,7 @@ class Steam
public function balanceByTransactions(Account $account, Carbon $date, ?TransactionCurrency $currency): array public function balanceByTransactions(Account $account, Carbon $date, ?TransactionCurrency $currency): array
{ {
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance-by-transactions'); $cache->addProperty('balance-by-transactions');
$cache->addProperty($date); $cache->addProperty($date);
@ -182,13 +199,12 @@ class Steam
return $cache->get(); return $cache->get();
} }
$query = $account->transactions() $query = $account->transactions()
->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->orderBy('transaction_journals.date', 'desc') ->orderBy('transaction_journals.date', 'desc')
->orderBy('transaction_journals.order', 'asc') ->orderBy('transaction_journals.order', 'asc')
->orderBy('transaction_journals.description', 'desc') ->orderBy('transaction_journals.description', 'desc')
->orderBy('transactions.amount', 'desc') ->orderBy('transactions.amount', 'desc');
;
if (null !== $currency) { if (null !== $currency) {
$query->where('transactions.transaction_currency_id', $currency->id); $query->where('transactions.transaction_currency_id', $currency->id);
$query->limit(1); $query->limit(1);
@ -203,7 +219,7 @@ class Steam
$return = []; $return = [];
$result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after']); $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after']);
foreach ($result as $entry) { foreach ($result as $entry) {
$key = (int) $entry->transaction_currency_id; $key = (int) $entry->transaction_currency_id;
if (array_key_exists($key, $return)) { if (array_key_exists($key, $return)) {
continue; continue;
} }
@ -222,7 +238,7 @@ class Steam
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
// abuse chart properties: // abuse chart properties:
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance'); $cache->addProperty('balance');
$cache->addProperty($date); $cache->addProperty($date);
@ -232,26 +248,24 @@ class Steam
} }
/** @var AccountRepositoryInterface $repository */ /** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class); $repository = app(AccountRepositoryInterface::class);
if (null === $currency) { if (null === $currency) {
$currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup);
} }
// first part: get all balances in own currency: // first part: get all balances in own currency:
$transactions = $account->transactions() $transactions = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', $currency->id) ->where('transactions.transaction_currency_id', $currency->id)
->get(['transactions.amount'])->toArray() ->get(['transactions.amount'])->toArray();
; $nativeBalance = $this->sumTransactions($transactions, 'amount');
$nativeBalance = $this->sumTransactions($transactions, 'amount');
// get all balances in foreign currency: // get all balances in foreign currency:
$transactions = $account->transactions() $transactions = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.foreign_currency_id', $currency->id) ->where('transactions.foreign_currency_id', $currency->id)
->where('transactions.transaction_currency_id', '!=', $currency->id) ->where('transactions.transaction_currency_id', '!=', $currency->id)
->get(['transactions.foreign_amount'])->toArray() ->get(['transactions.foreign_amount'])->toArray();
;
$foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount');
$balance = bcadd($nativeBalance, $foreignBalance); $balance = bcadd($nativeBalance, $foreignBalance);
$virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance;
@ -270,7 +284,7 @@ class Steam
public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance-in-range-converted'); $cache->addProperty('balance-in-range-converted');
$cache->addProperty($native->id); $cache->addProperty($native->id);
@ -290,35 +304,34 @@ class Steam
Log::debug(sprintf('Start balance on %s is %s', $formatted, $startBalance)); Log::debug(sprintf('Start balance on %s is %s', $formatted, $startBalance));
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
$converter = new ExchangeRateConverter(); $converter = new ExchangeRateConverter();
// not sure why this is happening: // not sure why this is happening:
$start->addDay(); $start->addDay();
// grab all transactions between start and end: // grab all transactions between start and end:
$set = $account->transactions() $set = $account->transactions()
->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59'))
->orderBy('transaction_journals.date', 'ASC') ->orderBy('transaction_journals.date', 'ASC')
->whereNull('transaction_journals.deleted_at') ->whereNull('transaction_journals.deleted_at')
->get( ->get(
[ [
'transaction_journals.date', 'transaction_journals.date',
'transactions.transaction_currency_id', 'transactions.transaction_currency_id',
'transactions.amount', 'transactions.amount',
'transactions.foreign_currency_id', 'transactions.foreign_currency_id',
'transactions.foreign_amount', 'transactions.foreign_amount',
] ]
)->toArray() )->toArray();
;
// loop the set and convert if necessary: // loop the set and convert if necessary:
$currentBalance = $startBalance; $currentBalance = $startBalance;
/** @var Transaction $transaction */ /** @var Transaction $transaction */
foreach ($set as $transaction) { foreach ($set as $transaction) {
$day = false; $day = false;
try { try {
$day = Carbon::parse($transaction['date'], config('app.timezone')); $day = Carbon::parse($transaction['date'], config('app.timezone'));
@ -328,7 +341,7 @@ class Steam
if (false === $day) { if (false === $day) {
$day = today(config('app.timezone')); $day = today(config('app.timezone'));
} }
$format = $day->format('Y-m-d'); $format = $day->format('Y-m-d');
// if the transaction is in the expected currency, change nothing. // if the transaction is in the expected currency, change nothing.
if ((int) $transaction['transaction_currency_id'] === $native->id) { if ((int) $transaction['transaction_currency_id'] === $native->id) {
// change the current balance, set it to today, continue the loop. // change the current balance, set it to today, continue the loop.
@ -351,21 +364,21 @@ class Steam
$currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId); $currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId);
$currencies[$currencyId] = $currency; $currencies[$currencyId] = $currency;
$rate = $converter->getCurrencyRate($currency, $native, $day); $rate = $converter->getCurrencyRate($currency, $native, $day);
$convertedAmount = bcmul($transaction['amount'], $rate); $convertedAmount = bcmul($transaction['amount'], $rate);
$currentBalance = bcadd($currentBalance, $convertedAmount); $currentBalance = bcadd($currentBalance, $convertedAmount);
$balances[$format] = $currentBalance; $balances[$format] = $currentBalance;
Log::debug(sprintf( Log::debug(sprintf(
'%s: transaction in %s(!). Conversion rate is %s. %s %s = %s %s', '%s: transaction in %s(!). Conversion rate is %s. %s %s = %s %s',
$format, $format,
$currency->code, $currency->code,
$rate, $rate,
$currency->code, $currency->code,
$transaction['amount'], $transaction['amount'],
$native->code, $native->code,
$convertedAmount $convertedAmount
)); ));
} }
$cache->store($balances); $cache->store($balances);
@ -397,7 +410,7 @@ class Steam
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
Log::debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code)); Log::debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code));
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance'); $cache->addProperty('balance');
$cache->addProperty($date); $cache->addProperty($date);
@ -418,72 +431,66 @@ class Steam
return $this->balance($account, $date); return $this->balance($account, $date);
} }
$new = []; $new = [];
$existing = []; $existing = [];
$new[] = $account->transactions() // 1 $new[] = $account->transactions() // 1
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', $currency->id) ->where('transactions.transaction_currency_id', $currency->id)
->whereNull('transactions.foreign_currency_id') ->whereNull('transactions.foreign_currency_id')
->get(['transaction_journals.date', 'transactions.amount'])->toArray() ->get(['transaction_journals.date', 'transactions.amount'])->toArray();
;
Log::debug(sprintf('%d transaction(s) in set #1', count($new[0]))); Log::debug(sprintf('%d transaction(s) in set #1', count($new[0])));
$existing[] = $account->transactions() // 2 $existing[] = $account->transactions() // 2
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', $native->id) ->where('transactions.transaction_currency_id', $native->id)
->whereNull('transactions.foreign_currency_id') ->whereNull('transactions.foreign_currency_id')
->get(['transactions.amount'])->toArray() ->get(['transactions.amount'])->toArray();
;
Log::debug(sprintf('%d transaction(s) in set #2', count($existing[0]))); Log::debug(sprintf('%d transaction(s) in set #2', count($existing[0])));
$new[] = $account->transactions() // 3 $new[] = $account->transactions() // 3
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', '!=', $currency->id) ->where('transactions.transaction_currency_id', '!=', $currency->id)
->where('transactions.transaction_currency_id', '!=', $native->id) ->where('transactions.transaction_currency_id', '!=', $native->id)
->whereNull('transactions.foreign_currency_id') ->whereNull('transactions.foreign_currency_id')
->get(['transaction_journals.date', 'transactions.amount'])->toArray() ->get(['transaction_journals.date', 'transactions.amount'])->toArray();
;
Log::debug(sprintf('%d transactions in set #3', count($new[1]))); Log::debug(sprintf('%d transactions in set #3', count($new[1])));
$existing[] = $account->transactions() // 4 $existing[] = $account->transactions() // 4
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.foreign_currency_id', $native->id) ->where('transactions.foreign_currency_id', $native->id)
->whereNotNull('transactions.foreign_amount') ->whereNotNull('transactions.foreign_amount')
->get(['transactions.foreign_amount'])->toArray() ->get(['transactions.foreign_amount'])->toArray();
;
Log::debug(sprintf('%d transactions in set #4', count($existing[1]))); Log::debug(sprintf('%d transactions in set #4', count($existing[1])));
$new[] = $account->transactions()// 5 $new[] = $account->transactions()// 5
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', $currency->id) ->where('transactions.transaction_currency_id', $currency->id)
->where('transactions.foreign_currency_id', '!=', $native->id) ->where('transactions.foreign_currency_id', '!=', $native->id)
->whereNotNull('transactions.foreign_amount') ->whereNotNull('transactions.foreign_amount')
->get(['transaction_journals.date', 'transactions.amount'])->toArray() ->get(['transaction_journals.date', 'transactions.amount'])->toArray();
;
Log::debug(sprintf('%d transactions in set #5', count($new[2]))); Log::debug(sprintf('%d transactions in set #5', count($new[2])));
$new[] = $account->transactions()// 6 $new[] = $account->transactions()// 6
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->where('transactions.transaction_currency_id', '!=', $currency->id) ->where('transactions.transaction_currency_id', '!=', $currency->id)
->where('transactions.foreign_currency_id', '!=', $native->id) ->where('transactions.foreign_currency_id', '!=', $native->id)
->whereNotNull('transactions.foreign_amount') ->whereNotNull('transactions.foreign_amount')
->get(['transaction_journals.date', 'transactions.amount'])->toArray() ->get(['transaction_journals.date', 'transactions.amount'])->toArray();
;
Log::debug(sprintf('%d transactions in set #6', count($new[3]))); Log::debug(sprintf('%d transactions in set #6', count($new[3])));
// process both sets of transactions. Of course, no need to convert set "existing". // process both sets of transactions. Of course, no need to convert set "existing".
$balance = $this->sumTransactions($existing[0], 'amount'); $balance = $this->sumTransactions($existing[0], 'amount');
$balance = bcadd($balance, $this->sumTransactions($existing[1], 'foreign_amount')); $balance = bcadd($balance, $this->sumTransactions($existing[1], 'foreign_amount'));
Log::debug(sprintf('Balance from set #2 and #4 is %f', $balance)); Log::debug(sprintf('Balance from set #2 and #4 is %f', $balance));
// need to convert the others. All sets use the "amount" value as their base (that's easy) // need to convert the others. All sets use the "amount" value as their base (that's easy)
// but we need to convert each transaction separately because the date difference may // but we need to convert each transaction separately because the date difference may
// incur huge currency changes. // incur huge currency changes.
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
$start = clone $date; $start = clone $date;
$end = clone $date; $end = clone $date;
$converter = new ExchangeRateConverter(); $converter = new ExchangeRateConverter();
foreach ($new as $set) { foreach ($new as $set) {
foreach ($set as $transaction) { foreach ($set as $transaction) {
$currentDate = false; $currentDate = false;
@ -506,7 +513,7 @@ class Steam
foreach ($new as $set) { foreach ($new as $set) {
foreach ($set as $transaction) { foreach ($set as $transaction) {
$currentDate = false; $currentDate = false;
try { try {
$currentDate = Carbon::parse($transaction['date'], config('app.timezone')); $currentDate = Carbon::parse($transaction['date'], config('app.timezone'));
@ -523,9 +530,9 @@ class Steam
} }
// add virtual balance (also needs conversion) // add virtual balance (also needs conversion)
$virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance;
$virtual = $converter->convert($currency, $native, $account->created_at, $virtual); $virtual = $converter->convert($currency, $native, $account->created_at, $virtual);
$balance = bcadd($balance, $virtual); $balance = bcadd($balance, $virtual);
$converter->summarize(); $converter->summarize();
$cache->store($balance); $cache->store($balance);
@ -542,9 +549,9 @@ class Steam
public function balancesByAccounts(Collection $accounts, Carbon $date): array public function balancesByAccounts(Collection $accounts, Carbon $date): array
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
$ids = $accounts->pluck('id')->toArray(); $ids = $accounts->pluck('id')->toArray();
// cache this property. // cache this property.
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($ids); $cache->addProperty($ids);
$cache->addProperty('balances'); $cache->addProperty('balances');
$cache->addProperty($date); $cache->addProperty($date);
@ -573,9 +580,9 @@ class Steam
public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
$ids = $accounts->pluck('id')->toArray(); $ids = $accounts->pluck('id')->toArray();
// cache this property. // cache this property.
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($ids); $cache->addProperty($ids);
$cache->addProperty('balances-converted'); $cache->addProperty('balances-converted');
$cache->addProperty($date); $cache->addProperty($date);
@ -591,9 +598,9 @@ class Steam
$default = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $default = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup);
$result[$account->id] $result[$account->id]
= [ = [
'balance' => $this->balance($account, $date), 'balance' => $this->balance($account, $date),
'native_balance' => $this->balanceConverted($account, $date, $default), 'native_balance' => $this->balanceConverted($account, $date, $default),
]; ];
} }
$cache->store($result); $cache->store($result);
@ -607,9 +614,9 @@ class Steam
public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
$ids = $accounts->pluck('id')->toArray(); $ids = $accounts->pluck('id')->toArray();
// cache this property. // cache this property.
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($ids); $cache->addProperty($ids);
$cache->addProperty('balances-per-currency'); $cache->addProperty('balances-per-currency');
$cache->addProperty($date); $cache->addProperty($date);
@ -634,7 +641,7 @@ class Steam
{ {
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
// abuse chart properties: // abuse chart properties:
$cache = new CacheProperties(); $cache = new CacheProperties();
$cache->addProperty($account->id); $cache->addProperty($account->id);
$cache->addProperty('balance-per-currency'); $cache->addProperty('balance-per-currency');
$cache->addProperty($date); $cache->addProperty($date);
@ -642,10 +649,9 @@ class Steam
return $cache->get(); return $cache->get();
} }
$query = $account->transactions() $query = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
->groupBy('transactions.transaction_currency_id') ->groupBy('transactions.transaction_currency_id');
;
$balances = $query->get(['transactions.transaction_currency_id', \DB::raw('SUM(transactions.amount) as sum_for_currency')]); // @phpstan-ignore-line $balances = $query->get(['transactions.transaction_currency_id', \DB::raw('SUM(transactions.amount) as sum_for_currency')]); // @phpstan-ignore-line
$return = []; $return = [];
@ -677,10 +683,10 @@ class Steam
// Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision));
if (str_contains($number, '.')) { if (str_contains($number, '.')) {
if ('-' !== $number[0]) { if ('-' !== $number[0]) {
return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision);
} }
return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision);
} }
return $number; return $number;
@ -763,15 +769,15 @@ class Steam
{ {
$list = []; $list = [];
$set = auth()->user()->transactions() $set = auth()->user()->transactions()
->whereIn('transactions.account_id', $accounts) ->whereIn('transactions.account_id', $accounts)
->groupBy(['transactions.account_id', 'transaction_journals.user_id']) ->groupBy(['transactions.account_id', 'transaction_journals.user_id'])
->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line
; ;
/** @var Transaction $entry */ /** @var Transaction $entry */
foreach ($set as $entry) { foreach ($set as $entry) {
$date = new Carbon($entry->max_date, config('app.timezone')); $date = new Carbon($entry->max_date, config('app.timezone'));
$date->setTimezone(config('app.timezone')); $date->setTimezone(config('app.timezone'));
$list[$entry->account_id] = $date; $list[$entry->account_id] = $date;
} }
@ -846,9 +852,9 @@ class Steam
public function getSafeUrl(string $unknownUrl, string $safeUrl): string public function getSafeUrl(string $unknownUrl, string $safeUrl): string
{ {
// Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl));
$returnUrl = $safeUrl; $returnUrl = $safeUrl;
$unknownHost = parse_url($unknownUrl, PHP_URL_HOST); $unknownHost = parse_url($unknownUrl, PHP_URL_HOST);
$safeHost = parse_url($safeUrl, PHP_URL_HOST); $safeHost = parse_url($safeUrl, PHP_URL_HOST);
if (null !== $unknownHost && $unknownHost === $safeHost) { if (null !== $unknownHost && $unknownHost === $safeHost) {
$returnUrl = $unknownUrl; $returnUrl = $unknownUrl;
@ -885,7 +891,7 @@ class Steam
*/ */
public function floatalize(string $value): string public function floatalize(string $value): string
{ {
$value = strtoupper($value); $value = strtoupper($value);
if (!str_contains($value, 'E')) { if (!str_contains($value, 'E')) {
return $value; return $value;
} }

View File

@ -26,6 +26,7 @@ namespace FireflyIII\TransactionRules\Actions;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
use FireflyIII\Events\TriggeredAuditLog; use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleAction;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
@ -81,6 +82,7 @@ class UpdatePiggybank implements ActionInterface
if ($source->account_id === $piggyBank->account_id) { if ($source->account_id === $piggyBank->account_id) {
app('log')->debug('Piggy bank account is linked to source, so remove amount from piggy bank.'); app('log')->debug('Piggy bank account is linked to source, so remove amount from piggy bank.');
throw new FireflyException('Reference the correct account here.');
$this->removeAmount($piggyBank, $journal, $journalObj, $destination->amount); $this->removeAmount($piggyBank, $journal, $journalObj, $destination->amount);
event( event(
@ -161,6 +163,7 @@ class UpdatePiggybank implements ActionInterface
} }
// make sure we can remove amount: // make sure we can remove amount:
throw new FireflyException('Reference the correct account here.');
if (false === $repository->canRemoveAmount($piggyBank, $amount)) { if (false === $repository->canRemoveAmount($piggyBank, $amount)) {
app('log')->warning(sprintf('Cannot remove %s from piggy bank.', $amount)); app('log')->warning(sprintf('Cannot remove %s from piggy bank.', $amount));
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_remove_from_piggy', ['amount' => $amount, 'name' => $piggyBank->name]))); event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_remove_from_piggy', ['amount' => $amount, 'name' => $piggyBank->name])));
@ -169,6 +172,7 @@ class UpdatePiggybank implements ActionInterface
} }
app('log')->debug(sprintf('Will now remove %s from piggy bank.', $amount)); app('log')->debug(sprintf('Will now remove %s from piggy bank.', $amount));
throw new FireflyException('Reference the correct account here.');
$repository->removeAmount($piggyBank, $amount, $journal); $repository->removeAmount($piggyBank, $amount, $journal);
} }
@ -199,6 +203,7 @@ class UpdatePiggybank implements ActionInterface
} }
// make sure we can add amount: // make sure we can add amount:
throw new FireflyException('Reference the correct account here.');
if (false === $repository->canAddAmount($piggyBank, $amount)) { if (false === $repository->canAddAmount($piggyBank, $amount)) {
app('log')->warning(sprintf('Cannot add %s to piggy bank.', $amount)); app('log')->warning(sprintf('Cannot add %s to piggy bank.', $amount));
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_add_to_piggy', ['amount' => $amount, 'name' => $piggyBank->name]))); event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_add_to_piggy', ['amount' => $amount, 'name' => $piggyBank->name])));

View File

@ -78,7 +78,7 @@ class PiggyBankTransformer extends AbstractTransformer
// get currently saved amount: // get currently saved amount:
$currency = $piggyBank->transactionCurrency; $currency = $piggyBank->transactionCurrency;
$currentAmount = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places); $currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank);
// Amounts, depending on 0.0 state of target amount // Amounts, depending on 0.0 state of target amount
$percentage = null; $percentage = null;

View File

@ -106,6 +106,10 @@ class User extends Authenticatable
return $this->hasMany(Account::class); return $this->hasMany(Account::class);
} }
public function piggyBanks() {
throw new FireflyException('Method no longer supported.');
}
/** /**
* Link to attachments * Link to attachments
*/ */

View File

@ -28,8 +28,8 @@ return [
'slack' => ['enabled' => true, 'ui_configurable' => 1], 'slack' => ['enabled' => true, 'ui_configurable' => 1],
'ntfy' => ['enabled' => true, 'ui_configurable' => 1], 'ntfy' => ['enabled' => true, 'ui_configurable' => 1],
'pushover' => ['enabled' => true, 'ui_configurable' => 1], 'pushover' => ['enabled' => true, 'ui_configurable' => 1],
'gotify' => ['enabled' => false, 'ui_configurable' => 0], // 'gotify' => ['enabled' => false, 'ui_configurable' => 0],
'pushbullet' => ['enabled' => false, 'ui_configurable' => 0], // 'pushbullet' => ['enabled' => false, 'ui_configurable' => 0],
], ],
'notifications' => [ 'notifications' => [
'user' => [ 'user' => [

View File

@ -25,9 +25,9 @@
<td style="text-align: right;"> <td style="text-align: right;">
{% if event.amount < 0 %} {% if event.amount < 0 %}
<span class="text-danger money-negative">{{ trans('firefly.removed_amount', {amount: formatAmountByAccount(event.piggyBank.account, event.amount, false)})|raw }}</span> <span class="text-danger money-negative">{{ trans('firefly.removed_amount', {amount: formatAmountBySymbol(event.amount,event.piggyBank.transactionCurrency.symbol, false)})|raw }}</span>
{% else %} {% else %}
<span class="text-success money-positive">{{ trans('firefly.added_amount', {amount: formatAmountByAccount(event.piggyBank.account, event.amount, false)})|raw }}</span> <span class="text-success money-positive">{{ trans('firefly.added_amount', {amount: formatAmountBySymbol(event.amount, event.piggyBank.transactionCurrency.symbol, false)})|raw }}</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -14,16 +14,16 @@
<h3 class="box-title">{{ trans('firefly.add_money_to_piggy', {name: piggyBank.name}) }}</h3> <h3 class="box-title">{{ trans('firefly.add_money_to_piggy', {name: piggyBank.name}) }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
{% if maxAmount > 0 %} {% if total > 0 %}
<p>
{{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(currency,maxAmount) }}.
</p>
<div class="input-group">
<div class="input-group-addon">{{ currency.symbol|raw }}</div> {% for account in accounts %}
<input step="any" class="form-control" id="amount" autocomplete="off" name="amount" max="{{ maxAmount|round(2) }}" <strong>{{ account.account.name }} ({{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.max_amount) }})</strong>
type="number"/> <div class="input-group">
</div> <div class="input-group-addon">{{ piggyBank.transactionCurrency.symbol|raw }}</div>
<input step="any" min="0" class="form-control" id="amount_{{ account.account.id }}" autocomplete="off" name="amount[{{ account.account.id }}]" max="{{ account.max_amount|round(piggyBank.transactionCurrency.decimal_places) }}" type="number"/>
</div>
{% endfor %}
<p> <p>
&nbsp; &nbsp;
</p> </p>

View File

@ -5,19 +5,17 @@
</button> </button>
<h4 class="modal-title">{{ trans('firefly.add_money_to_piggy_title', {name: piggyBank.name}) }}</h4> <h4 class="modal-title">{{ trans('firefly.add_money_to_piggy_title', {name: piggyBank.name}) }}</h4>
</div> </div>
{% if maxAmount > 0 %} {% if total > 0 %}
<form style="display: inline;" id="add" action="{{ route('piggy-banks.add', piggyBank.id) }}" method="POST"> <form style="display: inline;" id="add" action="{{ route('piggy-banks.add', piggyBank.id) }}" method="POST">
<div class="modal-body"> <div class="modal-body">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/> <input type="hidden" name="_token" value="{{ csrf_token() }}"/>
{% for account in accounts %}
<p> <strong>{{ account.account.name }} ({{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.max_amount) }})</strong>
{{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(currency,maxAmount) }}.
</p>
<div class="input-group"> <div class="input-group">
<div class="input-group-addon">{{ currency.symbol|raw }}</div> <div class="input-group-addon">{{ piggyBank.transactionCurrency.symbol|raw }}</div>
<input step="any" class="form-control" id="amount" autocomplete="off" name="amount" max="{{ maxAmount|round(currency.decimal_places) }}" type="number"/> <input step="any" min="0" class="form-control" id="amount_{{ account.account.id }}" autocomplete="off" name="amount[{{ account.account.id }}]" max="{{ account.max_amount|round(piggyBank.transactionCurrency.decimal_places) }}" type="number"/>
</div> </div>
{% endfor %}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button>

View File

@ -17,7 +17,7 @@
<div class="box-body"> <div class="box-body">
{{ ExpandedForm.text('name') }} {{ ExpandedForm.text('name') }}
{{ ExpandedForm.amountNoCurrency('target_amount') }} {{ ExpandedForm.amountNoCurrency('target_amount') }}
{{ CurrencyForm.currencyList('transaction_ currency_id', null, {helpText:'piggy_default_currency'|_}) }} {{ CurrencyForm.currencyList('transaction_currency_id', null, {helpText:'piggy_default_currency'|_}) }}
{{ AccountForm.assetLiabilityMultiAccountList('accounts', preFilled.accounts, {label: 'saveOnAccounts'|_, helpText: 'piggy_account_currency_match'|_ }) }} {{ AccountForm.assetLiabilityMultiAccountList('accounts', preFilled.accounts, {label: 'saveOnAccounts'|_, helpText: 'piggy_account_currency_match'|_ }) }}
</div> </div>

View File

@ -22,8 +22,9 @@
<div class="box-body"> <div class="box-body">
{{ ExpandedForm.text('name') }} {{ ExpandedForm.text('name') }}
{{ AccountForm.assetAccountList('account_id', null, {label: 'saveOnAccount'|_ }) }} {{ ExpandedForm.amountNoCurrency('target_amount') }}
{{ ExpandedForm.amountNoCurrency('targetamount') }} {{ CurrencyForm.currencyList('transaction_currency_id', null, {helpText:'piggy_default_currency'|_}) }}
{{ AccountForm.assetLiabilityMultiAccountList('accounts', preFilled.accounts, {label: 'saveOnAccounts'|_, helpText: 'piggy_account_currency_match'|_ }) }}
</div> </div>
</div> </div>

View File

@ -14,15 +14,17 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<p> {% for account in accounts %}
{{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(currency, repetition.currentamount) }}. <p>
</p> {{ account.account.name }}: {{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.saved_so_far) }}.
</p>
<div class="input-group">
<div class="input-group-addon">{{ piggyBank.transactionCurrency.symbol|raw }}</div>
<input step="any" class="form-control" id="amount_{{ account.account.id }}" autocomplete="off" name="amount[{{ account.account.id }}]" max="{{ account.saved_so_far }}"
type="number">
</div>
{% endfor %}
<div class="input-group">
<div class="input-group-addon">{{ currency.symbol|raw }}</div>
<input step="any" class="form-control" id="amount" autocomplete="off" name="amount" max="{{ repetition.currentamount }}"
type="number"/>
</div>
<p> <p>
&nbsp; &nbsp;
</p> </p>

View File

@ -10,15 +10,16 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
{% for account in accounts %}
<p> <p>
{{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(currency, repetition.currentamount) }}. {{ account.account.name }}: {{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.saved_so_far) }}.
</p> </p>
<div class="input-group"> <div class="input-group">
<div class="input-group-addon">{{ currency.symbol|raw }}</div> <div class="input-group-addon">{{ piggyBank.transactionCurrency.symbol|raw }}</div>
<input step="any" class="form-control" id="amount" autocomplete="off" name="amount" max="{{ repetition.currentamount }}" <input step="any" class="form-control" id="amount_{{ account.account.id }}" autocomplete="off" name="amount[{{ account.account.id }}]" max="{{ account.saved_so_far }}"
type="number"> type="number">
</div> </div>
{% endfor %}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{ 'close'|_ }}</button>

View File

@ -33,8 +33,12 @@
<div class="box-body no-padding"> <div class="box-body no-padding">
<table class="table table-responsive table-hover" id="piggyDetails"> <table class="table table-responsive table-hover" id="piggyDetails">
<tr> <tr>
<td style="width:40%;">{{ 'account'|_ }}</td> <td style="width:40%;">{{ 'saveOnAccounts'|_ }}</td>
<td><a href="{{ route('accounts.show', piggyBank.account_id) }}">{{ piggyBank.account.name }}</a></td> <td>
{% for account in piggy.accounts %}
<a href="{{ route('accounts.show', account.id) }}">{{ account.name }}</a><br>
{% endfor %}
</td>
</tr> </tr>
{% if piggy.object_group_title %} {% if piggy.object_group_title %}
<tr> <tr>