mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-04-01 09:55:55 +00:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac6949f95 | ||
|
|
43acafb7a7 | ||
|
|
ca85a4c034 | ||
|
|
22752559e1 | ||
|
|
a341cae6bb | ||
|
|
64035a71ea | ||
|
|
074ca1756d | ||
|
|
0aa73ccf96 | ||
|
|
85c37d8812 | ||
|
|
16eb2ca4ca | ||
|
|
f491155f9b | ||
|
|
c5706e95b7 | ||
|
|
fdabb2c994 | ||
|
|
615fa733e6 | ||
|
|
d3fc8673d3 | ||
|
|
db156ffcf2 | ||
|
|
59510a9acc | ||
|
|
7f9640087e | ||
|
|
29c51ad0e2 | ||
|
|
bf8c40d502 | ||
|
|
99912483de | ||
|
|
a3f4ab9b1b | ||
|
|
953fe7d9eb | ||
|
|
41a7890c7a | ||
|
|
b8e07c1df4 | ||
|
|
2a3d9001d1 | ||
|
|
bc7453e204 | ||
|
|
8c9ad9da83 | ||
|
|
c8bd8d5113 | ||
|
|
8ce5429e06 | ||
|
|
c1f8fb2f45 | ||
|
|
5fb4330c20 | ||
|
|
485eb224d2 | ||
|
|
de7033ee63 | ||
|
|
4c20547863 | ||
|
|
3881cd3e39 | ||
|
|
a3e7fa008d | ||
|
|
205b5bd3bf | ||
|
|
21c3dc3f56 | ||
|
|
aa4f5d5a2e | ||
|
|
9c1f79110c | ||
|
|
bd62957908 | ||
|
|
a7e4252b46 | ||
|
|
c18691d6b3 | ||
|
|
b4dbcdcfba | ||
|
|
b0a6acb334 | ||
|
|
a0c423b9ed | ||
|
|
2844929351 | ||
|
|
7bfba6a239 | ||
|
|
c0d4a70d46 | ||
|
|
a62f8bbaff | ||
|
|
b115b4ad92 | ||
|
|
dc60025097 | ||
|
|
d77769b2f4 | ||
|
|
c6497960f8 | ||
|
|
4eee0c79cd | ||
|
|
e333c3254b | ||
|
|
9d244883a1 | ||
|
|
a17131c2f7 | ||
|
|
898459198d | ||
|
|
6466bc9272 | ||
|
|
dd8a8dba85 | ||
|
|
ee16888317 | ||
|
|
56a2580fd7 | ||
|
|
2ab0225223 | ||
|
|
83662415c3 | ||
|
|
cf976b2ab1 | ||
|
|
bf79c9db72 | ||
|
|
29f4c09a7b | ||
|
|
22ef456dca | ||
|
|
9c706465b2 | ||
|
|
a40425fd75 | ||
|
|
e9a37592ba | ||
|
|
0c598cb034 | ||
|
|
f5dea9ac09 | ||
|
|
4ef7944147 | ||
|
|
4e1c84944c | ||
|
|
f36da26cc3 | ||
|
|
5983a8eb6d | ||
|
|
b4a8a219ff | ||
|
|
974c84a877 |
84
.ci/php-cs-fixer/composer.lock
generated
84
.ci/php-cs-fixer/composer.lock
generated
@@ -1264,16 +1264,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.7",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1330,7 +1330,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.7"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1350,7 +1350,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T14:06:22+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -1421,16 +1421,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v8.0.4",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
|
||||
"reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
|
||||
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6",
|
||||
"reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1482,7 +1482,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1502,7 +1502,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T11:45:55+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@@ -1652,16 +1652,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v8.0.6",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
|
||||
"reference": "8da41214757b87d97f181e3d14a4179286151007"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
|
||||
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007",
|
||||
"reference": "8da41214757b87d97f181e3d14a4179286151007",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1696,7 +1696,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.6"
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1716,20 +1716,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-29T09:41:02+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v8.0.0",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
|
||||
"reference": "b48bce0a70b914f6953dafbd10474df232ed4de8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8",
|
||||
"reference": "b48bce0a70b914f6953dafbd10474df232ed4de8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1767,7 +1767,7 @@
|
||||
"options"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1787,7 +1787,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-12T15:55:31+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -2370,16 +2370,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.5",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2411,7 +2411,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.5"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2431,7 +2431,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -2522,16 +2522,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v8.0.0",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
"reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942"
|
||||
"reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942",
|
||||
"reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3",
|
||||
"reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2564,7 +2564,7 @@
|
||||
"description": "Provides a way to profile code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v8.0.0"
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2584,20 +2584,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-04T07:36:47+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v8.0.6",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2654,7 +2654,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.6"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2674,7 +2674,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-09T10:14:57+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@@ -6,6 +6,8 @@
|
||||
2. If your PR is more than 25 lines, talk to me FIRST.
|
||||
3. If you fix spelling or code comments, talk to me FIRST.
|
||||
|
||||
This is to prevent AI bots, low-effort PRs and spam. Sorry about that.
|
||||
|
||||
Wanna talk to me? Open a GitHub Issue, Discussion, or email me: james@firefly-iii.org
|
||||
|
||||
👀 Please ensure you have taken a look at the contribution guidelines:
|
||||
@@ -17,7 +19,9 @@ Remember that your PR may be CLOSED:
|
||||
2. If you open a PR on the main branch, your PR will be CLOSED.
|
||||
3. If you only fix a spelling error or code comment, your PR will be CLOSED.
|
||||
|
||||
Thanks again, and happy developing!
|
||||
Again, this is to prevent AI bots, low-effort PRs and spam. I apologize for the harsh tone.
|
||||
|
||||
But if you made it this far thanks again for contributing, and happy developing!
|
||||
|
||||
-->
|
||||
|
||||
@@ -48,3 +52,6 @@ I used AI assistance for:
|
||||
<!--
|
||||
Thanks for contributing!
|
||||
-->
|
||||
|
||||
@JC5
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
|
||||
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
|
||||
|
||||
## 2026
|
||||
- Joe Longendyke
|
||||
- Daniel Holøien
|
||||
- Matthew Grove
|
||||
- Cinnamon Pyro
|
||||
|
||||
@@ -116,6 +116,7 @@ final class PiggyBankController extends Controller
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id,
|
||||
'object_group_title' => $objectGroup?->title,
|
||||
'object_group_order' => $objectGroup?->order,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Note;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
@@ -85,6 +86,9 @@ final class PurgeController extends Controller
|
||||
// rules
|
||||
Rule::whereUserId($user->id)->onlyTrashed()->forceDelete();
|
||||
|
||||
// notes (this will actually purge EVERYBODY's deleted notes)
|
||||
Note::onlyTrashed()->forceDelete();
|
||||
|
||||
// recurring transactions
|
||||
Recurrence::whereUserId($user->id)->onlyTrashed()->forceDelete();
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\CurrencyTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
/**
|
||||
@@ -154,7 +153,6 @@ final class UpdateController extends Controller
|
||||
public function update(UpdateRequest $request, TransactionCurrency $currency): JsonResponse
|
||||
{
|
||||
$data = $request->getAll();
|
||||
Log::debug(__METHOD__, $data);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
@@ -28,6 +28,7 @@ use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Rules\IsValidPositiveAmount;
|
||||
use FireflyIII\Rules\IsValidZeroOrMoreAmount;
|
||||
use FireflyIII\Rules\LessThanPiggyTarget;
|
||||
use FireflyIII\Rules\PiggyBank\IsEnoughInAccounts;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -84,7 +85,7 @@ class UpdateRequest extends FormRequest
|
||||
'accounts' => 'array',
|
||||
'accounts.*' => 'array',
|
||||
'accounts.*.account_id' => ['required', 'numeric', 'belongsToUser:accounts,id'],
|
||||
'accounts.*.current_amount' => ['numeric', 'nullable', new IsValidZeroOrMoreAmount(true)],
|
||||
'accounts.*.current_amount' => ['numeric', 'nullable', new IsValidZeroOrMoreAmount(true), new IsEnoughInAccounts($piggyBank, $this->getAll())],
|
||||
'object_group_id' => 'numeric|belongsToUser:object_groups,id',
|
||||
'object_group_title' => ['min:1', 'max:255'],
|
||||
'transaction_currency_id' => 'exists:transaction_currencies,id|nullable',
|
||||
|
||||
@@ -28,6 +28,7 @@ use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Rules\IsBoolean;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
@@ -45,15 +46,23 @@ class UpdateRequest extends FormRequest
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
// return nothing that isn't explicitly in the array:
|
||||
$fields = [
|
||||
'name' => ['name', 'convertString'],
|
||||
'code' => ['code', 'convertString'],
|
||||
'symbol' => ['symbol', 'convertString'],
|
||||
'decimal_places' => ['decimal_places', 'convertInteger'],
|
||||
'default' => ['default', 'boolean'],
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$isAdmin = $user->hasRole('owner');
|
||||
|
||||
$fields = [
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
];
|
||||
if ($isAdmin) {
|
||||
$fields = [
|
||||
'name' => ['name', 'convertString'],
|
||||
'code' => ['code', 'convertString'],
|
||||
'symbol' => ['symbol', 'convertString'],
|
||||
'decimal_places' => ['decimal_places', 'convertInteger'],
|
||||
'default' => ['default', 'boolean'],
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->getAllData($fields);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionGroup;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CorrectsGroupAccounts extends Command
|
||||
{
|
||||
@@ -46,6 +47,7 @@ class CorrectsGroupAccounts extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
Log::debug('Start of correction:group-accounts');
|
||||
$groups = [];
|
||||
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
|
||||
|
||||
@@ -59,13 +61,16 @@ class CorrectsGroupAccounts extends Command
|
||||
$flags->applyRules = false;
|
||||
$flags->fireWebhooks = false;
|
||||
$flags->recalculateCredit = true;
|
||||
$flags->unifyOnly = true;
|
||||
$objects = new TransactionGroupEventObjects();
|
||||
foreach ($groups as $groupId) {
|
||||
$group = TransactionGroup::find($groupId);
|
||||
$objects->appendFromTransactionGroup($group);
|
||||
}
|
||||
Log::debug(sprintf('Fire event for %d transaction group(s)', count($groups)));
|
||||
event(new UpdatedSingleTransactionGroup($flags, $objects));
|
||||
event(new WebhookMessagesRequestSending());
|
||||
Log::debug('End of correction:group-accounts');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ class TransactionGroupEventFlags
|
||||
public bool $fireWebhooks = true;
|
||||
public bool $batchSubmission = false;
|
||||
public bool $recalculateCredit = true;
|
||||
public bool $unifyOnly = false;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ class Handler extends ExceptionHandler
|
||||
'method' => request()->method(),
|
||||
'headers' => $headers,
|
||||
// @mago-expect lint:no-request-all
|
||||
'post' => 'POST' === request()->method() ? json_encode(request()->all()) : '',
|
||||
'post' => 'PUT' === request()->method() || 'POST' === request()->method() ? json_encode(request()->all()) : '',
|
||||
];
|
||||
|
||||
// create job that will mail.
|
||||
|
||||
@@ -134,6 +134,13 @@ class PiggyBankFactory
|
||||
$previous = $toBeLinked[$account->id]['current_amount'] ?? '0';
|
||||
$diff = bcsub($info['current_amount'], $previous);
|
||||
|
||||
// if money is added, check if we can!
|
||||
if (1 === bccomp($diff, '0') && !$this->piggyBankRepository->canAddAmount($piggyBank, $account, $diff)) {
|
||||
Log::debug(sprintf('Cannot add amount %s to piggy bank #%d ("%s")', $diff, $piggyBank->id, $piggyBank->name));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// create event for difference.
|
||||
if (0 !== bccomp($diff, '0')) {
|
||||
// 2025-10-01 for issue #10990 disable this event.
|
||||
|
||||
@@ -75,8 +75,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$this->userGroup = null;
|
||||
$this->limit = null;
|
||||
$this->page = null;
|
||||
$this->startRow = null;
|
||||
$this->endRow = null;
|
||||
|
||||
$this->hasAccountInfo = false;
|
||||
$this->hasCatInformation = false;
|
||||
@@ -443,9 +441,15 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds);
|
||||
}
|
||||
$result = $this->query->get($this->fields);
|
||||
$this->total = $result->count();
|
||||
// if no post-filters are present, it can be sliced and returned.
|
||||
if (0 === count($this->sorting) && 0 === count($this->postFilters) && null !== $this->limit && null !== $this->page) {
|
||||
$offset = ($this->page - 1) * $this->limit;
|
||||
$result = $result->slice($offset, $this->limit);
|
||||
}
|
||||
|
||||
// $this->dumpQueryInLogs();
|
||||
// Log::debug(sprintf('Count of result is %d', $result->count()));
|
||||
// now to parse this into an array.
|
||||
// now to parse the rest into an array.
|
||||
$collection = $this->parseArray($result);
|
||||
|
||||
// filter the array using all available post filters:
|
||||
@@ -454,19 +458,12 @@ class GroupCollector implements GroupCollectorInterface
|
||||
// sort the collection, if sort instructions are present.
|
||||
$collection = $this->sortCollection($collection);
|
||||
|
||||
// count it and continue:
|
||||
$this->total = $collection->count();
|
||||
|
||||
// now filter the array according to the page and the limit (if necessary)
|
||||
if (null !== $this->limit && null !== $this->page) {
|
||||
if (count($this->postFilters) > 0 && null !== $this->limit && null !== $this->page) {
|
||||
$offset = ($this->page - 1) * $this->limit;
|
||||
|
||||
return $collection->slice($offset, $this->limit);
|
||||
}
|
||||
// OR filter the array according to the start and end row variable
|
||||
if (null !== $this->startRow && null !== $this->endRow) {
|
||||
return $collection->slice($this->startRow, $this->endRow);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
@@ -477,17 +474,11 @@ class GroupCollector implements GroupCollectorInterface
|
||||
public function getPaginatedGroups(): LengthAwarePaginator
|
||||
{
|
||||
Log::debug('Now in getPaginatedGroups()');
|
||||
$set = $this->getGroups();
|
||||
$limit = $this->limit ?? 1;
|
||||
if (0 === $this->limit) {
|
||||
$this->setLimit(50);
|
||||
}
|
||||
if (null !== $this->startRow && null !== $this->endRow) {
|
||||
/** @var int $total */
|
||||
$total = $this->endRow - $this->startRow;
|
||||
|
||||
return new LengthAwarePaginator($set, $this->total, $total, 1);
|
||||
}
|
||||
$limit = $this->limit ?? 1;
|
||||
$set = $this->getGroups();
|
||||
|
||||
return new LengthAwarePaginator($set, $this->total, $limit, $this->page);
|
||||
}
|
||||
@@ -519,13 +510,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEndRow(int $endRow): self
|
||||
{
|
||||
$this->endRow = $endRow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExpandGroupSearch(bool $expandGroupSearch): GroupCollectorInterface
|
||||
{
|
||||
$this->expandGroupSearch = $expandGroupSearch;
|
||||
@@ -636,13 +620,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStartRow(int $startRow): self
|
||||
{
|
||||
$this->startRow = $startRow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the search to one specific transaction group.
|
||||
*/
|
||||
@@ -692,6 +669,10 @@ class GroupCollector implements GroupCollectorInterface
|
||||
#[Override]
|
||||
public function sortCollection(Collection $collection): Collection
|
||||
{
|
||||
if (0 === count($this->sorting)) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string $field
|
||||
* @var string $direction
|
||||
|
||||
@@ -469,11 +469,6 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setEnd(Carbon $end): self;
|
||||
|
||||
/**
|
||||
* Set the page to get.
|
||||
*/
|
||||
public function setEndRow(int $endRow): self;
|
||||
|
||||
public function setExpandGroupSearch(bool $expandGroupSearch): self;
|
||||
|
||||
/**
|
||||
@@ -573,11 +568,6 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setStart(Carbon $start): self;
|
||||
|
||||
/**
|
||||
* Set the page to get.
|
||||
*/
|
||||
public function setStartRow(int $startRow): self;
|
||||
|
||||
/**
|
||||
* Limit results to a specific tag.
|
||||
*/
|
||||
|
||||
@@ -132,7 +132,7 @@ final class NotificationController extends Controller
|
||||
return redirect(route('settings.notification.index'));
|
||||
}
|
||||
|
||||
$all = $request->only(['channel']);
|
||||
$all = $request->only(['test_submit']);
|
||||
$channel = $all['test_submit'] ?? '';
|
||||
|
||||
switch ($channel) {
|
||||
|
||||
@@ -83,7 +83,7 @@ final class RegisterController extends Controller
|
||||
throw new FireflyException('Registration is currently not available :(');
|
||||
}
|
||||
|
||||
$this->validator($request->only(['email', 'password']))->validate();
|
||||
$this->validator($request->only(['email', 'password', 'password_confirmation']))->validate();
|
||||
$user = $this->createUser($request->only(['email', 'password']));
|
||||
Log::info(sprintf('Registered new user %s', $user->email));
|
||||
$owner = new OwnerNotifiable();
|
||||
|
||||
@@ -172,8 +172,8 @@ final class BudgetLimitController extends Controller
|
||||
// return empty array:
|
||||
return response()->json([]);
|
||||
}
|
||||
if ((int) $amount > 268_435_456) { // intentional cast to integer
|
||||
$amount = '268435456';
|
||||
if ((int) $amount > 2_147_483_647) { // intentional cast to integer
|
||||
$amount = '2147483647';
|
||||
}
|
||||
if (-1 === bccomp($amount, '0')) {
|
||||
$amount = bcmul($amount, '-1');
|
||||
@@ -232,8 +232,8 @@ final class BudgetLimitController extends Controller
|
||||
if ('' === $amount) {
|
||||
$amount = '0';
|
||||
}
|
||||
if ((int) $amount > 268_435_456) { // 268 million, intentional integer
|
||||
$amount = '268435456';
|
||||
if ((int) $amount > 2_147_483_647) { // 268 million, intentional integer
|
||||
$amount = '2147483647';
|
||||
}
|
||||
// sanity check on amount:
|
||||
if (0 === bccomp($amount, '0')) {
|
||||
|
||||
@@ -246,14 +246,14 @@ final class PreferencesController extends Controller
|
||||
$all = $request->only($keys);
|
||||
foreach (config('notifications.notifications.user') as $key => $info) {
|
||||
$key = sprintf('notification_%s', $key);
|
||||
if (array_key_exists($key, $all)) {
|
||||
if (array_key_exists($key, $all) && false === auth()->user()->hasRole('demo')) {
|
||||
Log::debug(sprintf('update notification to true: %s', $key));
|
||||
Preferences::set($key, true);
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($key, $all)) {
|
||||
Log::debug(sprintf('update notification to false: %s', $key));
|
||||
Preferences::set($key, false);
|
||||
}
|
||||
Log::debug(sprintf('update notification to false: %s', $key));
|
||||
Preferences::set($key, false);
|
||||
}
|
||||
unset($all);
|
||||
|
||||
@@ -369,6 +369,12 @@ final class PreferencesController extends Controller
|
||||
$all = $request->only(['channel']);
|
||||
$channel = $all['channel'] ?? '';
|
||||
|
||||
if (true === auth()->user()->hasRole('demo')) {
|
||||
session()->flash('error', (string) trans('firefly.not_available_demo_user'));
|
||||
|
||||
return redirect(route('preferences.index'));
|
||||
}
|
||||
|
||||
switch ($channel) {
|
||||
default:
|
||||
session()->flash('error', (string) trans('firefly.notification_test_failed', ['channel' => $channel]));
|
||||
|
||||
@@ -170,7 +170,7 @@ final class MassController extends Controller
|
||||
*/
|
||||
public function update(MassEditJournalRequest $request): RedirectResponse
|
||||
{
|
||||
$journalIds = $request->get('journals');
|
||||
$journalIds = $request->input('journals');
|
||||
if (!is_array($journalIds)) {
|
||||
// TODO this is a weird error, should be caught.
|
||||
throw new FireflyException('This is not an array.');
|
||||
@@ -250,6 +250,8 @@ final class MassController extends Controller
|
||||
private function updateJournal(int $journalId, MassEditJournalRequest $request): void
|
||||
{
|
||||
$journal = $this->repository->find($journalId);
|
||||
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($journal->transactionGroup);
|
||||
|
||||
if (!$journal instanceof TransactionJournal) {
|
||||
throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId));
|
||||
}
|
||||
@@ -274,8 +276,9 @@ final class MassController extends Controller
|
||||
// call service to update.
|
||||
$service->setData($data);
|
||||
$service->update();
|
||||
$updated = $service->getTransactionJournal();
|
||||
$objects->appendFromTransactionGroup($updated->transactionGroup);
|
||||
$flags = new TransactionGroupEventFlags();
|
||||
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($journal->transactionGroup);
|
||||
event(new UpdatedSingleTransactionGroup($flags, $objects));
|
||||
event(new WebhookMessagesRequestSending());
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use FireflyIII\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class IsAdmin.
|
||||
@@ -41,6 +42,8 @@ class IsAdminApi
|
||||
* @param null|string $guard
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, $guard = null)
|
||||
{
|
||||
@@ -58,6 +61,8 @@ class IsAdminApi
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
if (!$repository->hasRole($user, 'owner')) {
|
||||
Log::error(sprintf('Cannot access %s?%s.', $request->url(), $request->getQueryString()));
|
||||
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ class ProcessesUpdatedTransactionGroup
|
||||
public function handle(UpdatedSingleTransactionGroup $event): void
|
||||
{
|
||||
Log::debug(sprintf('Now handling event %s', get_class($event)));
|
||||
$this->unifyAccounts($event);
|
||||
|
||||
$effect = $this->unifyAccounts($event);
|
||||
Log::debug(sprintf('Effect of unifyAccounts = %d', $effect));
|
||||
Log::debug(sprintf('Transaction journal count is %d', $event->objects->transactionJournals->count()));
|
||||
if (!$event->flags->applyRules) {
|
||||
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $event->objects->transactionJournals->count()));
|
||||
@@ -63,7 +63,13 @@ class ProcessesUpdatedTransactionGroup
|
||||
$this->createWebhookMessages($event->objects->transactionGroups, WebhookTrigger::UPDATE_TRANSACTION);
|
||||
}
|
||||
$this->removePeriodStatistics($event->objects);
|
||||
$this->recalculateRunningBalance($event->objects);
|
||||
if (0 === $effect && true === $event->flags->unifyOnly) {
|
||||
Log::debug('Effect = 0, will not recalculate running balance.');
|
||||
}
|
||||
if (0 !== $effect || false === $event->flags->unifyOnly) {
|
||||
Log::debug(sprintf('Effect is != 0 (%d) OR unifyOnly = false, will recalc running balance', $effect));
|
||||
$this->recalculateRunningBalance($event->objects);
|
||||
}
|
||||
|
||||
Log::debug('Done with handle() for UpdatedSingleTransactionGroup');
|
||||
}
|
||||
@@ -71,23 +77,26 @@ class ProcessesUpdatedTransactionGroup
|
||||
/**
|
||||
* This method will make sure all source / destination accounts are the same.
|
||||
*/
|
||||
protected function unifyAccounts(UpdatedSingleTransactionGroup $updatedGroupEvent): void
|
||||
protected function unifyAccounts(UpdatedSingleTransactionGroup $updatedGroupEvent): int
|
||||
{
|
||||
Log::debug('Now in unifyAccounts()');
|
||||
$effect = 0;
|
||||
|
||||
/** @var TransactionGroup $group */
|
||||
foreach ($updatedGroupEvent->objects->transactionGroups as $group) {
|
||||
$this->unifyAccountsForGroup($group);
|
||||
$effect += $this->unifyAccountsForGroup($group);
|
||||
}
|
||||
Log::debug('Done with unifyAccounts()');
|
||||
Log::debug(sprintf('Done with unifyAccounts(%d)', $effect));
|
||||
|
||||
return $effect;
|
||||
}
|
||||
|
||||
private function unifyAccountsForGroup(TransactionGroup $group): void
|
||||
private function unifyAccountsForGroup(TransactionGroup $group): int
|
||||
{
|
||||
if (1 === $group->transactionJournals->count()) {
|
||||
Log::debug('Nothing to do in unifyAccounts()');
|
||||
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first journal:
|
||||
@@ -104,7 +113,7 @@ class ProcessesUpdatedTransactionGroup
|
||||
if (null === $first) {
|
||||
Log::warning(sprintf('Group #%d has no transaction journals.', $group->id));
|
||||
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$all = $group->transactionJournals()->get()->pluck('id')->toArray();
|
||||
@@ -116,13 +125,30 @@ class ProcessesUpdatedTransactionGroup
|
||||
$destAccount = $first->transactions()->where('amount', '>', '0')->first()->account;
|
||||
|
||||
$type = $first->transactionType->type;
|
||||
$effect = 0;
|
||||
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) {
|
||||
// set all source transactions to source account:
|
||||
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]);
|
||||
$effect += Transaction::whereIn('transaction_journal_id', $all)
|
||||
->where('account_id', '!=', $sourceAccount->id)
|
||||
->where('amount', '<', 0)
|
||||
->update(['account_id' => $sourceAccount->id])
|
||||
;
|
||||
}
|
||||
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) {
|
||||
// set all destination transactions to destination account:
|
||||
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]);
|
||||
$effect += Transaction::whereIn('transaction_journal_id', $all)
|
||||
->where('account_id', '!=', $destAccount->id)
|
||||
->where('amount', '>', 0)
|
||||
->update(['account_id' => $destAccount->id])
|
||||
;
|
||||
}
|
||||
if (0 === $effect) {
|
||||
Log::debug(sprintf('Had nothing to do in unifyAccounts(#%d)', $group->id));
|
||||
|
||||
return 0;
|
||||
}
|
||||
Log::debug(sprintf('Updated %d transaction(s) in unifyAccounts(#%d)', $effect, $group->id));
|
||||
|
||||
return $effect;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ trait SupportsGroupProcessingTrait
|
||||
return;
|
||||
}
|
||||
|
||||
$array = $set->pluck('id')->toArray();
|
||||
$array = array_unique($set->pluck('id')->toArray());
|
||||
|
||||
/** @var TransactionJournal $first */
|
||||
$first = $set->first();
|
||||
$journalIds = implode(',', $array);
|
||||
$user = $first->user;
|
||||
// Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
|
||||
Log::debug(sprintf('Fire rule engine for journal(s): %s', $journalIds));
|
||||
|
||||
// collect rules:
|
||||
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
|
||||
@@ -56,6 +56,7 @@ trait SupportsGroupProcessingTrait
|
||||
$newRuleEngine->setUser($user);
|
||||
$newRuleEngine->setRuleGroups($groups);
|
||||
foreach ($array as $journalId) {
|
||||
Log::debug(sprintf('Fire rule engine for journal #%d', $journalId));
|
||||
$newRuleEngine->removeOperator('journal_id');
|
||||
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalId]);
|
||||
$newRuleEngine->fire();
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace FireflyIII\Notifications;
|
||||
|
||||
use Exception;
|
||||
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\User;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Illuminate\Notifications\Notification;
|
||||
@@ -36,8 +37,16 @@ class NotificationSender
|
||||
{
|
||||
public static function send(OwnerNotifiable|User $user, Notification $notification): void
|
||||
{
|
||||
// ::locale($user->locale))
|
||||
$lang = config('firefly.default_language');
|
||||
Log::debug(sprintf('Notification send language defaults to "%s"', $lang));
|
||||
if ($user instanceof User) {
|
||||
$lang = Preferences::getForUser($user, 'language', $lang)->data;
|
||||
Log::debug(sprintf('Notification send language set to "%s"', $lang));
|
||||
}
|
||||
|
||||
try {
|
||||
NotificationFacade::send($user, $notification);
|
||||
NotificationFacade::locale($lang)->send($user, $notification);
|
||||
} catch (ClientException $e) {
|
||||
Log::error(sprintf('[a] Error sending notification: %s', $e->getMessage()));
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -48,9 +48,10 @@ class UserTestNotificationEmail extends Notification
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$address = (string) $notifiable->email;
|
||||
$link = route('index');
|
||||
|
||||
return new MailMessage()
|
||||
->markdown('emails.admin-test', ['email' => $address])
|
||||
->markdown('emails.admin-test', ['email' => $address, 'link' => $link])
|
||||
->subject((string) trans('email.admin_test_subject'))
|
||||
;
|
||||
}
|
||||
|
||||
73
app/Rules/PiggyBank/IsEnoughInAccounts.php
Normal file
73
app/Rules/PiggyBank/IsEnoughInAccounts.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* IsEnoughInAccounts.php
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Rules\PiggyBank;
|
||||
|
||||
use Closure;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Override;
|
||||
|
||||
class IsEnoughInAccounts implements ValidationRule
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PiggyBank $piggyBank,
|
||||
private readonly array $data
|
||||
) {}
|
||||
|
||||
#[Override]
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// TODO: Implement validate() method.
|
||||
if (!array_key_exists('accounts', $this->data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var AccountRepositoryInterface $repository */
|
||||
$repository = app(AccountRepositoryInterface::class);
|
||||
|
||||
/** @var PiggyBankRepositoryInterface $piggyRepos */
|
||||
$piggyRepos = app(PiggyBankRepositoryInterface::class);
|
||||
|
||||
$accounts = $this->data['accounts'];
|
||||
foreach ($accounts as $info) {
|
||||
$account = $repository->find((int) $info['account_id']);
|
||||
$amount = $info['current_amount'] ?? '0';
|
||||
if (null === $account) {
|
||||
$fail('validation.no_asset_account')->translate();
|
||||
|
||||
return;
|
||||
}
|
||||
if ('' === $amount || 0 === bccomp($amount, '0')) {
|
||||
continue;
|
||||
}
|
||||
$diff = bcsub($amount, $piggyRepos->getCurrentAmount($this->piggyBank, $account));
|
||||
if (1 === bccomp($diff, '0') && !$piggyRepos->canAddAmount($this->piggyBank, $account, $amount)) {
|
||||
$fail('validation.cannot_add_piggy_amount')->translate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()?->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
@@ -225,7 +225,7 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()?->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
|
||||
@@ -111,6 +111,11 @@ class JournalUpdateService
|
||||
$this->transactionGroupRepository = app(TransactionGroupRepositoryInterface::class);
|
||||
}
|
||||
|
||||
public function getTransactionJournal(): ?TransactionJournal
|
||||
{
|
||||
return $this->transactionJournal;
|
||||
}
|
||||
|
||||
public function isCompareHashChanged(): bool
|
||||
{
|
||||
Log::debug(sprintf('Now in %s', __METHOD__));
|
||||
@@ -774,8 +779,8 @@ class JournalUpdateService
|
||||
$this->transactionJournal,
|
||||
'update_foreign_amount',
|
||||
[
|
||||
'currency_symbol' => $oldForeignCurrency->symbol,
|
||||
'decimal_places' => $oldForeignCurrency->decimal_places,
|
||||
'currency_symbol' => $oldForeignCurrency?->symbol,
|
||||
'decimal_places' => $oldForeignCurrency?->decimal_places,
|
||||
'amount' => $originalSourceAmount,
|
||||
],
|
||||
[
|
||||
|
||||
@@ -70,7 +70,7 @@ class Date implements BinderInterface
|
||||
try {
|
||||
$result = new Carbon($value);
|
||||
} catch (InvalidDateException|InvalidFormatException $e) {
|
||||
$message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage());
|
||||
$message = sprintf('Could not parse date "%s" for user #%d: %s', $value, (int) auth()->user()?->id, $e->getMessage());
|
||||
Log::error($message);
|
||||
|
||||
throw new NotFoundHttpException('Could not parse value', $e);
|
||||
|
||||
@@ -32,6 +32,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\Steam;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -60,6 +61,7 @@ trait ChartGeneration
|
||||
}
|
||||
Log::debug('Regenerate chart.account.account-balance-chart from scratch.');
|
||||
$locale = Steam::getLocale();
|
||||
$converter = new ExchangeRateConverter();
|
||||
|
||||
/** @var GeneratorInterface $generator */
|
||||
$generator = app(GeneratorInterface::class);
|
||||
@@ -76,10 +78,6 @@ trait ChartGeneration
|
||||
foreach ($accounts as $account) {
|
||||
Log::debug(sprintf('Now at account #%d ("%s)', $account->id, $account->name));
|
||||
$currency = $accountRepos->getAccountCurrency($account) ?? $primary;
|
||||
$usePrimary = $convertToPrimary && $primary->id !== $currency->id;
|
||||
$field = $convertToPrimary ? 'pc_balance' : 'balance';
|
||||
$currency = $usePrimary ? $primary : $currency;
|
||||
Log::debug(sprintf('Will use field %s', $field));
|
||||
$currentSet = ['label' => $account->name, 'currency_symbol' => $currency->symbol, 'entries' => []];
|
||||
|
||||
$currentStart = clone $start;
|
||||
@@ -90,9 +88,16 @@ trait ChartGeneration
|
||||
$format = $currentStart->format('Y-m-d');
|
||||
$label = trim($currentStart->isoFormat((string) trans('config.month_and_day_js', [], $locale)));
|
||||
$balance = $range[$format] ?? $previous;
|
||||
$converted = $balance['balance'] ?? '0';
|
||||
|
||||
// convert balance if necessary:
|
||||
if ($convertToPrimary) {
|
||||
$converted = $converter->convert($currency, $primary, $currentStart, $balance['balance']);
|
||||
}
|
||||
|
||||
$previous = $balance;
|
||||
$currentStart->addDay();
|
||||
$currentSet['entries'][$label] = $balance[$field] ?? '0';
|
||||
$currentSet['entries'][$label] = $converted;
|
||||
}
|
||||
$chartData[] = $currentSet;
|
||||
}
|
||||
|
||||
@@ -474,7 +474,10 @@ trait ConvertsDataTypes
|
||||
if (!array_key_exists('current_amount', $entry)) {
|
||||
$amount = null;
|
||||
}
|
||||
$return[] = ['account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')), 'current_amount' => $amount];
|
||||
$return[] = [
|
||||
'account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')),
|
||||
'current_amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
|
||||
@@ -72,7 +72,7 @@ trait ValidatesAutoBudgetRequest
|
||||
$validator->errors()->add('auto_budget_amount', (string) trans('validation.require_currency_info'));
|
||||
}
|
||||
// too big amount
|
||||
if ((int) $amount > 268_435_456) {
|
||||
if ((int) $amount > 2_147_483_647) {
|
||||
$validator->errors()->add('auto_budget_amount', (string) trans('validation.amount_required_for_auto_budget'));
|
||||
}
|
||||
}
|
||||
|
||||
21
changelog.md
21
changelog.md
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v6.5.9 - 2026-03-23
|
||||
|
||||
<!-- summary: Bug fixes mainly, but also updated dependencies and new wording in the instructions you see when you open a PR. -->
|
||||
|
||||
### Fixed
|
||||
- [Issue 12004](https://github.com/firefly-iii/firefly-iii/issues/12004) (Test notification buttons always generate an error) reported by @IDevJoe
|
||||
- [Issue 12014](https://github.com/firefly-iii/firefly-iii/issues/12014) (Converting a transaction to a transfer and setting the destination account to one with a different currency breaks the audit log) reported by @avee87
|
||||
|
||||
# Changed
|
||||
- [Issue 12000](https://github.com/firefly-iii/firefly-iii/issues/12000) (Improved transaction pagination for large data sets) reported by @christiaanderidder
|
||||
|
||||
## v6.5.8 - 2026-03-22
|
||||
|
||||
<!-- summary: This release fixes a regression bug in user registration. -->
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 11995](https://github.com/firefly-iii/firefly-iii/issues/11995) (User registration breaks on password validation) reported by @mikaelhm
|
||||
|
||||
## v6.5.7 - 2026-03-21
|
||||
|
||||
<!-- summary: There is a new security policy for AI-generated security advisories and of course, some interesting but annoying bugs fixed. -->
|
||||
@@ -2672,7 +2691,7 @@ problems:
|
||||
|
||||
## x.x.x - 20xx-xx-xx
|
||||
|
||||
<!-- summary: If you can read this I forgot to update the summary! -->
|
||||
<!-- summary: This release fixes ... If you can read this I forgot to update the summary! -->
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"jc5/google2fa-laravel": "^2.0",
|
||||
"jc5/recovery": "^2",
|
||||
"laravel-notification-channels/pushover": "^4.0",
|
||||
"laravel-notification-channels/pushover": "^5.0",
|
||||
"laravel/framework": "^12",
|
||||
"laravel/passport": "^12.0",
|
||||
"laravel/slack-notification-channel": "^3.3",
|
||||
|
||||
499
composer.lock
generated
499
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -78,8 +78,8 @@ return [
|
||||
'running_balance_column' => (bool)envDefaultWhenEmpty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
|
||||
// see cer.php for exchange rates feature flag.
|
||||
],
|
||||
'version' => '6.5.7',
|
||||
'build_time' => 1774075144,
|
||||
'version' => 'develop/2026-04-01',
|
||||
'build_time' => 1775021114,
|
||||
'api_version' => '2.1.0', // field is no longer used.
|
||||
'db_version' => 28, // field is no longer used.
|
||||
|
||||
|
||||
1521
package-lock.json
generated
1521
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,9 @@ export default {
|
||||
let currentPiggy = res.data[key];
|
||||
if (null !== currentPiggy.object_group_id) {
|
||||
let groupOrder = currentPiggy.object_group_order;
|
||||
if(0 === groupOrder) {
|
||||
groupOrder = 1;
|
||||
}
|
||||
if (!tempList[groupOrder]) {
|
||||
tempList[groupOrder] = {
|
||||
group: {
|
||||
|
||||
@@ -189,6 +189,6 @@
|
||||
},
|
||||
"config": {
|
||||
"html_language": "ru",
|
||||
"date_time_fns": "d MMMM yyyy @ HH:mm:ss"
|
||||
"date_time_fns": "d MMMM yyyy, HH:mm:ss"
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1",
|
||||
"laravel-vite-plugin": "^2",
|
||||
"laravel-vite-plugin": "^3",
|
||||
"patch-package": "^8",
|
||||
"sass": "^1",
|
||||
"vite": "^7",
|
||||
"vite": "^8",
|
||||
"vite-plugin-manifest-sri": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -70,6 +70,7 @@ return [
|
||||
'rule_action_value' => 'This value is invalid for the selected action.',
|
||||
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
|
||||
'file_attached' => 'Successfully uploaded file ":name".',
|
||||
'cannot_add_piggy_amount' => 'This amount cannot be added to the piggy bank.',
|
||||
'file_zero' => 'The file is zero bytes in size.',
|
||||
'must_exist' => 'The ID in field :attribute does not exist in the database.',
|
||||
'all_accounts_equal' => 'All accounts in this field must be equal.',
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{% if objectType != 'liabilities' %}
|
||||
<th class="hidden-sm hidden-xs hidden-md">{{ trans('list.lastActivity') }}</th>
|
||||
{% endif %}
|
||||
<th class="fifteen"
|
||||
class="hidden-sm hidden-xs hidden-md">{{ trans('list.balanceDiff') }}</th>
|
||||
<th
|
||||
class="fifteen hidden-sm hidden-xs hidden-md">{{ trans('list.balanceDiff') }}</th>
|
||||
<th class="hidden-sm hidden-xs"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -282,7 +282,8 @@
|
||||
{% if transaction.source_account_id == account.id %}
|
||||
<span title="Deposit, source">{{ formatAmountBySymbol(transaction.source_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}</span>
|
||||
{% else %}
|
||||
<span title="Deposit, dest">{{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}</span>
|
||||
{# changed from normal currency_symbol to foreign_currency_symbol for #12043 #}
|
||||
<span title="Deposit, dest">{{ formatAmountBySymbol(transaction.destination_balance_after, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% elseif transaction.transaction_type_type == 'Withdrawal' %}
|
||||
|
||||
@@ -347,7 +347,6 @@ Route::group(
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\UserGroup',
|
||||
'prefix' => 'v1/user-groups',
|
||||
'as' => 'api.v1.user-groups.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
|
||||
@@ -636,6 +635,7 @@ Route::group(
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
|
||||
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
|
||||
Route::get('primary', ['uses' => 'ShowController@showPrimary', 'as' => 'show.primary']);
|
||||
Route::get('default', ['uses' => 'ShowController@showPrimary', 'as' => 'show.default']);
|
||||
Route::get('{currency_code}', ['uses' => 'ShowController@show', 'as' => 'show']);
|
||||
@@ -666,7 +666,6 @@ Route::group(
|
||||
static function (): void {
|
||||
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
|
||||
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
|
||||
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user