Compare commits

...

57 Commits

Author SHA1 Message Date
github-actions[bot]
7e344e4332 Merge pull request #11047 from firefly-iii/release-1760124177
🤖 Automatically merge the PR into the develop branch.
2025-10-10 21:23:04 +02:00
JC5
0a55e9fb4e 🤖 Auto commit for release 'develop' on 2025-10-10 2025-10-10 21:22:57 +02:00
James Cole
ed2e0e86dc Don't validate on empty string. 2025-10-10 20:54:51 +02:00
James Cole
9d1fb2cd6a Make sure that null|string passed is always a string 2025-10-10 20:54:31 +02:00
github-actions[bot]
57617b750f Merge pull request #11045 from firefly-iii/release-1760116335
🤖 Automatically merge the PR into the develop branch.
2025-10-10 19:12:23 +02:00
JC5
f6411fdc5a 🤖 Auto commit for release 'develop' on 2025-10-10 2025-10-10 19:12:15 +02:00
James Cole
02e24fc919 Whoops. 2025-10-10 19:07:28 +02:00
github-actions[bot]
dbc0210304 Merge pull request #11044 from firefly-iii/release-1760115302
🤖 Automatically merge the PR into the develop branch.
2025-10-10 18:55:11 +02:00
JC5
a709e224d4 🤖 Auto commit for release 'develop' on 2025-10-10 2025-10-10 18:55:02 +02:00
James Cole
83f3eddf44 Small code changes for @ctrl-f5's PR.
- Dropped some "setParameters" calls if unused in transformer.
- Add correct copyright
- Sprintf instead of string concat
- Small ident changes courtesy of phpstorm.
2025-10-10 18:50:39 +02:00
James Cole
7cfc4c2671 Merge pull request #11039 from ctrl-f5/feat/improve-request-objects
proposal for improved request handling
2025-10-10 18:38:02 +02:00
mergify[bot]
1c6055cb2d Merge branch 'develop' into feat/improve-request-objects 2025-10-10 14:42:25 +00:00
James Cole
4f4576e458 Add option to select date, fix #11042 2025-10-10 16:41:46 +02:00
mergify[bot]
84d3bcbb37 Merge branch 'develop' into feat/improve-request-objects 2025-10-10 11:33:40 +00:00
github-actions[bot]
c91c87d646 Merge pull request #11043 from firefly-iii/release-1760095977
🤖 Automatically merge the PR into the develop branch.
2025-10-10 13:33:06 +02:00
JC5
e09b6034f7 🤖 Auto commit for release 'develop' on 2025-10-10 2025-10-10 13:32:57 +02:00
mergify[bot]
ad67bb80f3 Merge branch 'develop' into feat/improve-request-objects 2025-10-10 11:29:39 +00:00
Sander Dorigo
a88d0de34d Merge branch 'develop' of https://github.com/firefly-iii/firefly-iii into develop 2025-10-10 13:28:56 +02:00
Sander Dorigo
ecf498cc81 Add more options 2025-10-10 13:18:47 +02:00
mergify[bot]
569f553d26 Merge branch 'develop' into feat/improve-request-objects 2025-10-10 06:58:56 +00:00
James Cole
7d45bc46b8 Merge pull request #11041 from jreyesr/patch-1
Add XML mimetypes to the allowedMimes list
2025-10-10 08:58:18 +02:00
mergify[bot]
08553fcfb2 Merge branch 'develop' into patch-1 2025-10-10 06:47:30 +00:00
mergify[bot]
58c76bee94 Merge branch 'develop' into feat/improve-request-objects 2025-10-10 06:46:55 +00:00
mergify[bot]
3fa1b6dd27 Merge branch 'develop' into patch-1 2025-10-10 06:46:54 +00:00
Sander Dorigo
63aa8adab7 Import forgotten class 2025-10-10 08:46:47 +02:00
Sander Dorigo
70b8ea0acb Basic check on numbers, needs improv still. 2025-10-10 08:46:11 +02:00
jreyesr
d800a01e33 Add XML mimetypes to the allowedMimes list
Signed-off-by: jreyesr <jreyesr@users.noreply.github.com>
2025-10-09 15:40:07 -05:00
Nicky De Maeyer
52f3ec7d3d improved request handling 2025-10-09 15:05:52 +02:00
Sander Dorigo
d4978a09ee Fix #11038 2025-10-09 12:35:51 +02:00
github-actions[bot]
77095276e2 Merge pull request #11037 from firefly-iii/release-1759999962
🤖 Automatically merge the PR into the develop branch.
2025-10-09 10:52:52 +02:00
JC5
b77a8591dc 🤖 Auto commit for release 'develop' on 2025-10-09 2025-10-09 10:52:43 +02:00
James Cole
132d7d9ff8 Update some logging. 2025-10-09 06:27:15 +02:00
github-actions[bot]
a981e2c5cb Merge pull request #11034 from firefly-iii/release-1759932290
🤖 Automatically merge the PR into the develop branch.
2025-10-08 16:05:00 +02:00
JC5
77b88b7758 🤖 Auto commit for release 'develop' on 2025-10-08 2025-10-08 16:04:50 +02:00
James Cole
b9894eea57 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-10-08 15:58:43 +02:00
James Cole
469319a240 Remove plusses. 2025-10-08 15:56:33 +02:00
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
50 changed files with 804 additions and 493 deletions

