Compare commits

..

23 Commits

Author SHA1 Message Date
github-actions[bot]
3a9d89b53d Merge pull request #11497 from firefly-iii/release-1768064652
🤖 Automatically merge the PR into the develop branch.
2026-01-10 18:04:21 +01:00
JC5
badff64cfd 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 18:04:12 +01:00
James Cole
abacfa212e Throw a 410. Don't report it. 2026-01-10 18:00:18 +01:00
github-actions[bot]
add2e859c4 Merge pull request #11496 from firefly-iii/release-1768064331
🤖 Automatically merge the PR into the develop branch.
2026-01-10 17:58:57 +01:00
JC5
92f6421fc4 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 17:58:51 +01:00
James Cole
f350c19ec1 Add some debug info. 2026-01-10 17:52:45 +01:00
James Cole
8170804d74 No error, just 404. 2026-01-10 17:50:54 +01:00
James Cole
4400f6217d Add debug logging. 2026-01-10 17:50:47 +01:00
James Cole
e223cea74e Add more feedback. 2026-01-10 17:42:51 +01:00
James Cole
2f62e11338 Add some info on the user's input. 2026-01-10 17:38:03 +01:00
github-actions[bot]
d96c7931d6 Merge pull request #11494 from firefly-iii/release-1768053858
🤖 Automatically merge the PR into the develop branch.
2026-01-10 15:04:28 +01:00
JC5
2ab105a902 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 15:04:18 +01:00
James Cole
7ced1f8cf3 Add debug logging for https://github.com/orgs/firefly-iii/discussions/11431 2026-01-10 14:35:14 +01:00
James Cole
03364d9530 More strict check on transaction journal type. 2026-01-10 08:19:10 +01:00
James Cole
1f75612741 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-10 08:16:47 +01:00
James Cole
a141cf6e67 Remove bills from transfers in more places. 2026-01-10 08:16:41 +01:00
James Cole
c97fb07e8d Group tags by date. 2026-01-10 07:37:22 +01:00
James Cole
9833dd49a9 Merge pull request #11483 from pilipovicn/add-rsd-currency 2026-01-09 11:59:26 +01:00
embedded
b76f4fe7b9 Add Serbian Dinar to Currency Seeder 2026-01-09 11:32:04 +01:00
James Cole
6c114e2ffc Throw better error 2026-01-09 06:07:50 +01:00
James Cole
bd396673ed Fix bad header exception. 2026-01-09 05:58:05 +01:00
James Cole
ad72bc1722 Fix #11479 2026-01-09 05:57:55 +01:00
James Cole
466b42200d Fix #11473 2026-01-07 20:53:44 +01:00
24 changed files with 209 additions and 84 deletions

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.92.4",
"version": "v3.92.5",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "9e7488b19403423e02e8403cc1eb596baf4673b0"
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9e7488b19403423e02e8403cc1eb596baf4673b0",
"reference": "9e7488b19403423e02e8403cc1eb596baf4673b0",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"shasum": ""
},
"require": {
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.4"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2026-01-04T00:38:52+00:00"
"time": "2026-01-08T21:57:37+00:00"
},
{
"name": "psr/container",

View File

@@ -3,6 +3,9 @@
Over time, many people have contributed to Firefly III. Their efforts are not always visible, but always remembered and appreciated.
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2026
- embedded
## 2025
- Diego Algorta
- Jihad

View File

@@ -44,6 +44,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use League\Fractal\Resource\Item;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Class StoreController
@@ -133,7 +134,7 @@ class StoreController extends Controller
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new FireflyException('200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
throw HttpException::fromStatusCode(410, '200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
// enrich

View File

@@ -34,7 +34,7 @@ class RemovesBills extends Command
{
use ShowsFriendlyMessages;
protected $description = 'Remove bills from transactions that shouldn\'t have one.';
protected $description = 'Remove subscriptions from transactions that shouldn\'t have one.';
protected $signature = 'correction:bills';
/**

View File

@@ -198,15 +198,29 @@ class ApplyRules extends Command
$accountRepository = app(AccountRepositoryInterface::class);
$accountRepository->setUser($this->getUser());
foreach ($accountList as $accountId) {
$accountId = (int) $accountId;
$account = $accountRepository->find($accountId);
if (null !== $account && in_array($account->accountType->type, $this->acceptedAccounts, true)) {
$finalList->push($account);
$accountId = (int)$accountId;
if (0 === $accountId) {
$this->friendlyWarning('You provided an account with ID 0 (zero). It will be ignored.');
continue;
}
$account = $accountRepository->find($accountId);
if (null === $account) {
$this->friendlyWarning(sprintf('There is no account with ID #%d, it cannot be added.', $accountId));
continue;
}
$type = $account->accountType->type;
if (!in_array($account->accountType->type, $this->acceptedAccounts, true)) {
$this->friendlyWarning(sprintf('Account "%s" with ID #%d is of type "%s" and cannot be added.', $account->name, $accountId, $type));
continue;
}
$finalList->push($account);
}
if (0 === $finalList->count()) {
$this->friendlyError('Please make sure all accounts in --accounts are asset accounts or liabilities.');
$this->friendlyError('There are no accounts in the selection. Please make sure all accounts in --accounts are asset accounts or liabilities.');
return false;
}
@@ -225,13 +239,27 @@ class ApplyRules extends Command
$ruleGroupList = explode(',', $ruleGroupString);
foreach ($ruleGroupList as $ruleGroupId) {
$ruleGroup = $this->ruleGroupRepository->find((int) $ruleGroupId);
if (true === $ruleGroup->active) {
$this->ruleGroupSelection[] = $ruleGroup->id;
$ruleGroupId = (int)$ruleGroupId;
if (0 === $ruleGroupId) {
$this->friendlyWarning('You added a rule group with ID 0 (zero). It will be skipped.');
continue;
}
$ruleGroup = $this->ruleGroupRepository->find($ruleGroupId);
if (null === $ruleGroup) {
$this->friendlyWarning(sprintf('There is no rule group with ID #%d, this ID will be ignored.', $ruleGroupId));
continue;
}
if (false === $ruleGroup->active) {
$this->friendlyWarning(sprintf('Will ignore inactive rule group #%d ("%s")', $ruleGroup->id, $ruleGroup->title));
$this->friendlyWarning(sprintf('Rule group with ID #%d is not active, so this ID will be ignored.', $ruleGroupId));
continue;
}
$this->ruleGroupSelection[] = $ruleGroupId;
}
return true;
@@ -247,10 +275,24 @@ class ApplyRules extends Command
$ruleList = explode(',', $ruleString);
foreach ($ruleList as $ruleId) {
$rule = $this->ruleRepository->find((int) $ruleId);
if ($rule instanceof Rule && true === $rule->active) {
$this->ruleSelection[] = $rule->id;
$ruleId = (int)$ruleId;
if (0 === $ruleId) {
$this->friendlyWarning('You added a rule with ID 0 (zero). It will be skipped.');
continue;
}
$rule = $this->ruleRepository->find($ruleId);
if (null === $rule) {
$this->friendlyWarning(sprintf('There is no rule with ID #%d, this ID will be ignored.', $ruleId));
continue;
}
if (false === $rule->active) {
$this->friendlyWarning(sprintf('Rule with ID #%d is not active, so this ID will be ignored.', $ruleId));
continue;
}
$this->ruleSelection[] = $ruleId;
}
return true;
@@ -304,10 +346,12 @@ class ApplyRules extends Command
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
@@ -318,16 +362,20 @@ class ApplyRules extends Command
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array($group->id, $this->ruleGroupSelection, true)
|| in_array($rule->id, $this->ruleSelection, true)
return in_array((int)$group->id, $this->ruleGroupSelection, true)
|| in_array((int)$rule->id, $this->ruleSelection, true)
|| $this->allRules;
}
}

View File

@@ -45,6 +45,7 @@ use Override;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -71,6 +72,7 @@ class Handler extends ExceptionHandler
AuthenticationException::class,
LaravelValidationException::class,
NotFoundHttpException::class,
GoneHttpException::class,
OAuthServerException::class,
LaravelOAuthException::class,
TokenMismatchException::class,

View File

@@ -784,14 +784,23 @@ trait MetaCollection
$filter = static function (array $object) use ($list): bool {
Log::debug(sprintf('Now in setTags(%s) filter', implode(', ', $list)));
foreach ($object['transactions'] as $transaction) {
$total = count($transaction['tags']);
$matched = 0;
foreach ($transaction['tags'] as $tag) {
Log::debug(sprintf('"%s" versus', strtolower((string) $tag['name'])), $list);
if (in_array(strtolower((string) $tag['name']), $list, true)) {
Log::debug(sprintf('Transaction has tag "%s" so return true.', $tag['name']));
return true;
++$matched;
if (1 === count($list)) {
return true;
}
}
}
if (count($list) > 1 && $total === $matched && $matched === count($list)) {
Log::debug(sprintf('All %d searched tags are present.', $total));
return true;
}
}
Log::debug('Transaction has no tags from the list, so return false.');

View File

@@ -74,6 +74,7 @@ class IndexController extends Controller
{
$this->cleanupObjectGroups();
$this->repository->correctOrder();
$this->repository->correctTransfers();
$start = session('start');
$end = session('end');
$collection = $this->repository->getBills();

View File

@@ -122,6 +122,7 @@ class ShowController extends Controller
*/
public function show(Request $request, Bill $bill): Factory|\Illuminate\Contracts\View\View
{
$this->repository->correctTransfers();
// add info about rules:
$rules = $this->repository->getRulesForBill($bill);
$subTitle = $bill->name;

View File

@@ -198,12 +198,14 @@ class PreferencesController extends Controller
*/
public function postIndex(PreferencesRequest $request): Redirector|RedirectResponse
{
Log::debug('postIndex for preferences.');
// front page accounts
$frontpageAccounts = [];
if (is_array($request->get('frontpageAccounts')) && count($request->get('frontpageAccounts')) > 0) {
foreach ($request->get('frontpageAccounts') as $id) {
$frontpageAccounts[] = (int)$id;
}
Log::debug('Update frontpageAccounts', $frontpageAccounts);
Preferences::set('frontpageAccounts', $frontpageAccounts);
}
@@ -212,14 +214,17 @@ class PreferencesController extends Controller
foreach (config('notifications.notifications.user') as $key => $info) {
$key = sprintf('notification_%s', $key);
if (array_key_exists($key, $all)) {
Log::debug(sprintf('update notification to true: %s', $key));
Preferences::set($key, true);
}
if (!array_key_exists($key, $all)) {
Log::debug(sprintf('update notification to false: %s', $key));
Preferences::set($key, false);
}
}
// view range:
Log::debug(sprintf('Let viewRange to "%s"', $request->get('viewRange')));
Preferences::set('viewRange', $request->get('viewRange'));
// forget session values:
session()->forget('start');
@@ -319,6 +324,7 @@ class PreferencesController extends Controller
// save and continue
session()->flash('success', (string)trans('firefly.saved_preferences'));
Preferences::mark();
Log::debug('Done saving settings.');
return redirect(route('preferences.index'));
}

View File

@@ -58,6 +58,7 @@ class AcceptHeaders
// some routes are exempt from this.
$exempt = [
'api.v1.data.bulk.transactions',
'api.v1.attachments.upload',
];
if (('POST' === $method || 'PUT' === $method) && !$request->hasHeader('Content-Type') && !in_array($request->route()->getName(), $exempt, true)) {

View File

@@ -23,8 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Bill;
use FireflyIII\Support\Facades\Navigation;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\BillFactory;
use FireflyIII\Models\Attachment;
@@ -34,12 +34,14 @@ use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\Rule;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups;
use FireflyIII\Services\Internal\Destroy\BillDestroyService;
use FireflyIII\Services\Internal\Update\BillUpdateService;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Query\JoinClause;
@@ -48,6 +50,7 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Override;
/**
* Class BillRepository.
@@ -244,7 +247,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var null|Note $note */
$note = $bill->notes()->first();
return (string) $note?->text;
return (string)$note?->text;
}
public function getOverallAverage(Bill $bill): array
@@ -261,7 +264,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
foreach ($journals as $journal) {
/** @var Transaction $transaction */
$transaction = $journal->transactions()->where('amount', '<', 0)->first();
$currencyId = (int) $journal->transaction_currency_id;
$currencyId = (int)$journal->transaction_currency_id;
$currency = $journal->transactionCurrency;
$result[$currencyId] ??= [
'sum' => '0',
@@ -274,10 +277,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string) $transaction->amount);
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string)$transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], $transaction->native_amount ?? '0');
if ($journal->foreign_currency_id === Amount::getPrimaryCurrency()->id) {
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string) $transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string)$transaction->amount);
}
++$result[$currencyId]['count'];
}
@@ -288,8 +291,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
* @var array $arr
*/
foreach ($result as $currencyId => $arr) {
$result[$currencyId]['avg'] = bcdiv((string) $arr['sum'], (string) $arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string) $arr['pc_sum'], (string) $arr['count']);
$result[$currencyId]['avg'] = bcdiv((string)$arr['sum'], (string)$arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string)$arr['pc_sum'], (string)$arr['count']);
}
return $result;
@@ -398,7 +401,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
if (null === $transaction) {
continue;
}
$currencyId = (int) $journal->transaction_currency_id;
$currencyId = (int)$journal->transaction_currency_id;
$currency = $journal->transactionCurrency;
$result[$currencyId] ??= [
'sum' => '0',
@@ -410,10 +413,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string) $transaction->amount);
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string)$transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], $transaction->native_amount ?? '0');
if ($journal->foreign_currency_id === Amount::getPrimaryCurrency()->id) {
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string) $transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string)$transaction->amount);
}
++$result[$currencyId]['count'];
}
@@ -424,8 +427,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
* @var array $arr
*/
foreach ($result as $currencyId => $arr) {
$result[$currencyId]['avg'] = bcdiv((string) $arr['sum'], (string) $arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string) $arr['pc_sum'], (string) $arr['count']);
$result[$currencyId]['avg'] = bcdiv((string)$arr['sum'], (string)$arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string)$arr['pc_sum'], (string)$arr['count']);
}
return $result;
@@ -438,7 +441,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
{
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$journal = $bill->user->transactionJournals()->find((int) $transaction['transaction_journal_id']);
$journal = $bill->user->transactionJournals()->find((int)$transaction['transaction_journal_id']);
$journal->bill_id = $bill->id;
$journal->save();
Log::debug(sprintf('Linked journal #%d to bill #%d', $journal->id, $bill->id));
@@ -544,8 +547,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var Collection $set */
$set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']);
$currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency;
$return[(int) $currency->id] ??= [
'id' => (string) $currency->id,
$return[(int)$currency->id] ??= [
'id' => (string)$currency->id,
'name' => $currency->name,
'symbol' => $currency->symbol,
'code' => $currency->code,
@@ -557,9 +560,9 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var TransactionJournal $transactionJournal */
foreach ($set as $transactionJournal) {
// grab currency from transaction.
$transactionCurrency = $transactionJournal->transactionCurrency;
$return[(int) $transactionCurrency->id] ??= [
'id' => (string) $transactionCurrency->id,
$transactionCurrency = $transactionJournal->transactionCurrency;
$return[(int)$transactionCurrency->id] ??= [
'id' => (string)$transactionCurrency->id,
'name' => $transactionCurrency->name,
'symbol' => $transactionCurrency->symbol,
'code' => $transactionCurrency->code,
@@ -568,7 +571,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
];
// get currency from transaction as well.
$return[(int) $transactionCurrency->id]['sum'] = bcadd($return[(int) $transactionCurrency->id]['sum'], Amount::getAmountFromJournalObject($transactionJournal));
$return[(int)$transactionCurrency->id]['sum'] = bcadd($return[(int)$transactionCurrency->id]['sum'], Amount::getAmountFromJournalObject($transactionJournal));
// $setAmount = bcadd($setAmount, Amount::getAmountFromJournalObject($transactionJournal));
}
// Log::debug(sprintf('Bill #%d ("%s") with %d transaction(s) and sum %s %s', $bill->id, $bill->name, $set->count(), $currency->code, $setAmount));
@@ -622,14 +625,14 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
$average = bcdiv(bcadd($bill->{$maxField} ?? '0', $bill->{$minField} ?? '0'), '2');
Log::debug(sprintf('Amount to pay is %s %s (%d times)', $currency->code, $average, $total));
$return[$currency->id] ??= [
'id' => (string) $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'], bcmul($average, (string) $total));
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], bcmul($average, (string)$total));
}
}
@@ -704,4 +707,20 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
return $service->update($bill, $data);
}
#[Override]
public function correctTransfers(): void
{
/** @var null|TransactionType $withdrawal */
$withdrawal = TransactionType::where('type', TransactionTypeEnum::WITHDRAWAL->value)->first();
if (null === $withdrawal) {
return;
}
$this->user
->transactionJournals()
->whereNotNull('bill_id')
->where('transaction_type_id', '!=', $withdrawal->id)
->update(['bill_id' => null])
;
}
}

View File

@@ -54,6 +54,8 @@ interface BillRepositoryInterface
*/
public function correctOrder(): void;
public function correctTransfers(): void;
public function destroy(Bill $bill): bool;
public function destroyAll(): void;

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers;
use FireflyIII\Models\Tag;
use Illuminate\Support\Facades\Log;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Exceptions\FireflyException;
@@ -416,8 +417,21 @@ trait RenderPartialViews
$repository = app(TagRepositoryInterface::class);
$tags = $repository->get();
$grouped = [];
/** @var Tag $tag */
foreach ($tags as $tag) {
$year = (int) $tag->date?->year;
$grouped[$year] ??= [
'tags' => [],
'year' => 0 === $year ? trans('firefly.no_date') : $year,
];
$grouped[$year]['tags'][] = $tag;
}
ksort($grouped);
try {
$result = view('reports.options.tag', ['tags' => $tags])->render();
$result = view('reports.options.tag', ['tags' => $grouped])->render();
} catch (Throwable $e) {
Log::error(sprintf('Cannot render reports.options.tag: %s', $e->getMessage()));
$result = 'Could not render view.';

View File

@@ -202,7 +202,6 @@ class Navigation
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
if ('MTD' === $repeatFreq && $end->isFuture()) {
// fall back to a monthly schedule if the requested period is MTD.
@@ -325,6 +324,7 @@ class Navigation
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));

