Compare commits

...

21 Commits

Author SHA1 Message Date
github-actions[bot]
5a55593e34 Merge pull request #11033 from firefly-iii/release-1759931464
🤖 Automatically merge the PR into the develop branch.
2025-10-08 15:51:16 +02:00
JC5
8979e5ad5a 🤖 Auto commit for release 'develop' on 2025-10-08 2025-10-08 15:51:04 +02:00
James Cole
3ab65c27ac Fix missing array indicator. 2025-10-08 15:47:03 +02:00
James Cole
a3cac6fd0f Fix #11031 2025-10-08 15:16:10 +02:00
James Cole
1acb5d8681 Merge pull request #11028 from codearena-bot/codearena/agent-a-6398119d-1759907698909
Adding Latin American Currency Support
2025-10-08 09:47:22 +02:00
mergify[bot]
f24cdc7897 Merge branch 'develop' into codearena/agent-a-6398119d-1759907698909 2025-10-08 07:23:07 +00:00
github-actions[bot]
a2b611253b Merge pull request #11029 from firefly-iii/release-1759908132
🤖 Automatically merge the PR into the develop branch.
2025-10-08 09:22:23 +02:00
JC5
252459c29b 🤖 Auto commit for release 'develop' on 2025-10-08 2025-10-08 09:22:12 +02:00
mergify[bot]
3582baf9f7 Merge branch 'develop' into codearena/agent-a-6398119d-1759907698909 2025-10-08 07:18:35 +00:00
James Cole
b03ecab035 Move to another method here as well. 2025-10-08 09:17:56 +02:00
codearena-bot
547a4e9dbb Update database/seeders/TransactionCurrencySeeder.php via CodeArena 2025-10-08 00:14:59 -07:00
James Cole
62e33a51bd Better order for method arguments. 2025-10-08 08:54:26 +02:00
James Cole
8b0ee7e20a Support multi-currency accounts better, matches the old method. 2025-10-08 08:12:30 +02:00
James Cole
0e0ec89b26 Remove all uses of "finalAccountBalance", move towards one function to rule them all. 2025-10-08 07:09:42 +02:00
James Cole
ec08485c2b Clean up some code surrounding account balances. 2025-10-08 06:44:48 +02:00
James Cole
d91d30c8f0 Remove some debug logging, use Facade. 2025-10-08 06:31:47 +02:00
James Cole
634a43c361 Merge branch 'main' into develop 2025-10-08 06:31:34 +02:00
James Cole
f2f86e1139 Merge pull request #11024 from ctrl-f5/feat/improve-date-handling
improved balance range date handling
2025-10-08 06:30:56 +02:00
Nicky De Maeyer
a1c870c962 improved request and balance range date handling 2025-10-07 20:30:11 +02:00
github-actions[bot]
b254074867 Merge pull request #11021 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2025-10-07 10:11:49 +02:00
github-actions[bot]
2ddc012549 Merge pull request #11014 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2025-10-07 07:20:40 +02:00
29 changed files with 255 additions and 244 deletions

View File

@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2025
- codearena-bot
- Nicky De Maeyer
- Denis Iskandarov
- Lompi

View File

@@ -84,7 +84,7 @@ class AccountController extends Controller
$data = $request->getData();
$types = $data['types'];
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$date = $data['date'];
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));

View File

@@ -589,8 +589,6 @@ class BasicController extends Controller
private function getNetWorthInfo(Carbon $end): array
{
$end->endOfDay();
/** @var User $user */
$user = auth()->user();
Log::debug(sprintf('getNetWorthInfo up until "%s".', $end->format('Y-m-d H:i:s')));

View File

@@ -48,10 +48,12 @@ class AutocompleteRequest extends FormRequest
// remove 'initial balance' from allowed types. its internal
$array = array_diff($array, [AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::RECONCILIATION->value]);
$date = $this->getCarbonDate('date') ?? today(config('app.timezone'));
return [
'types' => $array,
'query' => $this->convertString('query'),
'date' => $this->getCarbonDate('date'),
'date' => $date->endOfDay(),
];
}

View File