View File

@@ -4,6 +4,8 @@ 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
- jreyesr
- 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

@@ -67,6 +67,8 @@ abstract class Controller extends BaseController
protected bool $convertToPrimary = false;
protected TransactionCurrency $primaryCurrency;
/** @deprecated use Request classes */
protected ParameterBag $parameters;
/**
@@ -98,7 +100,8 @@ abstract class Controller extends BaseController
}
/**
* Method to grab all parameters from the URL.
* @deprecated use Request classes
* Method to grab all parameters from the URL
*/
private function getParameters(): ParameterBag
{

View File

@@ -71,45 +71,45 @@ class ShowController extends Controller
public function index(ShowRequest $request): JsonResponse
{
$manager = $this->getManager();
$params = $request->getParameters();
$this->parameters->set('type', $params['type']);
// types to get, page size:
$types = $this->mapAccountTypes($params['type']);
[
'types' => $types,
'page' => $page,
'limit' => $limit,
'offset' => $offset,
'sort' => $sort,
'start' => $start,
'end' => $end,
'date' => $date,
]
= $request->attributes->all();
// get list of accounts. Count it and split it.
$this->repository->resetAccountOrder();
$collection = $this->repository->getAccountsByType($types, $params['sort']);
$collection = $this->repository->getAccountsByType($types, $sort);
$count = $collection->count();
// continue sort:
// TODO if the user sorts on DB dependent field there must be no slice before enrichment, only after.
// TODO still need to figure out how to do this easily.
$accounts = $collection->slice(($this->parameters->get('page') - 1) * $params['limit'], $params['limit']);
// #11007 go to the end of the previous day.
$this->parameters->set('start', $this->parameters->get('start')?->subSecond());
// #11018 also end of the day.
$this->parameters->set('end', $this->parameters->get('end')?->endOfDay());
$accounts = $collection->slice($offset, $limit);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new AccountEnrichment();
$enrichment->setSort($params['sort']);
$enrichment->setDate($this->parameters->get('date'));
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$enrichment->setSort($sort);
$enrichment->setDate($date);
$enrichment->setStart($start);
$enrichment->setEnd($end);
$enrichment->setUser($admin);
$accounts = $enrichment->enrich($accounts);
// make paginator:
$paginator = new LengthAwarePaginator($accounts, $count, $params['limit'], $this->parameters->get('page'));
$paginator = new LengthAwarePaginator($accounts, $count, $limit, $page);
$paginator->setPath(route('api.v1.accounts.index').$this->buildParams());
/** @var AccountTransformer $transformer */
$transformer = app(AccountTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($accounts, $transformer, self::RESOURCE_KEY);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
@@ -128,28 +128,25 @@ class ShowController extends Controller
// get list of accounts. Count it and split it.
$this->repository->resetAccountOrder();
$account->refresh();
$manager = $this->getManager();
// #11007 go to the end of the previous day.
$this->parameters->set('start', $this->parameters->get('start')?->subSecond());
// #11018 also end of the day.
$this->parameters->set('end', $this->parameters->get('end')?->endOfDay());
$manager = $this->getManager();
['start' => $start,
'end' => $end,
'date' => $date,] = $request->attributes->all();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new AccountEnrichment();
$enrichment->setDate($this->parameters->get('date'));
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$admin = auth()->user();
$enrichment = new AccountEnrichment();
$enrichment->setDate($date);
$enrichment->setStart($start);
$enrichment->setEnd($end);
$enrichment->setUser($admin);
$account = $enrichment->enrichSingle($account);
$account = $enrichment->enrichSingle($account);
/** @var AccountTransformer $transformer */
$transformer = app(AccountTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($account, $transformer, self::RESOURCE_KEY);
$transformer = app(AccountTransformer::class);
$resource = new Item($account, $transformer, self::RESOURCE_KEY);
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -25,7 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\UserGroup;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Api\V1\Requests\PaginationRequest;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Transformers\UserGroupTransformer;
use Illuminate\Http\JsonResponse;
@@ -52,17 +52,19 @@ class IndexController extends Controller
);
}
public function index(DateRequest $request): JsonResponse
public function index(PaginationRequest $request): JsonResponse
{
$administrations = $this->repository->get();
$pageSize = $this->parameters->get('limit');
[
'page' => $page,
'limit' => $limit,
'offset' => $offset,
] = $request->attributes->all();
$count = $administrations->count();
$administrations = $administrations->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$paginator = new LengthAwarePaginator($administrations, $count, $pageSize, $this->parameters->get('page'));
$administrations = $administrations->slice($offset, $limit);
$paginator = new LengthAwarePaginator($administrations, $count, $limit, $page);
$transformer = new UserGroupTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)

View File

@@ -27,7 +27,7 @@ namespace FireflyIII\Api\V1\Controllers\Summary;
use Carbon\Carbon;
use Exception;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Api\V1\Requests\DateRangeRequest;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
@@ -94,10 +94,10 @@ class BasicController extends Controller
*
* @throws Exception
*/
public function basic(DateRequest $request): JsonResponse
public function basic(DateRangeRequest $request): JsonResponse
{
// parameters for boxes:
$dates = $request->getAll();
$dates = $request->attributes->all();
$start = $dates['start'];
$end = $dates['end'];
$code = $request->get('currency_code');
@@ -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

@@ -0,0 +1,92 @@
<?php
/*
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Validation\Validator;
use RuntimeException;
abstract class AggregateFormRequest extends ApiRequest
{
/**
* @var ApiRequest[]
*/
protected array $requests = [];
/** @return class-string[] */
abstract protected function getRequests(): array;
public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): void
{
parent::initialize($query, $request, $attributes, $cookies, $files, $server, $content);
// instantiate all subrequests and share current requests' bags with them
foreach ($this->getRequests() as $config) {
$requestClass = is_array($config) ? array_shift($config) : $config;
if (!is_a($requestClass, Request::class, true)) {
throw new RuntimeException('getRequests() must return class-strings of subclasses of Request');
}
$instance = $this->requests[] = new $requestClass();
$instance->request = $this->request;
$instance->query = $this->query;
$instance->attributes = $this->attributes;
$instance->cookies = $this->cookies;
$instance->files = $this->files;
$instance->server = $this->server;
$instance->headers = $this->headers;
if ($instance instanceof ApiRequest) {
$instance->handleConfig(is_array($config) ? $config : []);
}
}
}
public function rules(): array
{
// check all subrequests for rules and combine them
return array_reduce(
$this->requests,
static fn (array $rules, FormRequest $request) => $rules
+ (
method_exists($request, 'rules')
? $request->rules()
: []
),
[],
);
}
public function withValidator(Validator $validator): void
{
// register all subrequests' validators
foreach ($this->requests as $request) {
if (method_exists($request, 'withValidator')) {
$request->withValidator($validator);
}
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
class ApiRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
protected string $required = '';
public function handleConfig(array $config): void
{
if (in_array('required', $config, true)) {
$this->required = 'required';
}
}
}

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

@@ -1,83 +0,0 @@
<?php
/*
* DateRequest.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Data;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
/**
* Request class for end points that require date parameters.
*
* Class DateRequest
*/
class DateRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
/**
* Get all data from the request.
*/
public function getAll(): array
{
$start = $this->getCarbonDate('start');
$end = $this->getCarbonDate('end');
if (null === $start) {
$start = now()->startOfMonth();
}
if (null === $end) {
$end = now()->endOfMonth();
}
// sanity check on dates:
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$start->startOfDay();
$end->endOfDay();
if ($start->diffInYears($end, true) > 5) {
throw new ValidationException('Date range out of range.');
}
return [
'start' => $start,
'end' => $end,
'date' => $this->getCarbonDate('date'),
];
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
return [
'date' => 'date|after:1970-01-02|before:2038-01-17',
'start' => 'date|after:1970-01-02|before:2038-01-17|before:end|required_with:end',
'end' => 'date|after:1970-01-02|before:2038-01-17|after:start|required_with:start',
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use Illuminate\Validation\Validator;
class DateRangeRequest extends ApiRequest
{
public function rules(): array
{
return [
'start' => sprintf('date|after:1970-01-02|before:2038-01-17|before:end|required_with:end|', $this->required),
'end' => sprintf('date|after:1970-01-02|before:2038-01-17|after:start|required_with:start|', $this->required),
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (!$validator->valid()) {
return;
}
$start = $this->getCarbonDate('start')?->startOfDay();
$end = $this->getCarbonDate('end')?->endOfDay();
$this->attributes->set('start', $start);
$this->attributes->set('end', $end);
}
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use Illuminate\Validation\Validator;
class DateRequest extends ApiRequest
{
public function rules(): array
{
return [
'date' => 'date|after:1970-01-02|before:2038-01-17|'.$this->required,
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (!$validator->valid()) {
return;
}
$date = $this->getCarbonDate('date')?->endOfDay();
// if we also have a range, date must be in that range
$start = $this->attributes->get('start');
$end = $this->attributes->get('end');
if ($date && $start && $end && !$date->between($start, $end)) {
$validator->errors()->add('date', (string)trans('validation.between_date'));
}
$this->attributes->set('date', $date);
}
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* AccountTypeApiRequest.php
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Requests\Models\Account;
use FireflyIII\Api\V1\Requests\ApiRequest;
use FireflyIII\Support\Http\Api\AccountFilter;
use Illuminate\Validation\Validator;
class AccountTypeApiRequest extends ApiRequest
{
use AccountFilter;
public function rules(): array
{
return [
'type' => sprintf('in:%s', implode(',', array_keys($this->types))),
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (!$validator->valid()) {
return;
}
$type = $this->convertString('type', 'all');
$this->attributes->add([
'type' => $type,
'types' => $this->mapAccountTypes($type),
]);
}
);
}
}

View File

@@ -23,77 +23,21 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Account;
use Carbon\Carbon;
use FireflyIII\Api\V1\Requests\AggregateFormRequest;
use FireflyIII\Api\V1\Requests\DateRangeRequest;
use FireflyIII\Api\V1\Requests\DateRequest;
use FireflyIII\Api\V1\Requests\PaginationRequest;
use FireflyIII\Models\Account;
use FireflyIII\Rules\IsValidSortInstruction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class ShowRequest extends FormRequest
class ShowRequest extends AggregateFormRequest
{
use AccountFilter;
use ConvertsDataTypes;
public function getParameters(): array
protected function getRequests(): array
{
$limit = $this->convertInteger('limit');
if (0 === $limit) {
// get default for user:
/** @var User $user */
$user = auth()->user();
$limit = (int)Preferences::getForUser($user, 'listPageSize', 50)->data;
}
$page = $this->convertInteger('page');
$page = min(max(1, $page), 2 ** 16);
return [
'type' => $this->convertString('type', 'all'),
'limit' => $limit,
'sort' => $this->convertSortParameters('sort', Account::class),
'page' => $page,
[PaginationRequest::class, 'sort_class' => Account::class],
DateRangeRequest::class,
DateRequest::class,
AccountTypeApiRequest::class,
];
}
public function rules(): array
{
$keys = implode(',', array_keys($this->types));
return [
'date' => 'date',
'start' => 'date|present_with:end|before_or_equal:end|before:2038-01-17|after:1970-01-02',
'end' => 'date|present_with:start|after_or_equal:start|before:2038-01-17|after:1970-01-02',
'sort' => ['nullable', new IsValidSortInstruction(Account::class)],
'type' => sprintf('in:%s', $keys),
'limit' => 'numeric|min:1|max:131337',
'page' => 'numeric|min:1|max:131337',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (count($validator->failed()) > 0) {
return;
}
$data = $validator->getData();
if (array_key_exists('date', $data) && array_key_exists('start', $data) && array_key_exists('end', $data)) {
// assume valid dates, before we got here.
$start = Carbon::parse($data['start'], config('app.timezone'))->startOfDay();
$end = Carbon::parse($data['end'], config('app.timezone'))->endOfDay();
$date = Carbon::parse($data['date'], config('app.timezone'));
if (!$date->between($start, $end)) {
$validator->errors()->add('date', (string)trans('validation.between_date'));
}
}
}
);
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* Copyright (c) 2025 https://github.com/ctrl-f5
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use FireflyIII\Rules\IsValidSortInstruction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Validation\Validator;
use RuntimeException;
class PaginationRequest extends ApiRequest
{
private ?string $sortClass = null;
public function handleConfig(array $config): void
{
parent::handleConfig($config);
$this->sortClass = $config['sort_class'] ?? null;
if (!$this->sortClass) {
throw new RuntimeException('PaginationRequest requires a sort_class config');
}
}
public function rules(): array
{
return [
'sort' => ['nullable', new IsValidSortInstruction((string)$this->sortClass)],
'limit' => 'numeric|min:1|max:131337',
'page' => 'numeric|min:1|max:131337',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (!$validator->valid()) {
return;
}
$limit = $this->convertInteger('limit');
if (0 === $limit) {
// get default for user:
/** @var User $user */
$user = auth()->user();
$limit = (int)Preferences::getForUser($user, 'listPageSize', 50)->data;
}
$page = $this->convertInteger('page');
$page = min(max(1, $page), 2 ** 16);
$offset = ($page - 1) * $limit;
$sort = $this->sortClass ? $this->convertSortParameters('sort', $this->sortClass) : $this->get('sort');
$this->attributes->set('limit', $limit);
$this->attributes->set('sort', $sort);
$this->attributes->set('page', $page);
$this->attributes->set('offset', $offset);
}
);
}
}

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Factory;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use Illuminate\Support\Facades\Log;
/**
* Create piggy bank events.
@@ -36,9 +37,9 @@ class PiggyBankEventFactory
{
public function create(TransactionJournal $journal, ?PiggyBank $piggyBank): void
{
app('log')->debug(sprintf('Now in PiggyBankEventCreate for a %s', $journal->transactionType->type));
Log::debug(sprintf('Now in PiggyBankEventCreate for a %s', $journal->transactionType->type));
if (!$piggyBank instanceof PiggyBank) {
app('log')->debug('Piggy bank is null');
Log::debug('Piggy bank is null');
return;
}
@@ -49,7 +50,7 @@ class PiggyBankEventFactory
$amount = $piggyRepos->getExactAmount($piggyBank, $journal);
if (0 === bccomp($amount, '0')) {
app('log')->debug('Amount is zero, will not create event.');
Log::debug('Amount is zero, will not create event.');
return;
}

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

@@ -81,6 +81,12 @@ class ReconcileController extends Controller
if (!$start instanceof Carbon && !$end instanceof Carbon) {
throw new FireflyException('Invalid dates submitted.');
}
if (!is_numeric($startBalance)) {
$startBalance = '0';
}
if (!is_numeric($endBalance)) {
$endBalance = '0';
}
if ($end->lt($start)) {
[$start, $end] = [$end, $start];
}
@@ -197,10 +203,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

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Rule;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\SelectTransactionsRequest;
@@ -56,7 +57,7 @@ class SelectController extends Controller
$this->middleware(
static function ($request, $next) {
app('view')->share('title', (string) trans('firefly.rules'));
app('view')->share('title', (string)trans('firefly.rules'));
app('view')->share('mainTitleIcon', 'fa-random');
return $next($request);
@@ -73,11 +74,20 @@ class SelectController extends Controller
/** @var User $user */
$user = auth()->user();
$accounts = implode(',', $request->get('accounts'));
// create new rule engine:
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
// add date operators.
if (null !== $request->get('start')) {
$startDate = new Carbon($request->get('start'));
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
}
if (null !== $request->get('end')) {
$endDate = new Carbon($request->get('end'));
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
}
// add extra operators:
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);
@@ -102,7 +112,7 @@ class SelectController extends Controller
return redirect(route('rules.index'));
}
// does the user have shared accounts?
$subTitle = (string) trans('firefly.apply_rule_selection', ['title' => $rule->title]);
$subTitle = (string)trans('firefly.apply_rule_selection', ['title' => $rule->title]);
return view('rules.rule.select-transactions', compact('rule', 'subTitle'));
}
@@ -127,7 +137,7 @@ class SelectController extends Controller
// warn if nothing.
if (0 === count($textTriggers)) {
return response()->json(['html' => '', 'warning' => (string) trans('firefly.warning_no_valid_triggers')]);
return response()->json(['html' => '', 'warning' => (string)trans('firefly.warning_no_valid_triggers')]);
}
foreach ($textTriggers as $textTrigger) {
@@ -160,7 +170,7 @@ class SelectController extends Controller
// Warn the user if only a subset of transactions is returned
$warning = '';
if (0 === count($collection)) {
$warning = (string) trans('firefly.warning_no_matching_transactions');
$warning = (string)trans('firefly.warning_no_matching_transactions');
}
// Return json response
@@ -190,7 +200,7 @@ class SelectController extends Controller
$triggers = $rule->ruleTriggers;
if (0 === count($triggers)) {
return response()->json(['html' => '', 'warning' => (string) trans('firefly.warning_no_valid_triggers')]);
return response()->json(['html' => '', 'warning' => (string)trans('firefly.warning_no_valid_triggers')]);
}
// create new rule engine:
$newRuleEngine = app(RuleEngineInterface::class);
@@ -202,7 +212,7 @@ class SelectController extends Controller
$warning = '';
if (0 === count($collection)) {
$warning = (string) trans('firefly.warning_no_matching_transactions');
$warning = (string)trans('firefly.warning_no_matching_transactions');
}
// Return json response

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\RuleGroup;
use Carbon\Carbon;
use Exception;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\SelectTransactionsRequest;
@@ -73,6 +74,16 @@ class ExecutionController extends Controller
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
// add date operators.
if (null !== $request->get('start')) {
$startDate = new Carbon($request->get('start'));
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
}
if (null !== $request->get('end')) {
$endDate = new Carbon($request->get('end'));
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
}
// add extra operators:
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);

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')
@@ -262,6 +263,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
[ // @phpstan-ignore-line
'rules' => static function (HasMany $query): void {
$query->orderBy('order', 'ASC');
$query->where('rules.active', true);
},
'rules.ruleTriggers' => static function (HasMany $query): void {
$query->orderBy('order', 'ASC');
@@ -275,23 +277,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;
}
@@ -318,6 +320,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface, UserGroupInte
[ // @phpstan-ignore-line
'rules' => static function (HasMany $query): void {
$query->orderBy('order', 'ASC');
$query->where('rules.active', true);
},
'rules.ruleTriggers' => static function (HasMany $query): void {
$query->orderBy('order', 'ASC');
@@ -331,23 +334,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 +417,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 +428,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

@@ -38,6 +38,11 @@ class IsValidSortInstruction implements ValidationRule
return;
}
if ('' === $value) {
// don't validate.
return;
}
$validParameters = config(sprintf('firefly.allowed_sort_parameters.%s', $shortClass));
if (!is_array($validParameters)) {
$fail('validation.no_sort_instructions')->translate(['object' => $shortClass]);

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

@@ -420,6 +420,7 @@ class Navigation
'week' => (string)trans('config.week_in_year_js'),
'weekly' => (string)trans('config.week_in_year_js'),
'1M' => (string)trans('config.month_js'),
'MTD' => (string)trans('config.month_js'),
'month' => (string)trans('config.month_js'),
'monthly' => (string)trans('config.month_js'),
'1Y' => (string)trans('config.year_js'),
@@ -427,6 +428,11 @@ class Navigation
'year' => (string)trans('config.year_js'),
'yearly' => (string)trans('config.year_js'),
'6M' => (string)trans('config.half_year_js'),
'last7' => (string)trans('config.specific_day_js'),
'last30' => (string)trans('config.month_js'),
'last90' => (string)trans('config.month_js'),
'last365' => (string)trans('config.year_js'),
'QTD' => (string)trans('config.month_js'),
];
if (array_key_exists($repeatFrequency, $formatMap)) {

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

@@ -59,9 +59,7 @@ class UpdatePiggyBank implements ActionInterface
$piggyBank = $this->findPiggyBank($user, $actionValue);
if (!$piggyBank instanceof PiggyBank) {
Log::info(
sprintf('No piggy bank named "%s", cant execute action #%d of rule #%d', $actionValue, $this->action->id, $this->action->rule_id)
);
Log::info(sprintf('No piggy bank named "%s", cant execute action #%d of rule #%d', $actionValue, $this->action->id, $this->action->rule_id));
event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.cannot_find_piggy', ['name' => $actionValue])));
return false;

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

@@ -34,11 +34,17 @@ abstract class AbstractTransformer extends TransformerAbstract
{
protected ParameterBag $parameters;
/**
* @deprecated
*/
final public function getParameters(): ParameterBag
{
return $this->parameters;
}
/**
* @deprecated
*/
final public function setParameters(ParameterBag $parameters): void
{
$this->parameters = $parameters;

View File

@@ -30,7 +30,6 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* Class AccountTransformer
@@ -46,7 +45,6 @@ class AccountTransformer extends AbstractTransformer
*/
public function __construct()
{
$this->parameters = new ParameterBag();
$this->repository = app(AccountRepositoryInterface::class);
$this->convertToPrimary = Amount::convertToPrimary();
$this->primary = Amount::getPrimaryCurrency();

58
composer.lock generated
View File

@@ -2485,34 +2485,34 @@
},
{
"name": "lcobucci/clock",
"version": "3.3.1",
"version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/clock.git",
"reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b"
"reference": "f91d84f65cb3e974988bbe872b5da8ca132a155f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b",
"reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b",
"url": "https://api.github.com/repos/lcobucci/clock/zipball/f91d84f65cb3e974988bbe872b5da8ca132a155f",
"reference": "f91d84f65cb3e974988bbe872b5da8ca132a155f",
"shasum": ""
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"php": "~8.3.0 || ~8.4.0",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"infection/infection": "^0.29",
"infection/infection": "^0.31",
"lcobucci/coding-standard": "^11.1.0",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.10.25",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.13",
"phpstan/phpstan-strict-rules": "^1.5.1",
"phpunit/phpunit": "^11.3.6"
"phpstan/phpstan": "^2.0.0",
"phpstan/phpstan-deprecation-rules": "^2.0.0",
"phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^12.0.0"
},
"type": "library",
"autoload": {
@@ -2533,7 +2533,7 @@
"description": "Yet another clock abstraction",
"support": {
"issues": "https://github.com/lcobucci/clock/issues",
"source": "https://github.com/lcobucci/clock/tree/3.3.1"
"source": "https://github.com/lcobucci/clock/tree/3.4.0"
},
"funding": [
{
@@ -2545,7 +2545,7 @@
"type": "patreon"
}
],
"time": "2024-09-24T20:45:14+00:00"
"time": "2025-10-08T18:00:48+00:00"
},
{
"name": "lcobucci/jwt",
@@ -11333,11 +11333,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.30",
"version": "2.1.31",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
"reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96",
"reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96",
"shasum": ""
},
"require": {
@@ -11382,7 +11382,7 @@
"type": "github"
}
],
"time": "2025-10-02T16:07:52+00:00"
"time": "2025-10-10T14:14:11+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -11815,16 +11815,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.4.0",
"version": "12.4.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "f62aab5794e36ccd26860db2d1bbf89ac19028d9"
"reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f62aab5794e36ccd26860db2d1bbf89ac19028d9",
"reference": "f62aab5794e36ccd26860db2d1bbf89ac19028d9",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc5413a2e6d240d2f6d9317bdf7f0a24e73de194",
"reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194",
"shasum": ""
},
"require": {
@@ -11892,7 +11892,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.0"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.1"
},
"funding": [
{
@@ -11916,20 +11916,20 @@
"type": "tidelift"
}
],
"time": "2025-10-03T04:28:03+00:00"
"time": "2025-10-09T14:08:29+00:00"
},
{
"name": "rector/rector",
"version": "2.2.1",
"version": "2.2.2",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "e1aaf3061e9ae9342ed0824865e3a3360defddeb"
"reference": "5b353f7457b9a0c63fc91ef340f5d119a40991ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/e1aaf3061e9ae9342ed0824865e3a3360defddeb",
"reference": "e1aaf3061e9ae9342ed0824865e3a3360defddeb",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/5b353f7457b9a0c63fc91ef340f5d119a40991ed",
"reference": "5b353f7457b9a0c63fc91ef340f5d119a40991ed",
"shasum": ""
},
"require": {
@@ -11968,7 +11968,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.2.1"
"source": "https://github.com/rectorphp/rector/tree/2.2.2"
},
"funding": [
{
@@ -11976,7 +11976,7 @@
"type": "github"
}
],
"time": "2025-10-06T21:25:14+00:00"
"time": "2025-10-09T19:50:20+00:00"
},
{
"name": "sebastian/cli-parser",

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-10',
'build_time' => 1760124066,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.
@@ -225,6 +225,8 @@ return [
// plain files
'text/plain',
'text/html',
'text/xml',
'application/xml',
// images
'image/jpeg',

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];

54
package-lock.json generated
View File

@@ -3173,9 +3173,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz",
"integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
"version": "24.7.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4075,9 +4075,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.13",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz",
"integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -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": [
{
@@ -4966,13 +4966,13 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz",
"integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==",
"version": "3.46.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
"integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.25.3"
"browserslist": "^4.26.3"
},
"funding": {
"type": "opencollective",
@@ -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.234",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz",
"integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==",
"dev": true,
"license": "ISC"
},
@@ -7088,9 +7088,9 @@
}
},
"node_modules/i18next": {
"version": "25.5.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz",
"integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==",
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz",
"integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==",
"funding": [
{
"type": "individual",
@@ -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": {

View File

@@ -24,6 +24,8 @@
</p>
<div class="row">
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-12">
{{ ExpandedForm.date('start') }}
{{ ExpandedForm.date('end') }}
{{ AccountForm.assetAccountCheckList('accounts', {'select_all': true,'class': 'account-checkbox', 'label': trans('firefly.include_transactions_from_accounts') }) }}
</div>
</div>

View File

@@ -23,6 +23,8 @@
</p>
<div class="row">
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-12">
{{ ExpandedForm.date('start') }}
{{ ExpandedForm.date('end') }}
{{ AccountForm.assetAccountCheckList('accounts', {'select_all': true, 'class': 'account-checkbox', 'label': trans('firefly.include_transactions_from_accounts') }) }}
</div>
</div>