View File

@@ -54,7 +54,11 @@ class LinkToBill implements ActionInterface
$billName = $this->action->getValue($journal);
$bill = $repository->findByName($billName);
if (null !== $bill && TransactionTypeEnum::WITHDRAWAL->value === $journal['transaction_type_type']) {
/** @var TransactionJournal $object */
$object = TransactionJournal::with('transactionType')->find($journal['transaction_journal_id']);
$type = $object->transactionType->type;
if (null !== $bill && TransactionTypeEnum::WITHDRAWAL->value === $type) {
$count = DB::table('transaction_journals')->where('id', '=', $journal['transaction_journal_id'])->where('bill_id', $bill->id)->count();
if (0 !== $count) {
Log::error(sprintf('RuleAction LinkToBill could not set the bill of journal #%d to bill "%s": already set.', $journal['transaction_journal_id'], $billName));

View File

@@ -24,6 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\TransactionRules\Expressions;
use FireflyIII\Exceptions\FireflyException;
use Illuminate\Support\Facades\Log;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
@@ -141,6 +143,11 @@ class ActionExpression
private function evaluateExpression(string $expr, array $journal): string
{
$result = $this->expressionLanguage->evaluate($expr, $journal);
if (is_array($result)) {
Log::error('Result of evaluating the expression is an array, please investigate', $result);
throw new FireflyException('Result of evaluating the expression is an array, please open a GitHub issue about this and include the error logs.');
}
return (string) $result;
}

View File

@@ -457,8 +457,9 @@ class FireflyValidator extends Validator
*
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function validateSecurePassword($attribute, string $value): bool
public function validateSecurePassword($attribute, ?string $value): bool
{
$value = (string)$value;
$verify = false;
if (array_key_exists('verify_password', $this->data)) {
$verify = 1 === (int) $this->data['verify_password'];

20
composer.lock generated
View File

@@ -1878,16 +1878,16 @@
},
{
"name": "laravel/framework",
"version": "v12.45.0",
"version": "v12.46.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "9dfd2afc48f2519bfdbe6862dfb9849491c673ad"
"reference": "9dcff48d25a632c1fadb713024c952fec489c4ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/9dfd2afc48f2519bfdbe6862dfb9849491c673ad",
"reference": "9dfd2afc48f2519bfdbe6862dfb9849491c673ad",
"url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae",
"reference": "9dcff48d25a632c1fadb713024c952fec489c4ae",
"shasum": ""
},
"require": {
@@ -2096,7 +2096,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-01-06T15:24:52+00:00"
"time": "2026-01-07T23:26:53+00:00"
},
{
"name": "laravel/passport",
@@ -2235,16 +2235,16 @@
},
{
"name": "laravel/sanctum",
"version": "v4.2.1",
"version": "v4.2.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
"shasum": ""
},
"require": {
@@ -2294,7 +2294,7 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-11-21T13:59:03+00:00"
"time": "2026-01-06T23:11:51+00:00"
},
{
"name": "laravel/serializable-closure",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => (bool)envNonEmpty('USE_RUNNING_BALANCE', true), // this is only the default value, is not used.
// see cer.php for exchange rates feature flag.
],
'version' => '6.4.15',
'build_time' => 1767729818,
'version' => 'develop/2026-01-10',
'build_time' => 1768064547,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

View File

@@ -87,6 +87,7 @@ class TransactionCurrencySeeder extends Seeder
$currencies[] = ['code' => 'CZK', 'name' => 'Czech koruna', 'symbol' => 'Kč', 'decimal_places' => 2];
$currencies[] = ['code' => 'KZT', 'name' => 'Kazakhstani tenge', 'symbol' => '₸', 'decimal_places' => 2];
$currencies[] = ['code' => 'SAR', 'name' => 'Saudi Riyal', 'symbol' => 'SAR', 'decimal_places' => 2];
$currencies[] = ['code' => 'RSD', 'name' => 'Serbian Dinar', 'symbol' => 'RSD', 'decimal_places' => 2];
foreach ($currencies as $currency) {
if (null === TransactionCurrency::where('code', $currency['code'])->first()) {

42
package-lock.json generated
View File

@@ -3092,9 +3092,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3105,9 +3105,9 @@
}
},
"node_modules/@types/express/node_modules/@types/express-serve-static-core": {
"version": "4.19.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
"integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
"version": "4.19.8",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
"integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3218,9 +3218,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"version": "25.0.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.5.tgz",
"integrity": "sha512-FuLxeLuSVOqHPxSN1fkcD8DLU21gAP7nCKqGRJ/FglbCUBs0NYN6TpHcdmyLeh8C0KwGIaZQJSv+OYG+KZz+Gw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4117,9 +4117,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"version": "2.9.14",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4550,9 +4550,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001762",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
"version": "1.0.30001763",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz",
"integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==",
"dev": true,
"funding": [
{
@@ -7093,9 +7093,9 @@
}
},
"node_modules/i18next": {
"version": "25.7.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
"integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
"version": "25.7.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
"integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
"funding": [
{
"type": "individual",
@@ -11462,9 +11462,9 @@
}
},
"node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -2480,6 +2480,7 @@ return [
'balanceFor' => 'Balance for :name',
'no_tags' => '(no tags)',
'nothing_found' => '(nothing found)',
'no_date' => '(no date)',
// page settings and wizard dialogs

View File

@@ -2,9 +2,13 @@
<label for="inputTags" class="col-sm-3 control-label">{{ 'select_tag'|_ }}</label>
<div class="col-sm-9">
<select id="inputTags" name="tag[]" multiple="multiple" class="form-control">
{% for tag in tags %}
<option value="{{ tag.id }}" label="{{ tag.tag|e('html') }}">{{ tag.tag|e('html') }}</option>
{% for year in tags %}
<optgroup label="{{ year.year }}">
{% for tag in year.tags %}
<option value="{{ tag.id }}" label="{{ tag.tag|e('html') }}">{{ tag.tag|e('html') }}</option>
{% endfor %}
{% endfor %}
</select>
</div>
</div>