@@ -55,7 +55,6 @@ class DateRequest extends FormRequest
// sanity check on dates:
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$start->startOfDay();
$end->endOfDay();
if ($start->diffInYears($end, true) > 5) {

View File

@@ -137,9 +137,12 @@ class MonthReportGenerator implements ReportGeneratorInterface
;
$journals = $collector->getExtractedJournals();
$journals = array_reverse($journals, true);
// this call is correct.
Log::debug(sprintf('getAuditReport: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$dayBeforeBalance = Steam::finalAccountBalance($account, $date);
Log::debug(sprintf('getAuditReport: Call accountsBalancesOptimized with date/time "%s"', $date->toIso8601String()));
// 2025-10-08 replace with accountsBalancesOptimized.
// $dayBeforeBalance = Steam::finalAccountBalance($account, $date);
$dayBeforeBalance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id];
$startBalance = $dayBeforeBalance['balance'];
$primaryCurrency = app('amount')->getPrimaryCurrencyByUserGroup($account->user->userGroup);
$currency = $accountRepository->getAccountCurrency($account) ?? $primaryCurrency;
@@ -176,12 +179,14 @@ class MonthReportGenerator implements ReportGeneratorInterface
// call is correct.
Log::debug(sprintf('getAuditReport end: Call finalAccountBalance with date/time "%s"', $this->end->toIso8601String()));
// 2025-10-08 replace with accountsBalancesOptimized:
return [
'journals' => $journals,
'currency' => $currency,
'exists' => 0 !== count($journals),
'end' => $this->end->isoFormat((string) trans('config.month_and_day_moment_js', [], $locale)),
'endBalance' => Steam::finalAccountBalance($account, $this->end)['balance'],
// 'endBalance' => Steam::finalAccountBalance($account, $this->end)['balance'],
'endBalance' => Steam::accountsBalancesOptimized(new Collection()->push($account), $this->end)[$account->id]['balance'],
'dayBefore' => $date->isoFormat((string) trans('config.month_and_day_moment_js', [], $locale)),
'dayBeforeBalance' => $dayBeforeBalance,
];

View File

@@ -62,12 +62,13 @@ trait AccountCollection
if (null === $account) {
continue;
}
// 2025-10-08 replace with accountsBalancesOptimized
// the balance must be found BEFORE the transaction date.
// so sub one second. This is not perfect, but works well enough.
$date = clone $transaction['date'];
$date->subSecond();
Log::debug(sprintf('accountBalanceIs: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date);
// so inclusive = false
Log::debug(sprintf('accountBalanceIs: Call accountsBalancesOptimized with date/time "%s"', $transaction['date']->toIso8601String()));
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $transaction['date'], convertToPrimary: null, inclusive: false)[$account->id];
// $balance = Steam::finalAccountBalance($account, $date);
$result = bccomp((string) $balance['balance'], $value);
Log::debug(sprintf('"%s" vs "%s" is %d', $balance['balance'], $value, $result));

View File

@@ -25,8 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Helpers\Report;
use Carbon\Carbon;
use Deprecated;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\UserGroup;
@@ -130,55 +128,4 @@ class NetWorth implements NetWorthInterface
$this->accountRepository->setUserGroup($userGroup);
}
#[Deprecated]
public function sumNetWorthByCurrency(Carbon $date): array
{
/**
* Collect accounts
*/
$accounts = $this->getAccounts();
$return = [];
Log::debug(sprintf('SumNetWorth: accountsBalancesOptimized("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::accountsBalancesOptimized($accounts, $date);
foreach ($accounts as $account) {
$currency = $this->accountRepository->getAccountCurrency($account);
$balance = $balances[$account->id]['balance'] ?? '0';
// always subtract virtual balance.
$virtualBalance = $account->virtual_balance;
if ('' !== $virtualBalance) {
$balance = bcsub($balance, (string) $virtualBalance);
}
$return[$currency->id] ??= [
'id' => (string) $currency->id,
'name' => $currency->name,
'symbol' => $currency->symbol,
'code' => $currency->code,
'decimal_places' => $currency->decimal_places,
'sum' => '0',
];
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $balance);
}
return $return;
}
private function getAccounts(): Collection
{
$accounts = $this->accountRepository->getAccountsByType(
[AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]
);
$filtered = new Collection();
/** @var Account $account */
foreach ($accounts as $account) {
if (1 === (int) $this->accountRepository->getMetaValue($account, 'include_net_worth')) {
$filtered->push($account);
}
}
return $filtered;
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Helpers\Report;
use Carbon\Carbon;
use Deprecated;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -49,12 +48,4 @@ interface NetWorthInterface
public function setUser(Authenticatable|User|null $user): void;
public function setUserGroup(UserGroup $userGroup): void;
/**
* TODO move to repository
*
* Same as above but cleaner function with less dependencies.
*/
#[Deprecated]
public function sumNetWorthByCurrency(Carbon $date): array;
}

View File

@@ -74,32 +74,30 @@ class IndexController extends Controller
*/
public function inactive(Request $request, string $objectType)
{
$inactivePage = true;
$subTitle = (string) trans(sprintf('firefly.%s_accounts_inactive', $objectType));
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType));
$types = config(sprintf('firefly.accountTypesByIdentifier.%s', $objectType));
$collection = $this->repository->getInactiveAccountsByType($types);
$total = $collection->count();
$page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
$accounts = $collection->slice(($page - 1) * $pageSize, $pageSize);
$inactivePage = true;
$subTitle = (string) trans(sprintf('firefly.%s_accounts_inactive', $objectType));
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType));
$types = config(sprintf('firefly.accountTypesByIdentifier.%s', $objectType));
$collection = $this->repository->getInactiveAccountsByType($types);
$total = $collection->count();
$page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
$accounts = $collection->slice(($page - 1) * $pageSize, $pageSize);
unset($collection);
/** @var Carbon $start */
$start = clone session('start', today(config('app.timezone'))->startOfMonth());
$start = clone session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $end */
$end = clone session('end', today(config('app.timezone'))->endOfMonth());
$end = clone session('end', today(config('app.timezone'))->endOfMonth());
// #10618 go to the end of the previous day.
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('inactive start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('inactive end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('inactive start: accountsBalancesInRange("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
[
$startBalances,
$endBalances,
] = Steam::accountsBalancesInRange($accounts, $start, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);
$accounts->each(
@@ -119,7 +117,7 @@ class IndexController extends Controller
);
// make paginator:
$accounts = new LengthAwarePaginator($accounts, $total, $pageSize, $page);
$accounts = new LengthAwarePaginator($accounts, $total, $pageSize, $page);
$accounts->setPath(route('accounts.inactive.index', [$objectType]));
return view('accounts.index', compact('objectType', 'inactivePage', 'subTitleIcon', 'subTitle', 'page', 'accounts'));
@@ -169,14 +167,12 @@ class IndexController extends Controller
/** @var Carbon $end */
$end = clone session('end', today(config('app.timezone'))->endOfMonth());
// #10618 go to the end of the previous day.
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('index start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('index end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug(sprintf('index: accountsBalancesInRange("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
[
$startBalances,
$endBalances,
] = Steam::accountsBalancesInRange($accounts, $start, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);

View File

@@ -39,6 +39,7 @@ use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
@@ -113,13 +114,20 @@ class ReconcileController extends Controller
$start->startOfDay();
$end->endOfDay();
$startDate = clone $start;
$startDate->subDay()->endOfDay(); // this is correct, subday endofday ends at 23:59:59
// $startDate = clone $start;
// $startDate->subDay()->endOfDay(); // this is correct, subday endofday ends at 23:59:59
// both are validated and are correct.
Log::debug(sprintf('reconcile: Call finalAccountBalance with date/time "%s"', $startDate->toIso8601String()));
Log::debug(sprintf('reconcile2: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
$startBalance = Steam::bcround(Steam::finalAccountBalance($account, $startDate)['balance'], $currency->decimal_places);
$endBalance = Steam::bcround(Steam::finalAccountBalance($account, $end)['balance'], $currency->decimal_places);
// Log::debug(sprintf('reconcile: Call finalAccountBalance with date/time "%s"', $startDate->toIso8601String()));
// Log::debug(sprintf('reconcile2: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
// $startBalance = Steam::bcround(Steam::finalAccountBalance($account, $startDate)['balance'], $currency->decimal_places);
// $endBalance = Steam::bcround(Steam::finalAccountBalance($account, $end)['balance'], $currency->decimal_places);
// 2025-10-08 replace with accountsBalancesOptimized
// no longer need to do subday->endofday on $start, set inclusive = false for the same effect.
$startBalance = Steam::bcround(Steam::accountsBalancesOptimized(new Collection()->push($account), $start, convertToPrimary: null, inclusive: false)[$account->id]['balance'], $currency->decimal_places);
$endBalance = Steam::bcround(Steam::accountsBalancesOptimized(new Collection()->push($account), $end)[$account->id]['balance'], $currency->decimal_places);
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $account->accountType->type));
$subTitle = (string) trans('firefly.reconcile_account', ['account' => $account->name]);

View File

@@ -169,8 +169,9 @@ class ShowController extends Controller
$now = $end;
}
Log::debug(sprintf('show: Call finalAccountBalance with date/time "%s"', $now->toIso8601String()));
$balances = Steam::filterAccountBalance(Steam::finalAccountBalance($account, $now), $account, $this->convertToPrimary, $accountCurrency);
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
$balances = Steam::accountsBalancesOptimized(new Collection()->push($account), $now)[$account->id];
// $balances = Steam::filterAccountBalance(Steam::finalAccountBalance($account, $now), $account, $this->convertToPrimary, $accountCurrency);
return view(
'accounts.show',
@@ -237,8 +238,12 @@ class ShowController extends Controller
$chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$showAll = true;
// correct
Log::debug(sprintf('showAll: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
$balances = Steam::filterAccountBalance(Steam::finalAccountBalance($account, $end), $account, $this->convertToPrimary, $accountCurrency);
Log::debug(sprintf('showAll: Call accountsBalancesOptimized with date/time "%s"', $end->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balances = Steam::finalAccountBalance($account, $end);
// $balances = Steam::filterAccountBalance($balances, $account, $this->convertToPrimary, $accountCurrency);
$balances = Steam::accountsBalancesOptimized(new Collection()->push($account), $end)[$account->id];
return view(
'accounts.show',

View File

@@ -116,10 +116,11 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug(sprintf('expenseAccounts: accountsBalancesInRange("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
[
$startBalances,
$endBalances,
] = Steam::accountsBalancesInRange($accounts, $start, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug('Done collecting balances');
// loop the accounts, then check for balance and currency info.
foreach ($accounts as $account) {
@@ -521,9 +522,9 @@ class AccountController extends Controller
$range = Steam::filterAccountBalances($range, $account, $this->convertToPrimary, $accountCurrency);
Log::debug('Get and filter balance for entire range end');
// temp, get end balance.
Log::debug(sprintf('period: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
Steam::finalAccountBalance($account, $end);
Log::debug('END temp get end balance done');
// Log::debug(sprintf('period: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
// Steam::finalAccountBalance($account, $end);
// Log::debug('END temp get end balance done');
$previous = array_values($range)[0];
$accountCurrency ??= $this->primaryCurrency; // do this AFTER getting the balances.
@@ -666,10 +667,11 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug(sprintf('revAccounts: accountsBalancesInRange("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
[
$startBalances,
$endBalances,
] = Steam::accountsBalancesInRange($accounts, $start, $end, $this->primaryCurrency, $this->convertToPrimary);
// loop the accounts, then check for balance and currency info.

View File

@@ -38,6 +38,7 @@ use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* Class BoxController.
@@ -165,7 +166,7 @@ class BoxController extends Controller
$allAccounts = $accountRepository->getActiveAccountsByType(
[AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]
);
app('log')->debug(sprintf('Found %d accounts.', $allAccounts->count()));
Log::debug(sprintf('Found %d accounts.', $allAccounts->count()));
// filter list on preference of being included.
$filtered = $allAccounts->filter(
@@ -173,7 +174,7 @@ class BoxController extends Controller
$includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth');
$result = null === $includeNetWorth || '1' === $includeNetWorth;
if (false === $result) {
app('log')->debug(sprintf('Will not include "%s" in net worth charts.', $account->name));
Log::debug(sprintf('Will not include "%s" in net worth charts.', $account->name));
}
return $result;

View File

@@ -197,10 +197,16 @@ class ReconcileController extends Controller
$currency = $this->accountRepos->getAccountCurrency($account) ?? $this->primaryCurrency;
// correct
Log::debug(sprintf('transactions: Call finalAccountBalance with date/time "%s"', $startDate->toIso8601String()));
Log::debug(sprintf('transactions2: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
$startBalance = Steam::bcround(Steam::finalAccountBalance($account, $startDate)['balance'], $currency->decimal_places);
$endBalance = Steam::bcround(Steam::finalAccountBalance($account, $end)['balance'], $currency->decimal_places);
Log::debug(sprintf('transactions: Call accountsBalancesOptimized with date/time "%s"', $startDate->toIso8601String()));
Log::debug(sprintf('transactions2: Call accountsBalancesOptimized with date/time "%s"', $end->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized
// $startBalance = Steam::bcround(Steam::finalAccountBalance($account, $startDate)['balance'], $currency->decimal_places);
// $endBalance = Steam::bcround(Steam::finalAccountBalance($account, $end)['balance'], $currency->decimal_places);
$startBalance = Steam::accountsBalancesOptimized(new Collection()->push($account), $startDate)[$account->id];
$endBalance = Steam::accountsBalancesOptimized(new Collection()->push($account), $end)[$account->id];
// get the transactions
$selectionStart = clone $start;

View File

@@ -37,6 +37,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Update\JournalUpdateService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\ModelInformation;
use FireflyIII\Transformers\TransactionGroupTransformer;
@@ -45,6 +46,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
@@ -228,11 +230,13 @@ class ConvertController extends Controller
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getLiabilities: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date)['balance'];
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = 'l_'.$account->accountType->type;
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $account->name.' ('.app('amount')->formatAnything($currency, $balance, false).')';
$role = sprintf('l_%s', $account->accountType->type);
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;
@@ -252,15 +256,18 @@ class ConvertController extends Controller
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getAssetAccounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date)['balance'];
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
if ('' === $role) {
$role = 'no_account_type';
}
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $account->name.' ('.app('amount')->formatAnything($currency, $balance, false).')';
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;

View File

@@ -50,10 +50,11 @@ class AccountTasker implements AccountTaskerInterface, UserGroupInterface
$yesterday = clone $start;
$yesterday->subDay()->endOfDay(); // exactly up until $start but NOT including.
$end->endOfDay(); // needs to be end of day to be correct.
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $yesterday->format('Y-m-d H:i:s')));
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startSet = Steam::accountsBalancesOptimized($accounts, $yesterday);
$endSet = Steam::accountsBalancesOptimized($accounts, $end);
Log::debug(sprintf('getAccountReport: accountsBalancesInRange("%s", "%s")', $yesterday->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
[
$startSet,
$endSet,
] = Steam::accountsBalancesInRange($accounts, $yesterday, $end);
Log::debug('Start of accountreport');
/** @var AccountRepositoryInterface $repository */

View File

@@ -375,7 +375,9 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface, UserGroupInte
{
Log::debug(sprintf('leftOnAccount("%s","%s","%s")', $piggyBank->name, $account->name, $date->format('Y-m-d H:i:s')));
Log::debug(sprintf('leftOnAccount: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date)['balance'];
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'];
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
Log::debug(sprintf('Balance is: %s', $balance));

View File

@@ -139,7 +139,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
/** @var Rule $entry */
foreach ($set as $entry) {
if ($entry->order !== $count) {
app('log')->debug(sprintf('Rule #%d was on spot %d but must be on spot %d', $entry->id, $entry->order, $count));
Log::debug(sprintf('Rule #%d was on spot %d but must be on spot %d', $entry->id, $entry->order, $count));
$entry->order = $count;
$entry->save();
}
@@ -167,7 +167,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
if ($action->order !== $index) {
$action->order = $index;
$action->save();
app('log')->debug(sprintf('Rule action #%d was on spot %d but must be on spot %d', $action->id, $action->order, $index));
Log::debug(sprintf('Rule action #%d was on spot %d but must be on spot %d', $action->id, $action->order, $index));
}
++$index;
}
@@ -189,7 +189,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
if ($order !== $index) {
$trigger->order = $index;
$trigger->save();
app('log')->debug(sprintf('Rule trigger #%d was on spot %d but must be on spot %d', $trigger->id, $order, $index));
Log::debug(sprintf('Rule trigger #%d was on spot %d but must be on spot %d', $trigger->id, $order, $index));
}
++$index;
}
@@ -235,6 +235,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
public function getActiveStoreRules(RuleGroup $group): Collection
{
return $group->rules()
->orderBy('rules.order', 'ASC')
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
->where('rule_triggers.trigger_type', 'user_action')
->where('rule_triggers.trigger_value', 'store-journal')
@@ -275,23 +276,23 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
if (null === $filter) {
return $groups;
}
app('log')->debug(sprintf('Will filter getRuleGroupsWithRules on "%s".', $filter));
Log::debug(sprintf('Will filter getRuleGroupsWithRules on "%s".', $filter));
return $groups->map(
static function (RuleGroup $group) use ($filter) { // @phpstan-ignore-line
app('log')->debug(sprintf('Now filtering group #%d', $group->id));
Log::debug(sprintf('Now filtering group #%d', $group->id));
// filter the rules in the rule group:
$group->rules = $group->rules->filter(
static function (Rule $rule) use ($filter) {
app('log')->debug(sprintf('Now filtering rule #%d', $rule->id));
Log::debug(sprintf('Now filtering rule #%d', $rule->id));
foreach ($rule->ruleTriggers as $trigger) {
if ('user_action' === $trigger->trigger_type && $filter === $trigger->trigger_value) {
app('log')->debug(sprintf('Rule #%d triggers on %s, include it.', $rule->id, $filter));
Log::debug(sprintf('Rule #%d triggers on %s, include it.', $rule->id, $filter));
return true;
}
}
app('log')->debug(sprintf('Rule #%d does not trigger on %s, do not include it.', $rule->id, $filter));
Log::debug(sprintf('Rule #%d does not trigger on %s, do not include it.', $rule->id, $filter));
return false;
}
@@ -331,23 +332,23 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
if (null === $filter) {
return $groups;
}
app('log')->debug(sprintf('Will filter getRuleGroupsWithRules on "%s".', $filter));
Log::debug(sprintf('Will filter getRuleGroupsWithRules on "%s".', $filter));
return $groups->map(
static function (RuleGroup $group) use ($filter) { // @phpstan-ignore-line
app('log')->debug(sprintf('Now filtering group #%d', $group->id));
Log::debug(sprintf('Now filtering group #%d', $group->id));
// filter the rules in the rule group:
$group->rules = $group->rules->filter(
static function (Rule $rule) use ($filter) {
app('log')->debug(sprintf('Now filtering rule #%d', $rule->id));
Log::debug(sprintf('Now filtering rule #%d', $rule->id));
foreach ($rule->ruleTriggers as $trigger) {
if ('user_action' === $trigger->trigger_type && $filter === $trigger->trigger_value) {
app('log')->debug(sprintf('Rule #%d triggers on %s, include it.', $rule->id, $filter));
Log::debug(sprintf('Rule #%d triggers on %s, include it.', $rule->id, $filter));
return true;
}
}
app('log')->debug(sprintf('Rule #%d does not trigger on %s, do not include it.', $rule->id, $filter));
Log::debug(sprintf('Rule #%d does not trigger on %s, do not include it.', $rule->id, $filter));
return false;
}
@@ -414,7 +415,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
->decrement('order')
;
$ruleGroup->order = $newOrder;
app('log')->debug(sprintf('Order of group #%d ("%s") is now %d', $ruleGroup->id, $ruleGroup->title, $newOrder));
Log::debug(sprintf('Order of group #%d ("%s") is now %d', $ruleGroup->id, $ruleGroup->title, $newOrder));
$ruleGroup->save();
return;
@@ -425,7 +426,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
->increment('order')
;
$ruleGroup->order = $newOrder;
app('log')->debug(sprintf('Order of group #%d ("%s") is now %d', $ruleGroup->id, $ruleGroup->title, $newOrder));
Log::debug(sprintf('Order of group #%d ("%s") is now %d', $ruleGroup->id, $ruleGroup->title, $newOrder));
$ruleGroup->save();
}

View File

@@ -49,14 +49,14 @@ class RemoteUserGuard implements Guard
{
/** @var null|Request $request */
$request = $app->get('request');
Log::debug(sprintf('Created RemoteUserGuard for %s "%s"', $request?->getMethod(), $request?->getRequestUri()));
// Log::debug(sprintf('Created RemoteUserGuard for %s "%s"', $request?->getMethod(), $request?->getRequestUri()));
$this->application = $app;
$this->user = null;
}
public function authenticate(): void
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
if ($this->user instanceof User) {
Log::debug(sprintf('%s is found: #%d, "%s".', $this->user::class, $this->user->id, $this->user->email));
@@ -104,21 +104,21 @@ class RemoteUserGuard implements Guard
public function check(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
return $this->user() instanceof User;
}
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
return !$this->check();
}
public function hasUser(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
throw new FireflyException('Did not implement RemoteUserGuard::hasUser()');
}
@@ -128,14 +128,14 @@ class RemoteUserGuard implements Guard
*/
public function id(): int|string|null
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
return $this->user?->id;
}
public function setUser(Authenticatable|User|null $user): void // @phpstan-ignore-line
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
if ($user instanceof User) {
$this->user = $user;
@@ -146,7 +146,7 @@ class RemoteUserGuard implements Guard
public function user(): ?User
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
@@ -164,14 +164,14 @@ class RemoteUserGuard implements Guard
*/
public function validate(array $credentials = []): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
throw new FireflyException('Did not implement RemoteUserGuard::validate()');
}
public function viaRemember(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
// Log::debug(sprintf('Now at %s', __METHOD__));
return false;
}

View File

@@ -289,8 +289,10 @@ class AccountEnrichment implements EnrichmentInterface
{
$this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary);
if ($this->start instanceof Carbon && $this->end instanceof Carbon) {
$this->startBalances = Steam::accountsBalancesOptimized($this->collection, $this->start, $this->primaryCurrency, $this->convertToPrimary);
$this->endBalances = Steam::accountsBalancesOptimized($this->collection, $this->end, $this->primaryCurrency, $this->convertToPrimary);
[
$this->startBalances,
$this->endBalances,
] = Steam::accountsBalancesInRange($this->collection, $this->start, $this->end, $this->primaryCurrency, $this->convertToPrimary);
}
}

View File

@@ -402,21 +402,21 @@ trait ConvertsDataTypes
*/
protected function getCarbonDate(string $field): ?Carbon
{
$result = null;
$data = (string)$this->get($field);
Log::debug(sprintf('Date string is "%s"', $data));
Log::debug(sprintf('Date string is "%s"', (string)$this->get($field)));
if ('' === $data) {
return null;
}
try {
$result = '' !== (string)$this->get($field) ? new Carbon((string)$this->get($field), config('app.timezone')) : null;
return new Carbon($data, config('app.timezone'));
} catch (InvalidFormatException) {
// @ignoreException
Log::debug(sprintf('Exception when parsing date "%s".', $this->get($field)));
}
if (!$result instanceof Carbon) {
Log::debug(sprintf('Exception when parsing date "%s".', $this->get($field)));
Log::debug(sprintf('Exception when parsing date "%s".', $data));
}
return $result;
return null;
}
/**

View File

@@ -49,9 +49,9 @@ use function Safe\preg_replace;
*/
class Steam
{
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null, bool $inclusive = true): array
{
Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s" (inclusive: %s)', $accounts->count(), $date->toIso8601String(), var_export($inclusive, true)));
$result = [];
$convertToPrimary ??= Amount::convertToPrimary();
$primary ??= Amount::getPrimaryCurrency();
@@ -61,14 +61,15 @@ class Steam
$arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->where('transaction_journals.date', $inclusive ? '<=' : '<', $date->format('Y-m-d H:i:s'))
->groupBy(['transactions.account_id', 'transaction_currencies.code'])
->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
;
Log::debug('Array of sums: ', $arrayOfSums);
/** @var Account $account */
foreach ($accounts as $account) {
// this array is PER account, so we wait a bit before we change code here.
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
@@ -76,19 +77,19 @@ class Steam
$currency = $currencies[$account->id];
// second array
$accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
if (0 === count($accountSum)) {
$accountSums = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
if (0 === count($accountSums)) {
$result[$account->id] = $return;
continue;
}
$accountSum = array_values($accountSum)[0];
$sumOfAmount = (string)$accountSum['sum_of_amount'];
$sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
$sumsByCode = [
$accountSum['code'] => $sumOfAmount,
];
$sumsByCode = [];
foreach ($accountSums as $accountSum) {
// $accountSum = array_values($accountSum)[0];
$sumOfAmount = (string)$accountSum['sum_of_amount'];
$sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
$sumsByCode[$accountSum['code']] = $sumOfAmount;
}
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $sumsByCode[$currency->code] ?? '0';
@@ -96,6 +97,7 @@ class Steam
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
@@ -119,12 +121,23 @@ class Steam
}
$final = array_merge($return, $sumsByCode);
$result[$account->id] = $final;
// Log::debug('Final balance is', $final);
Log::debug(sprintf('Final balance for account #%d is', $account->id), $final);
}
return $result;
}
/**
* Calls accountsBalancesOptimized for the given accounts and makes sure that inclusive is set to false, so it properly gets the balance of a range.
*/
public function accountsBalancesInRange(Collection $accounts, Carbon $start, Carbon $end, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
return [
$this->accountsBalancesOptimized($accounts, $start, $primary, $convertToPrimary, inclusive: false),
$this->accountsBalancesOptimized($accounts, $end, $primary, $convertToPrimary),
];
}
/**
* https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers
*/
@@ -276,7 +289,9 @@ class Steam
}
/**
* Returns smaller than or equal to, so be careful with END OF DAY.
* @deprecated
* By default this method returns "smaller than or equal to", so be careful with END OF DAY.
* If you need end of day balance, use "inclusive = false".
*
* Returns the balance of an account at exact moment given. Array with at least one value.
* Always returns:
@@ -288,18 +303,17 @@ class Steam
* --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency.
* "EUR": balance in EUR (or whatever currencies the account has balance in)
*/
public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null, bool $inclusive = true): array
{
$cache = new CacheProperties();
$cache->addProperty($account->id);
$cache->addProperty($date);
if ($cache->has()) {
Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s, inclusive:%s)', $account->id, $date->format('Y-m-d H:i:s'), var_export($inclusive, true)));
// return $cache->get();
}
// Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
if (null === $convertToPrimary) {
$convertToPrimary = Amount::convertToPrimary($account->user);
}
@@ -312,7 +326,6 @@ class Steam
$accountCurrency = $account->meta['currency'];
}
if (!$currencyPresent) {
$accountCurrency = $this->getAccountCurrency($account);
}
$hasCurrency = null !== $accountCurrency;
@@ -325,11 +338,11 @@ class Steam
$array = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->where('transaction_journals.date', $inclusive ? '<=' : '<', $date->format('Y-m-d H:i:s'))
->get(['transaction_currencies.code', 'transactions.amount'])->toArray()
;
$others = $this->groupAndSumTransactions($array, 'code', 'amount');
// Log::debug('All balances are (joined)', $others);
Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $others[$currency->code] ?? '0';
if (!$convertToPrimary) {
@@ -338,7 +351,7 @@ class Steam
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert.
$return['pc_balance'] = $this->convertAllBalances($others, $primary, $date);
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
}
@@ -358,18 +371,21 @@ class Steam
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $others);
// Log::debug('Final balance is', $final);
Log::debug('Final balance is', $final);
$cache->store($final);
return $final;
}
/**
* Returns the balance for the given account in the range, with daily precision.
*/
public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToPrimary): array
{
// expand period.
$start->startOfDay();
$end->endOfDay();
Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
Log::debug(sprintf('called finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
// set up cache
$cache = new CacheProperties();
@@ -379,28 +395,21 @@ class Steam
$cache->addProperty($convertToPrimary);
$cache->addProperty($end);
if ($cache->has()) {
return $cache->get();
Log::debug('Return cached finalAccountBalanceInRange');
// return $cache->get();
}
$balances = [];
$formatted = $start->format('Y-m-d');
/*
* To make sure the start balance is correct, we need to get the balance at the exact end of the previous day.
* Since we just did "startOfDay" we can do subDay()->endOfDay() to get the correct moment.
* THAT will be the start balance.
*/
$request = clone $start;
$request->subDay()->endOfDay();
Log::debug('Get first balance to start.');
Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String()));
$startBalance = $this->finalAccountBalance($account, $request);
// 2025-10-08 replaced finalAccountBalance with accountsBalancesOptimized:
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
$startBalance = $this->accountsBalancesOptimized(new Collection()->push($account), $start, $primaryCurrency, $convertToPrimary, false)[$account->id];
$accountCurrency = $this->getAccountCurrency($account);
$hasCurrency = $accountCurrency instanceof TransactionCurrency;
$currency = $accountCurrency ?? $primaryCurrency;
Log::debug(sprintf('Currency is %s', $currency->code));
// set start balances:
$startBalance[$currency->code] ??= '0';
if ($hasCurrency) {
@@ -542,7 +551,7 @@ class Steam
try {
$hostName = gethostbyaddr($ipAddress);
} catch (Exception $e) {
app('log')->error($e->getMessage());
Log::error($e->getMessage());
$hostName = $ipAddress;
}
@@ -757,7 +766,7 @@ class Steam
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
}
$total = bcadd((string) $current, $total);
$total = bcadd((string)$current, $total);
}
return $total;

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\System;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Crypt;
@@ -63,10 +64,10 @@ class OAuthKeys
$privateKey = '';
$publicKey = '';
// better check if keys are in the database:
if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) {
if (FireflyConfig::has(self::PRIVATE_KEY) && FireflyConfig::has(self::PUBLIC_KEY)) {
try {
$privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
$privateKey = (string)FireflyConfig::get(self::PRIVATE_KEY)?->data;
$publicKey = (string)FireflyConfig::get(self::PUBLIC_KEY)?->data;
} catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) {
app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage()));
app('log')->error($e->getTraceAsString());
@@ -87,8 +88,8 @@ class OAuthKeys
*/
public static function restoreKeysFromDB(): bool
{
$privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
$privateKey = (string)FireflyConfig::get(self::PRIVATE_KEY)?->data;
$publicKey = (string)FireflyConfig::get(self::PUBLIC_KEY)?->data;
try {
$privateContent = Crypt::decrypt($privateKey);
@@ -98,8 +99,8 @@ class OAuthKeys
app('log')->error($e->getMessage());
// delete config vars from DB:
app('fireflyconfig')->delete(self::PRIVATE_KEY);
app('fireflyconfig')->delete(self::PUBLIC_KEY);
FireflyConfig::delete(self::PRIVATE_KEY);
FireflyConfig::delete(self::PUBLIC_KEY);
return false;
}
@@ -115,8 +116,8 @@ class OAuthKeys
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
FireflyConfig::set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
FireflyConfig::set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
}
public static function verifyKeysRoutine(): void

View File

@@ -30,6 +30,7 @@ use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Search\OperatorQuerySearch;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use League\CommonMark\GithubFlavoredMarkdownConverter;
@@ -155,9 +156,12 @@ class General extends AbstractExtension
}
/** @var Carbon $date */
$date = session('end', today(config('app.timezone'))->endOfMonth());
$date = now();
Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$info = Steam::finalAccountBalance($account, $date);
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
$info = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id];
// $info = Steam::finalAccountBalance($account, $date);
$currency = Steam::getAccountCurrency($account);
$primary = Amount::getPrimaryCurrency();
$convertToPrimary = Amount::convertToPrimary();

View File

@@ -506,9 +506,23 @@ class SearchRuleEngine implements RuleEngineInterface
*/
private function fireGroup(RuleGroup $group): void
{
Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $group->rules->count()));
$rules = $group->rules()->orderBy('order', 'ASC')->get();
$rules = [];
if ($group->relationLoaded('rules')) {
Log::debug('Group rules have been pre-loaded, do not reload them.');
$rules = $group->rules;
}
if (!$group->relationLoaded('rules')) {
Log::debug('Group rules have NOT been pre-loaded, load them NOW.');
$rules = $group->rules()
->orderBy('rules.order', 'ASC')
// ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
// ->where('rule_triggers.trigger_type', 'user_action')
// ->where('rule_triggers.trigger_value', 'store-journal')
->where('rules.active', true)
->get(['rules.*'])
;
}
Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $rules->count()));
/** @var Rule $rule */
foreach ($rules as $rule) {

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-10-07',
'build_time' => 1759859933,
'version' => 'develop/2025-10-08',
'build_time' => 1759931348,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

View File

@@ -53,6 +53,10 @@ class TransactionCurrencySeeder extends Seeder
$currencies[] = ['code' => 'BRL', 'name' => 'Brazilian real', 'symbol' => 'R$', 'decimal_places' => 2];
$currencies[] = ['code' => 'CAD', 'name' => 'Canadian dollar', 'symbol' => 'C$', 'decimal_places' => 2];
$currencies[] = ['code' => 'MXN', 'name' => 'Mexican peso', 'symbol' => 'MX$', 'decimal_places' => 2];
+ $currencies[] = ['code' => 'PEN', 'name' => 'Peruvian Sol', 'symbol' => 'S/', 'decimal_places' => 2];
+ $currencies[] = ['code' => 'ARS', 'name' => 'Argentinian Peso', 'symbol' => '$', 'decimal_places' => 2];
+ $currencies[] = ['code' => 'COP', 'name' => 'Colombian Peso', 'symbol' => '$', 'decimal_places' => 2];
+ $currencies[] = ['code' => 'CLP', 'name' => 'Chilean Peso', 'symbol' => '$', 'decimal_places' => 2];
// oceanian currencies
$currencies[] = ['code' => 'IDR', 'name' => 'Indonesian rupiah', 'symbol' => 'Rp', 'decimal_places' => 2];

28
package-lock.json generated
View File

@@ -4521,9 +4521,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001748",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
"integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
"version": "1.0.30001749",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
"integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
"dev": true,
"funding": [
{
@@ -5736,9 +5736,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.232",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz",
"integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==",
"version": "1.5.233",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz",
"integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==",
"dev": true,
"license": "ISC"
},
@@ -7908,13 +7908,17 @@
"license": "MIT"
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/loader-utils": {
@@ -10290,9 +10294,9 @@
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {