mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-04-02 10:25:11 +00:00
Compare commits
152 Commits
develop-20
...
develop-20
| 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 | ||
|
|
4190c4d243 | ||
|
|
70cbbc1523 | ||
|
|
c724f13501 | ||
|
|
5f01a83b43 | ||
|
|
53c13d221d | ||
|
|
266cd7d8d0 | ||
|
|
7c09278c8e | ||
|
|
21af34c65a | ||
|
|
594c04b121 | ||
|
|
c50408249b | ||
|
|
b05a38c0e2 | ||
|
|
0bb1afdf6c | ||
|
|
547b83b36e | ||
|
|
134d8c8cf6 | ||
|
|
94144a407d | ||
|
|
15e29d133a | ||
|
|
889ba9f3e6 | ||
|
|
32fe62df03 | ||
|
|
21f9be6504 | ||
|
|
d514792f4d | ||
|
|
5894695ad6 | ||
|
|
7004c9aaf5 | ||
|
|
1893a33d84 | ||
|
|
d345b31cd4 | ||
|
|
a27642024d | ||
|
|
c23ad831d0 | ||
|
|
d50c283973 | ||
|
|
9ea3519585 | ||
|
|
ddb5bc6038 | ||
|
|
b3d048eb67 | ||
|
|
caadef7c64 | ||
|
|
f7c01e6821 | ||
|
|
3a971d738c | ||
|
|
1eb4ae3a2c | ||
|
|
0f30eb59a4 | ||
|
|
9c10b01e8b | ||
|
|
7c4f80a360 | ||
|
|
e5c19f6088 | ||
|
|
b067215ba8 | ||
|
|
a17d10b064 | ||
|
|
859fea532d | ||
|
|
75261a46d9 | ||
|
|
a4a99310ea | ||
|
|
8de0844e55 | ||
|
|
e7a6dd792f | ||
|
|
395ccf8f75 | ||
|
|
dfbfdb6aa2 | ||
|
|
a367ee96bd | ||
|
|
b6b1261df5 | ||
|
|
52b611d7b3 | ||
|
|
bbc96f457b | ||
|
|
974c84a877 | ||
|
|
490c421ae5 | ||
|
|
48e5adbbfd | ||
|
|
b123f7e6f1 | ||
|
|
bff351bad2 | ||
|
|
2f0a3238c3 | ||
|
|
dc88781607 | ||
|
|
d6c2698eae | ||
|
|
aee804940b | ||
|
|
b53a756e5a | ||
|
|
ff5d83eba5 | ||
|
|
4c10c4a26f | ||
|
|
f75817b44d | ||
|
|
9ad5dfd45b | ||
|
|
9e3b8e6232 | ||
|
|
6c3b4a77b5 | ||
|
|
34b5d9fcf6 | ||
|
|
12bbc91dca | ||
|
|
a77ff6a51f | ||
|
|
62eb054c7f | ||
|
|
13536a2f65 |
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": [],
|
||||
|
||||
@@ -173,7 +173,7 @@ MAIL_ENCRYPTION=null
|
||||
MAIL_SENDMAIL_COMMAND=
|
||||
|
||||
#
|
||||
# If you use self-signed certificates for your STMP server, you can use the following settings.
|
||||
# If you use self-signed certificates for your SMTP server, you can use the following settings.
|
||||
#
|
||||
MAIL_ALLOW_SELF_SIGNED=false
|
||||
MAIL_VERIFY_PEER=true
|
||||
|
||||
62
.github/pull_request_template.md
vendored
62
.github/pull_request_template.md
vendored
@@ -1,25 +1,57 @@
|
||||
<!--
|
||||
|
||||
Please TALK TO ME FIRST before you open a PR.
|
||||
🙌 Thanks for contributing a pull request. Before you continue:
|
||||
|
||||
1. If you fix a problem that has no ticket, talk to me FIRST.
|
||||
2. If you introduce new financial solutions or concepts, talk to me FIRST.
|
||||
3. If your PR is more than 25 lines, talk to me FIRST.
|
||||
4. If you used AI to write your PR, talk to me FIRST.
|
||||
5. If you fix spelling or code comments, talk to me FIRST.
|
||||
1. If you introduce new financial solutions or concepts, talk to me FIRST.
|
||||
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.
|
||||
|
||||
Wanna talk to me? Open a GitHub Issue, Discussion, or send me an email: james@firefly-iii.org
|
||||
This is to prevent AI bots, low-effort PRs and spam. Sorry about that.
|
||||
|
||||
See also: https://docs.firefly-iii.org/explanation/support/#contributing-code
|
||||
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:
|
||||
https://docs.firefly-iii.org/explanation/support/#contributing-code
|
||||
|
||||
Remember that your PR may be CLOSED:
|
||||
|
||||
1. If you do not refer to an existing issue, your PR will 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.
|
||||
|
||||
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!
|
||||
|
||||
-->
|
||||
|
||||
@JC5
|
||||
|
||||
This PR fixes issue # <!-- mandatory field! -->.
|
||||
#### Reference issues and PRs
|
||||
<!--
|
||||
Example: Fixes #1234. See also #3456.
|
||||
-->
|
||||
|
||||
Changes in this pull request:
|
||||
#### What does this implement/fix? Explain your changes.
|
||||
|
||||
|
||||
|
||||
#### AI usage disclosure
|
||||
<!--
|
||||
If AI tools were involved in creating this PR, please check all boxes that apply
|
||||
below and make sure that you adhere to our Automated Contributions Policy:
|
||||
https://docs.firefly-iii.org/explanation/support/#automated-contributions-policy
|
||||
-->
|
||||
I used AI assistance for:
|
||||
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
|
||||
- [ ] Test/benchmark generation
|
||||
- [ ] Documentation (including examples)
|
||||
- [ ] Research and understanding
|
||||
|
||||
|
||||
#### Any other comments?
|
||||
|
||||
<!--
|
||||
Thanks for contributing!
|
||||
-->
|
||||
|
||||
@JC5
|
||||
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
10
.github/security.md
vendored
10
.github/security.md
vendored
@@ -99,15 +99,15 @@ compatibility.
|
||||
## Security scanning through automated means
|
||||
|
||||
There is some additional guidance for security vulnerabilities or suspected security vulnerabilities that have been
|
||||
found with the full or partial support of AI coding agents, large language models and other code-scanning tools. Many of
|
||||
such reports the developer of Firefly III receives are not applicable. This takes time away from responding to
|
||||
actual security vulnerabilities or suspected security vulnerabilities. If you use automated means to find these in
|
||||
the Firefly III code base, please take care to:
|
||||
found with the full or partial support of AI coding agents, large language models and other code-scanning tools. These reports are often not applicable, not actually a vulnerability, or just plain wrong. This takes time away from responding to
|
||||
*actual* security vulnerabilities or suspected security vulnerabilities. If you use automated means to search for security vulnerabilities in the Firefly III code base, please take care to:
|
||||
|
||||
1. Manually validate the results before you submit a report,
|
||||
1. manually validate the results before you submit a report,
|
||||
2. explain how the vulnerability can actually be abused by a nefarious third party, and
|
||||
3. try to limit the verbosity of your report.
|
||||
|
||||
At the discretion of the maintainer of the developer, your report may be closed without resolve.
|
||||
|
||||
## Credits
|
||||
|
||||
This security policy is based on [Harbor](https://github.com/goharbor/harbor)'s security policy.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 2026
|
||||
- Joe Longendyke
|
||||
- Daniel Holøien
|
||||
- Matthew Grove
|
||||
- Cinnamon Pyro
|
||||
- R1DEN
|
||||
|
||||
@@ -96,7 +96,7 @@ final class AccountController extends Controller
|
||||
$nameWithBalance = $account->name;
|
||||
$currency = $this->repository->getAccountCurrency($account) ?? $this->primaryCurrency;
|
||||
$useCurrency = $currency;
|
||||
if (in_array($account->accountType->type, $this->balanceTypes, true)) {
|
||||
if (in_array($account->accountType->type, $this->balanceTypes, strict: true)) {
|
||||
// this one is correct.
|
||||
Log::debug(sprintf('accounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
|
||||
$balance = $allBalances[$account->id] ?? [];
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\DestroyRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
@@ -59,11 +60,12 @@ final class DestroyController extends Controller
|
||||
public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
|
||||
{
|
||||
$this->repository->deleteRates($from, $to);
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
|
||||
public function destroySingleByDate(Request $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
|
||||
{
|
||||
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
|
||||
if ($exchangeRate instanceof CurrencyExchangeRate) {
|
||||
@@ -72,14 +74,19 @@ final class DestroyController extends Controller
|
||||
if (!$exchangeRate instanceof CurrencyExchangeRate) {
|
||||
throw new FireflyException('Bla');
|
||||
}
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
|
||||
public function destroySingleById(Request $request, CurrencyExchangeRate $exchangeRate): JsonResponse
|
||||
{
|
||||
$from = $exchangeRate->fromCurrency;
|
||||
$to = $exchangeRate->toCurrency;
|
||||
$this->repository->deleteRate($exchangeRate);
|
||||
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequ
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
|
||||
@@ -73,10 +75,12 @@ final class StoreController extends Controller
|
||||
if ($object instanceof CurrencyExchangeRate) {
|
||||
// just update it, no matter.
|
||||
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
|
||||
event(new UpdatedCurrencyExchangeRate($rate));
|
||||
}
|
||||
if (!$object instanceof CurrencyExchangeRate) {
|
||||
// store new
|
||||
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
event(new CreatedCurrencyExchangeRate($rate));
|
||||
}
|
||||
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
@@ -97,10 +101,12 @@ final class StoreController extends Controller
|
||||
// update existing rate.
|
||||
$existing = $this->repository->updateExchangeRate($existing, $rate);
|
||||
$collection->push($existing);
|
||||
event(new UpdatedCurrencyExchangeRate($existing));
|
||||
|
||||
continue;
|
||||
}
|
||||
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
event(new CreatedCurrencyExchangeRate($new));
|
||||
$collection->push($new);
|
||||
}
|
||||
|
||||
@@ -124,11 +130,13 @@ final class StoreController extends Controller
|
||||
// update existing rate.
|
||||
$existing = $this->repository->updateExchangeRate($existing, $rate);
|
||||
$collection->push($existing);
|
||||
event(new UpdatedCurrencyExchangeRate($existing));
|
||||
|
||||
continue;
|
||||
}
|
||||
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
$collection->push($new);
|
||||
event(new CreatedCurrencyExchangeRate($new));
|
||||
}
|
||||
|
||||
$count = $collection->count();
|
||||
|
||||
@@ -28,6 +28,7 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
|
||||
@@ -66,7 +67,7 @@ final class UpdateController extends Controller
|
||||
$date = $request->getDate();
|
||||
$rate = $request->getRate();
|
||||
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
|
||||
|
||||
event(new UpdatedCurrencyExchangeRate($exchangeRate));
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
|
||||
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
|
||||
@@ -77,6 +78,7 @@ final class UpdateController extends Controller
|
||||
$date = $request->getDate();
|
||||
$rate = $request->getRate();
|
||||
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
|
||||
event(new UpdatedCurrencyExchangeRate($exchangeRate));
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
$transformer->setParameters($this->parameters);
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ final class ListController extends Controller
|
||||
// use new group collector:
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
if (0 === count($journalIds)) {
|
||||
$collector->findNothing();
|
||||
}
|
||||
$collector
|
||||
->setUser($admin)
|
||||
// filter on journal IDs.
|
||||
|
||||
@@ -28,9 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -41,7 +39,6 @@ use Illuminate\Validation\ValidationException;
|
||||
final class DestroyController extends Controller
|
||||
{
|
||||
private CurrencyRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* CurrencyRepository constructor.
|
||||
@@ -50,8 +47,7 @@ final class DestroyController extends Controller
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$this->repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->repository->setUser(auth()->user());
|
||||
|
||||
return $next($request);
|
||||
@@ -69,15 +65,8 @@ final class DestroyController extends Controller
|
||||
*/
|
||||
public function destroy(TransactionCurrency $currency): JsonResponse
|
||||
{
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['currency_code' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
// access denied:
|
||||
$messages = ['currency_code' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
if ($this->repository->currencyInUse($currency)) {
|
||||
$messages = ['currency_code' => '200006: Currency in use.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -32,7 +32,6 @@ use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class DestroyController
|
||||
@@ -72,11 +71,6 @@ final class DestroyController extends Controller
|
||||
if (false === $linkType->editable) {
|
||||
throw new FireflyException('200020: Link type cannot be changed.');
|
||||
}
|
||||
if (false === auth()->user()->hasRole('owner')) {
|
||||
Log::channel('audit')->warning('Non-owner user tries to delete a link type.');
|
||||
|
||||
response()->json([], 401);
|
||||
}
|
||||
|
||||
$this->repository->destroy($linkType);
|
||||
Preferences::mark();
|
||||
|
||||
@@ -84,6 +84,9 @@ final class ListController extends Controller
|
||||
// use new group collector:
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
if (0 === count($journalIds)) {
|
||||
$collector->findNothing();
|
||||
}
|
||||
$collector
|
||||
->setUser($admin)
|
||||
// filter on journal IDs.
|
||||
|
||||
@@ -27,12 +27,10 @@ namespace FireflyIII\Api\V1\Controllers\Models\TransactionLinkType;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\StoreRequest;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\LinkTypeTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
@@ -44,7 +42,6 @@ final class StoreController extends Controller
|
||||
use TransactionFilter;
|
||||
|
||||
private LinkTypeRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* LinkTypeController constructor.
|
||||
@@ -54,9 +51,8 @@ final class StoreController extends Controller
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->repository->setUser($user);
|
||||
|
||||
return $next($request);
|
||||
@@ -73,15 +69,6 @@ final class StoreController extends Controller
|
||||
*/
|
||||
public function store(StoreRequest $request): JsonResponse
|
||||
{
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['name' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
// access denied:
|
||||
$messages = ['name' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
$data = $request->getAll();
|
||||
// if currency ID is 0, find the currency by the code:
|
||||
$linkType = $this->repository->store($data);
|
||||
|
||||
@@ -29,12 +29,10 @@ use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\UpdateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\LinkType;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\LinkTypeTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
@@ -46,7 +44,6 @@ final class UpdateController extends Controller
|
||||
use TransactionFilter;
|
||||
|
||||
private LinkTypeRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* LinkTypeController constructor.
|
||||
@@ -56,9 +53,8 @@ final class UpdateController extends Controller
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->repository->setUser($user);
|
||||
|
||||
return $next($request);
|
||||
@@ -80,15 +76,6 @@ final class UpdateController extends Controller
|
||||
throw new FireflyException('200020: Link type cannot be changed.');
|
||||
}
|
||||
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['name' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
$messages = ['name' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
|
||||
$data = $request->getAll();
|
||||
$this->repository->update($linkType, $data);
|
||||
$manager = $this->getManager();
|
||||
|
||||
@@ -63,7 +63,7 @@ final class AccountController extends Controller
|
||||
$query = trim((string) $request->get('query'));
|
||||
$field = trim((string) $request->get('field'));
|
||||
$type = $request->get('type') ?? 'all';
|
||||
if ('' === $query || !in_array($field, $this->validFields, true)) {
|
||||
if ('' === $query || !in_array($field, $this->validFields, strict: true)) {
|
||||
return response(null, 422);
|
||||
}
|
||||
Log::debug(sprintf('Now in account search("%s", "%s")', $field, $query));
|
||||
|
||||
@@ -66,7 +66,7 @@ final class TransactionController extends Controller
|
||||
$internalRef = (string) $request->attributes->get('internal_reference');
|
||||
$notes = (string) $request->attributes->get('notes');
|
||||
$description = (string) $request->attributes->get('description');
|
||||
Log::debug(sprintf('Include deleted? %s', var_export($includeDeleted, true)));
|
||||
Log::debug(sprintf('Include deleted? %s', var_export(value: $includeDeleted, return: true)));
|
||||
if ('' !== $externalId) {
|
||||
$count += $this->repository->countByMeta('external_id', $externalId, $includeDeleted);
|
||||
Log::debug(sprintf('Search for transactions with external_identifier "%s", count is now %d', $externalId, $count));
|
||||
|
||||
@@ -323,7 +323,7 @@ final class BasicController extends Controller
|
||||
$today = today(config('app.timezone'));
|
||||
$available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end);
|
||||
$budgets = $this->budgetRepository->getActiveBudgets();
|
||||
$spent = $this->opsRepository->sumExpenses($start, $end, null, $budgets);
|
||||
$spent = $this->opsRepository->sumExpenses($start, $end, null, $budgets, null, true);
|
||||
$days = (int) $today->diffInDays($end, true) + 1;
|
||||
$currencies = [];
|
||||
|
||||
|
||||
@@ -30,12 +30,10 @@ use FireflyIII\Enums\WebhookDelivery;
|
||||
use FireflyIII\Enums\WebhookResponse;
|
||||
use FireflyIII\Enums\WebhookTrigger;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Binder\EitherConfigKey;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
@@ -43,21 +41,6 @@ use Illuminate\Validation\ValidationException;
|
||||
*/
|
||||
final class ConfigurationController extends Controller
|
||||
{
|
||||
private UserRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
* ConfigurationController constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->repository = app(UserRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is documented at:
|
||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getConfiguration
|
||||
@@ -142,11 +125,6 @@ final class ConfigurationController extends Controller
|
||||
*/
|
||||
public function update(UpdateRequest $request, string $name): JsonResponse
|
||||
{
|
||||
$rules = ['value' => 'required'];
|
||||
if (!$this->repository->hasRole(auth()->user(), 'owner')) {
|
||||
$messages = ['value' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
$data = $request->getAll();
|
||||
$shortName = str_replace('configuration.', '', $name);
|
||||
|
||||
|
||||
@@ -74,13 +74,9 @@ final class UserController extends Controller
|
||||
return response()->json([], 500);
|
||||
}
|
||||
|
||||
if ($this->repository->hasRole($admin, 'owner')) {
|
||||
$this->repository->destroy($user);
|
||||
$this->repository->destroy($user);
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
throw new FireflyException('200025: No access to function.');
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ class ApiRequest extends FormRequest
|
||||
|
||||
public function handleConfig(array $config): void
|
||||
{
|
||||
if (in_array('required', $config, true)) {
|
||||
if (in_array('required', $config, strict: true)) {
|
||||
$this->required = 'required';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use FireflyIII\Validation\FireflyValidator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -69,9 +69,9 @@ class ChartRequest extends FormRequest
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
public function withValidator(FireflyValidator $validator): void
|
||||
{
|
||||
$validator->after(static function (Validator $validator): void {
|
||||
$validator->after(static function (FireflyValidator $validator): void {
|
||||
// validate transaction query data.
|
||||
$data = $validator->getData();
|
||||
if (!array_key_exists('accounts', $data)) {
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace FireflyIII\Api\V1\Requests\Data\Bulk;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use FireflyIII\Validation\FireflyValidator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -61,9 +61,9 @@ class MoveTransactionsRequest extends FormRequest
|
||||
* Configure the validator instance with special rules for after the basic validation rules.
|
||||
* TODO this is duplicate.
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
public function withValidator(FireflyValidator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator): void {
|
||||
$validator->after(function (FireflyValidator $validator): void {
|
||||
// validate start before end only if both are there.
|
||||
$data = $validator->getData();
|
||||
if (array_key_exists('original_account', $data) && array_key_exists('destination_account', $data)) {
|
||||
@@ -75,7 +75,7 @@ class MoveTransactionsRequest extends FormRequest
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMove(Validator $validator): void
|
||||
private function validateMove(FireflyValidator $validator): void
|
||||
{
|
||||
$data = $validator->getData();
|
||||
$repository = app(AccountRepositoryInterface::class);
|
||||
|
||||
@@ -72,7 +72,7 @@ class GenericRequest extends FormRequest
|
||||
if (in_array(
|
||||
$type,
|
||||
[AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value],
|
||||
true
|
||||
strict: true
|
||||
)) {
|
||||
$return->push($account);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use FireflyIII\Rules\UniqueIban;
|
||||
use FireflyIII\Support\Request\AppendsLocationData;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use FireflyIII\Validation\FireflyValidator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -121,9 +121,9 @@ class UpdateRequest extends FormRequest
|
||||
/**
|
||||
* Configure the validator instance with special rules for after the basic validation rules.
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
public function withValidator(FireflyValidator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator): void {
|
||||
$validator->after(function (FireflyValidator $validator): void {
|
||||
// validate start before end only if both are there.
|
||||
$data = $validator->getData();
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class StoreRequest extends FormRequest
|
||||
$validator->errors()->add(sprintf('accounts.%d', $index), trans('validation.invalid_account_currency'));
|
||||
}
|
||||
$type = $account->accountType->type;
|
||||
if (!in_array($type, $types, true)) {
|
||||
if (!in_array($type, $types, strict: true)) {
|
||||
$validator->errors()->add(sprintf('accounts.%d', $index), trans('validation.invalid_account_type'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -51,7 +51,7 @@ class StoreRequest extends FormRequest
|
||||
{
|
||||
$fields = [
|
||||
'title' => ['title', 'convertString'],
|
||||
'description' => ['description', 'convertString'],
|
||||
'description' => ['description', 'stringWithNewlines'],
|
||||
'rule_group_id' => ['rule_group_id', 'convertInteger'],
|
||||
'order' => ['order', 'convertInteger'],
|
||||
'rule_group_title' => ['rule_group_title', 'convertString'],
|
||||
|
||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Api\V1\Requests\Models\TransactionCurrency;
|
||||
|
||||
use FireflyIII\Api\V1\Requests\ApiRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use FireflyIII\Validation\FireflyValidator;
|
||||
|
||||
class CurrencyCodeRequest extends ApiRequest
|
||||
{
|
||||
@@ -34,10 +34,10 @@ class CurrencyCodeRequest extends ApiRequest
|
||||
return ['code' => sprintf('exists:transaction_currencies,code|%s', $this->required)];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
public function withValidator(FireflyValidator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator): void {
|
||||
if (!$validator->valid()) {
|
||||
$validator->after(function (FireflyValidator $validator): void {
|
||||
if (0 === count($validator->valid())) {
|
||||
return;
|
||||
}
|
||||
$code = $this->convertString('code', '');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CreateRequest extends FormRequest
|
||||
$responses = $this->get('responses', []);
|
||||
$deliveries = $this->get('deliveries', []);
|
||||
|
||||
if (in_array(0, [count($triggers), count($responses), count($deliveries)], true)) {
|
||||
if (in_array(0, [count($triggers), count($responses), count($deliveries)], strict: true)) {
|
||||
throw new FireflyException('Unexpectedly got no responses, triggers or deliveries.');
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class UpdateRequest extends FormRequest
|
||||
$responses = $this->get('responses', []);
|
||||
$deliveries = $this->get('deliveries', []);
|
||||
|
||||
if (in_array(0, [count($triggers), count($responses), count($deliveries)], true)) {
|
||||
if (in_array(0, [count($triggers), count($responses), count($deliveries)], strict: true)) {
|
||||
throw new FireflyException('Unexpectedly got no responses, triggers or deliveries.');
|
||||
}
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ class UpdateRequest extends FormRequest
|
||||
public function getAll(): array
|
||||
{
|
||||
$name = $this->route()->parameter('dynamicConfigKey');
|
||||
if (in_array($name, $this->booleans, true)) {
|
||||
if (in_array($name, $this->booleans, strict: true)) {
|
||||
return ['value' => $this->boolean('value')];
|
||||
}
|
||||
if (in_array($name, $this->integers, true)) {
|
||||
if (in_array($name, $this->integers, strict: true)) {
|
||||
return ['value' => $this->convertInteger('value')];
|
||||
}
|
||||
|
||||
@@ -73,13 +73,13 @@ class UpdateRequest extends FormRequest
|
||||
{
|
||||
$name = $this->route()->parameter('configName');
|
||||
|
||||
if (in_array($name, $this->booleans, true)) {
|
||||
if (in_array($name, $this->booleans, strict: true)) {
|
||||
return ['value' => ['required', new IsBoolean()]];
|
||||
}
|
||||
if ('configuration.permission_update_check' === $name) {
|
||||
return ['value' => 'required|numeric|min:-1|max:1'];
|
||||
}
|
||||
if (in_array($name, $this->integers, true)) {
|
||||
if (in_array($name, $this->integers, strict: true)) {
|
||||
return ['value' => 'required|numeric|min:464272080'];
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class CorrectsAccountTypes extends Command
|
||||
|
||||
private function canCreateDestination(array $validDestinations): bool
|
||||
{
|
||||
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
|
||||
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, strict: true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +133,7 @@ class CorrectsAccountTypes extends Command
|
||||
*/
|
||||
private function canCreateSource(array $validSources): bool
|
||||
{
|
||||
return in_array(AccountTypeEnum::REVENUE->value, $validSources, true);
|
||||
return in_array(AccountTypeEnum::REVENUE->value, $validSources, strict: true);
|
||||
}
|
||||
|
||||
private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void
|
||||
@@ -308,7 +308,7 @@ class CorrectsAccountTypes extends Command
|
||||
|
||||
private function hasValidAccountType(array $validTypes, string $accountType): bool
|
||||
{
|
||||
return in_array($accountType, $validTypes, true);
|
||||
return in_array($accountType, $validTypes, strict: true);
|
||||
}
|
||||
|
||||
private function inspectJournal(TransactionJournal $journal): void
|
||||
@@ -342,7 +342,7 @@ class CorrectsAccountTypes extends Command
|
||||
return;
|
||||
}
|
||||
$expectedTypes = $this->expected[$type][$sourceAccountType];
|
||||
if (!in_array($destAccountType, $expectedTypes, true)) {
|
||||
if (!in_array($destAccountType, $expectedTypes, strict: true)) {
|
||||
Log::debug(sprintf('[b] Going to fix journal #%d', $journal->id));
|
||||
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ class UpgradesToGroups extends Command
|
||||
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
|
||||
{
|
||||
$set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
|
||||
$amount = ((float) $transaction->amount * -1) === (float) $subject->amount; // intentional float
|
||||
$amount = -(float) $transaction->amount === (float) $subject->amount; // intentional float
|
||||
$identifier = $transaction->identifier === $subject->identifier;
|
||||
Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
|
||||
Log::debug(sprintf('ID the same? %s', var_export($identifier, true)));
|
||||
Log::debug(sprintf('Amount the same? %s', var_export($amount, return: true)));
|
||||
Log::debug(sprintf('ID the same? %s', var_export($identifier, return: true)));
|
||||
|
||||
return $amount && $identifier;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* CreatedCurrencyExchangeRate.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\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CreatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('CreatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* DestroyedCurrencyExchangeRate.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\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DestroyedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public TransactionCurrency $from,
|
||||
public TransactionCurrency $to,
|
||||
public UserGroup $userGroup
|
||||
) {
|
||||
Log::debug(sprintf('DestroyedCurrencyExchangeRate(%s, %s) Event', $from->code, $to->code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* UpdatedCurrencyExchangeRate.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\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UpdatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('UpdatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,5 @@ class TransactionGroupEventFlags
|
||||
public bool $fireWebhooks = true;
|
||||
public bool $batchSubmission = false;
|
||||
public bool $recalculateCredit = true;
|
||||
public bool $unifyOnly = false;
|
||||
}
|
||||
|
||||
@@ -250,7 +250,8 @@ class Handler extends ExceptionHandler
|
||||
'json' => request()->acceptsJson(),
|
||||
'method' => request()->method(),
|
||||
'headers' => $headers,
|
||||
'post' => 'POST' === request()->method() ? json_encode(request()->all()) : '',
|
||||
// @mago-expect lint:no-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.
|
||||
|
||||
@@ -56,11 +56,12 @@ class YearReportGenerator implements ReportGeneratorInterface
|
||||
$reportType = 'default';
|
||||
|
||||
try {
|
||||
$result = view('reports.default.year', ['accountIds' => $accountIds, 'reportType' => $reportType])
|
||||
->with('start', $this->start)
|
||||
->with('end', $this->end)
|
||||
->render()
|
||||
;
|
||||
$result = view('reports.default.year', [
|
||||
'accountIds' => $accountIds,
|
||||
'reportType' => $reportType,
|
||||
'start' => $this->start,
|
||||
'end' => $this->end,
|
||||
])->render();
|
||||
} catch (Throwable $e) {
|
||||
Log::error(sprintf('Cannot render reports.account.report: %s', $e->getMessage()));
|
||||
Log::error($e->getTraceAsString());
|
||||
|
||||
@@ -63,13 +63,15 @@ class MonthReportGenerator implements ReportGeneratorInterface
|
||||
|
||||
// render!
|
||||
try {
|
||||
$result = view('reports.tag.month', ['accountIds' => $accountIds, 'reportType' => $reportType, 'tagIds' => $tagIds])
|
||||
->with('start', $this->start)
|
||||
->with('end', $this->end)
|
||||
->with('tags', $this->tags)
|
||||
->with('accounts', $this->accounts)
|
||||
->render()
|
||||
;
|
||||
$result = view('reports.tag.month', [
|
||||
'accountIds' => $accountIds,
|
||||
'reportType' => $reportType,
|
||||
'tagIds' => $tagIds,
|
||||
'start' => $this->start,
|
||||
'end' => $this->end,
|
||||
'tags' => $this->tags,
|
||||
'accounts' => $this->accounts,
|
||||
])->render();
|
||||
} catch (Throwable $e) {
|
||||
Log::error(sprintf('Cannot render reports.tag.month: %s', $e->getMessage()));
|
||||
Log::error($e->getTraceAsString());
|
||||
|
||||
@@ -955,9 +955,9 @@ trait MetaCollection
|
||||
$this->fields[] = 'tags.tag as tag_name';
|
||||
$this->fields[] = 'tags.date as tag_date';
|
||||
$this->fields[] = 'tags.description as tag_description';
|
||||
$this->fields[] = 'tags.latitude as tag_latitude';
|
||||
$this->fields[] = 'tags.longitude as tag_longitude';
|
||||
$this->fields[] = 'tags.zoomLevel as tag_zoom_level';
|
||||
// $this->fields[] = 'tags.latitude as tag_latitude';
|
||||
// $this->fields[] = 'tags.longitude as tag_longitude';
|
||||
// $this->fields[] = 'tags.zoomLevel as tag_zoom_level';
|
||||
|
||||
$this->joinTagTables();
|
||||
|
||||
|
||||
@@ -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->all();
|
||||
$all = $request->only(['test_submit']);
|
||||
$channel = $all['test_submit'] ?? '';
|
||||
|
||||
switch ($channel) {
|
||||
|
||||
@@ -83,8 +83,8 @@ final class RegisterController extends Controller
|
||||
throw new FireflyException('Registration is currently not available :(');
|
||||
}
|
||||
|
||||
$this->validator($request->all())->validate();
|
||||
$user = $this->createUser($request->all());
|
||||
$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();
|
||||
event(new NewUserRegistered($owner, $user));
|
||||
|
||||
@@ -132,7 +132,7 @@ final class ResetPasswordController extends Controller
|
||||
$allowRegistration = false;
|
||||
}
|
||||
|
||||
return view('auth.passwords.reset')->with([
|
||||
return view('auth.passwords.reset', [
|
||||
'token' => $token,
|
||||
'email' => $request->email,
|
||||
'allowRegistration' => $allowRegistration,
|
||||
|
||||
@@ -137,15 +137,15 @@ final class BudgetLimitController extends Controller
|
||||
*/
|
||||
public function store(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
Log::debug('Going to store new budget-limit.', $request->all());
|
||||
Log::debug('Going to store new budget-limit.');
|
||||
// first search for existing one and update it if necessary.
|
||||
$currency = $this->currencyRepos->find((int) $request->get('transaction_currency_id'));
|
||||
$budget = $this->repository->find((int) $request->get('budget_id'));
|
||||
$currency = $this->currencyRepos->find((int) $request->input('transaction_currency_id'));
|
||||
$budget = $this->repository->find((int) $request->input('budget_id'));
|
||||
if (!$currency instanceof TransactionCurrency || !$budget instanceof Budget) {
|
||||
throw new FireflyException('No valid currency or budget.');
|
||||
}
|
||||
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
|
||||
$start = Carbon::createFromFormat('Y-m-d', $request->input('start'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $request->input('end'));
|
||||
|
||||
if (!$start instanceof Carbon || !$end instanceof Carbon) {
|
||||
return response()->json();
|
||||
@@ -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')) {
|
||||
|
||||
@@ -99,6 +99,7 @@ abstract class Controller extends BaseController
|
||||
$logoutUrl = config('firefly.custom_logout_url');
|
||||
|
||||
// overrule v2 layout back to v1.
|
||||
|
||||
if ('true' === request()->get('force_default_layout') && 'v2' === config('view.layout')) {
|
||||
// config('view.layout','v1');
|
||||
Config::set('view.layout', 'v1');
|
||||
|
||||
@@ -44,6 +44,7 @@ use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -108,7 +109,7 @@ final class DebugController extends Controller
|
||||
Preferences::mark();
|
||||
$request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range', 'temp-mfa-secret', 'temp-mfa-codes']);
|
||||
|
||||
Artisan::call('cache:clear');
|
||||
Cache::clear();
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('route:clear');
|
||||
Artisan::call('view:clear');
|
||||
@@ -302,7 +303,7 @@ final class DebugController extends Controller
|
||||
}
|
||||
|
||||
return [
|
||||
'debug' => var_export(config('app.debug'), true),
|
||||
'debug' => var_export(config('app.debug'), return: true),
|
||||
'audit_log_channel' => implode(', ', config('logging.channels.audit.channels')),
|
||||
'default_language' => (string) config('firefly.default_language'),
|
||||
'default_locale' => (string) config('firefly.default_locale'),
|
||||
|
||||
@@ -106,7 +106,7 @@ final class HomeController extends Controller
|
||||
}
|
||||
|
||||
$request->session()->put('is_custom_range', $isCustomRange);
|
||||
Log::debug(sprintf('Set is_custom_range to %s', var_export($isCustomRange, true)));
|
||||
Log::debug(sprintf('Set is_custom_range to %s', var_export($isCustomRange, return: true)));
|
||||
$request->session()->put('start', $start);
|
||||
Log::debug(sprintf('Set start to %s', $start->format('Y-m-d H:i:s')));
|
||||
$request->session()->put('end', $end);
|
||||
|
||||
@@ -116,7 +116,7 @@ final class JavascriptController extends Controller
|
||||
'currencyCode' => $currency->code,
|
||||
'currencySymbol' => $currency->symbol,
|
||||
'accountingLocaleInfo' => $accounting,
|
||||
'anonymous' => var_export(Steam::anonymous(), true),
|
||||
'anonymous' => var_export(Steam::anonymous(), return: true),
|
||||
'language' => $lang,
|
||||
'dateRangeTitle' => $dateRange['title'],
|
||||
'locale' => $locale,
|
||||
|
||||
@@ -82,7 +82,7 @@ final class IntroController extends Controller
|
||||
|
||||
Log::debug('Elements is array', $elements);
|
||||
Log::debug('Keys is', array_keys($elements));
|
||||
Log::debug(sprintf('Keys has "outro": %s', var_export($hasStep, true)));
|
||||
Log::debug(sprintf('Keys has "outro": %s', var_export($hasStep, return: true)));
|
||||
|
||||
return $hasStep;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ final class AmountController extends Controller
|
||||
*/
|
||||
public function postAdd(Request $request, PiggyBank $piggyBank): RedirectResponse
|
||||
{
|
||||
$data = $request->all();
|
||||
$data = $request->only(['amount']);
|
||||
$amounts = $data['amount'] ?? [];
|
||||
$total = '0';
|
||||
Log::debug('Start with loop.');
|
||||
|
||||
@@ -231,8 +231,8 @@ final class PreferencesController extends Controller
|
||||
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) {
|
||||
if (is_array($request->input('frontpageAccounts')) && count($request->input('frontpageAccounts')) > 0) {
|
||||
foreach ($request->input('frontpageAccounts') as $id) {
|
||||
$frontpageAccounts[] = (int) $id;
|
||||
}
|
||||
Log::debug('Update frontpageAccounts', $frontpageAccounts);
|
||||
@@ -240,22 +240,26 @@ final class PreferencesController extends Controller
|
||||
}
|
||||
|
||||
// extract notifications:
|
||||
$all = $request->all();
|
||||
$keys = array_map(function (string $value): string {
|
||||
return sprintf('notification_%s', $value);
|
||||
}, array_keys(config('notifications.notifications.user')));
|
||||
$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);
|
||||
|
||||
// view range:
|
||||
Log::debug(sprintf('Let viewRange to "%s"', $request->get('viewRange')));
|
||||
Preferences::set('viewRange', $request->get('viewRange'));
|
||||
Log::debug(sprintf('Let viewRange to "%s"', $request->input('viewRange')));
|
||||
Preferences::set('viewRange', $request->input('viewRange'));
|
||||
// forget session values:
|
||||
session()->forget('start');
|
||||
session()->forget('end');
|
||||
@@ -264,6 +268,7 @@ final class PreferencesController extends Controller
|
||||
// notification settings, cannot be set by the demo user.
|
||||
if (!auth()->user()->hasRole('demo')) {
|
||||
$variables = ['slack_webhook_url', 'pushover_app_token', 'pushover_user_token', 'ntfy_server', 'ntfy_topic', 'ntfy_user', 'ntfy_pass'];
|
||||
$all = $request->only($variables);
|
||||
foreach ($variables as $variable) {
|
||||
if ('' === $all[$variable]) {
|
||||
Preferences::delete($variable);
|
||||
@@ -274,9 +279,10 @@ final class PreferencesController extends Controller
|
||||
}
|
||||
Preferences::set('ntfy_auth', $all['ntfy_auth'] ?? false);
|
||||
}
|
||||
unset($all);
|
||||
|
||||
// convert primary
|
||||
$convertToPrimary = 1 === (int) $request->get('convertToPrimary');
|
||||
$convertToPrimary = 1 === (int) $request->input('convertToPrimary');
|
||||
if ($convertToPrimary && !$this->convertToPrimary) {
|
||||
// set to true!
|
||||
Log::debug('User sets convertToPrimary to true.');
|
||||
@@ -288,9 +294,9 @@ final class PreferencesController extends Controller
|
||||
Preferences::set('convert_to_primary', $convertToPrimary);
|
||||
|
||||
// custom fiscal year
|
||||
$customFiscalYear = 1 === (int) $request->get('customFiscalYear');
|
||||
$customFiscalYear = 1 === (int) $request->input('customFiscalYear');
|
||||
Preferences::set('customFiscalYear', $customFiscalYear);
|
||||
$fiscalYearString = (string) $request->get('fiscalYearStart');
|
||||
$fiscalYearString = (string) $request->input('fiscalYearStart');
|
||||
if ('' !== $fiscalYearString) {
|
||||
$fiscalYearStart = Carbon::parse($fiscalYearString, config('app.timezone'))->format('m-d');
|
||||
Preferences::set('fiscalYearStart', $fiscalYearStart);
|
||||
@@ -298,7 +304,7 @@ final class PreferencesController extends Controller
|
||||
|
||||
// save page size:
|
||||
Preferences::set('listPageSize', 50);
|
||||
$listPageSize = (int) $request->get('listPageSize');
|
||||
$listPageSize = (int) $request->input('listPageSize');
|
||||
if ($listPageSize > 0 && $listPageSize < 1337) {
|
||||
Preferences::set('listPageSize', $listPageSize);
|
||||
}
|
||||
@@ -306,7 +312,7 @@ final class PreferencesController extends Controller
|
||||
// language:
|
||||
/** @var Preference $currentLang */
|
||||
$currentLang = Preferences::get('language', 'en_US');
|
||||
$lang = $request->get('language');
|
||||
$lang = $request->input('language');
|
||||
if (array_key_exists($lang, config('firefly.languages'))) {
|
||||
Preferences::set('language', $lang);
|
||||
}
|
||||
@@ -317,13 +323,13 @@ final class PreferencesController extends Controller
|
||||
|
||||
// same for locale:
|
||||
if (!auth()->user()->hasRole('demo')) {
|
||||
$locale = (string) $request->get('locale');
|
||||
$locale = (string) $request->input('locale');
|
||||
$locale = '' === $locale ? null : $locale;
|
||||
Preferences::set('locale', $locale);
|
||||
}
|
||||
|
||||
// optional fields for transactions:
|
||||
$setOptions = $request->get('tj') ?? [];
|
||||
$setOptions = $request->input('tj') ?? [];
|
||||
$optionalTj = [
|
||||
'interest_date' => array_key_exists('interest_date', $setOptions),
|
||||
'book_date' => array_key_exists('book_date', $setOptions),
|
||||
@@ -341,13 +347,13 @@ final class PreferencesController extends Controller
|
||||
Preferences::set('transaction_journal_optional_fields', $optionalTj);
|
||||
|
||||
// dark mode
|
||||
$darkMode = $request->get('darkMode') ?? 'browser';
|
||||
$darkMode = $request->input('darkMode') ?? 'browser';
|
||||
if (in_array($darkMode, config('firefly.available_dark_modes'), true)) {
|
||||
Preferences::set('darkMode', $darkMode);
|
||||
}
|
||||
|
||||
// anonymous amounts?
|
||||
$anonymous = '1' === $request->get('anonymous');
|
||||
$anonymous = '1' === $request->input('anonymous');
|
||||
Preferences::set('anonymous', $anonymous);
|
||||
|
||||
// save and continue
|
||||
@@ -360,9 +366,15 @@ final class PreferencesController extends Controller
|
||||
|
||||
public function testNotification(Request $request): mixed
|
||||
{
|
||||
$all = $request->all();
|
||||
$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]));
|
||||
|
||||
@@ -225,7 +225,7 @@ final class TagController extends Controller
|
||||
{
|
||||
// default values:
|
||||
$subTitleIcon = 'fa-tag';
|
||||
$page = (int) $request->get('page');
|
||||
$page = (int) $request->input('page');
|
||||
$pageSize = (int) Preferences::get('listPageSize', 50)->data;
|
||||
$start ??= session('start');
|
||||
$end ??= session('end');
|
||||
@@ -249,18 +249,20 @@ final class TagController extends Controller
|
||||
// collect transaction journal IDs in repository,
|
||||
// this makes the collector faster and more accurate.
|
||||
$journalIds = $this->repository->getJournalIds($tag);
|
||||
|
||||
if (0 === count($journalIds)) {
|
||||
$collector->findNothing();
|
||||
}
|
||||
$collector
|
||||
->setRange($start, $end)
|
||||
->setLimit($pageSize)
|
||||
->setPage($page)
|
||||
->setJournalIds($journalIds)
|
||||
->withAccountInformation()
|
||||
// ->setTag($tag)
|
||||
->withBudgetInformation()
|
||||
->withCategoryInformation()
|
||||
->withAttachmentInformation()
|
||||
;
|
||||
|
||||
$groups = $collector->getPaginatedGroups();
|
||||
$groups->setPath($path);
|
||||
$sums = $this->repository->sumsOfTag($tag, $start, $end);
|
||||
@@ -303,10 +305,13 @@ final class TagController extends Controller
|
||||
|
||||
// collect transaction journal IDs in repository,
|
||||
// this makes the collector faster and more accurate.
|
||||
$journalIds = $this->repository->getJournalIds($tag);
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$journalIds = $this->repository->getJournalIds($tag);
|
||||
if (0 === count($journalIds)) {
|
||||
$collector->findNothing();
|
||||
}
|
||||
|
||||
$collector
|
||||
->setRange($start, $end)
|
||||
->setLimit($pageSize)
|
||||
|
||||
@@ -153,7 +153,7 @@ final class ConvertController extends Controller
|
||||
foreach ($group->transactionJournals as $journal) {
|
||||
// catch FF exception.
|
||||
try {
|
||||
$this->convertJournal($journal, $destinationType, $request->all());
|
||||
$this->convertJournal($journal, $destinationType, $request->only(['source_id', 'source_name', 'destination_id', 'destination_name']));
|
||||
} catch (FireflyException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ final class EditController extends Controller
|
||||
];
|
||||
$optionalFields['external_url'] ??= false;
|
||||
$optionalFields['location'] ??= false;
|
||||
$optionalFields['location'] = $optionalFields['location']
|
||||
$optionalFields['location'] = true === $optionalFields['location']
|
||||
&& true === FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
|
||||
|
||||
// map info voor v2:
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -43,6 +43,6 @@ final class EditController extends Controller
|
||||
$mainTitleIcon = 'fa-book';
|
||||
Log::debug(sprintf('Now at %s', __METHOD__));
|
||||
|
||||
return view('administrations.edit')->with(['title' => $title, 'subTitle' => $subTitle, 'mainTitleIcon' => $mainTitleIcon]);
|
||||
return view('administrations.edit', ['title' => $title, 'subTitle' => $subTitle, 'mainTitleIcon' => $mainTitleIcon]);
|
||||
}
|
||||
}
|
||||
|
||||
71
app/Http/Middleware/IsAdminApi.php
Normal file
71
app/Http/Middleware/IsAdminApi.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* IsAdmin.php
|
||||
* Copyright (c) 2019 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\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class IsAdmin.
|
||||
*/
|
||||
class IsAdminApi
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request. Must be admin.
|
||||
*
|
||||
* @param null|string $guard
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, $guard = null)
|
||||
{
|
||||
if (Auth::guard($guard)->guest()) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
}
|
||||
|
||||
return response()->redirectTo(route('login'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
/** @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();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class SecureHeaders
|
||||
// sprintf("style-src 'self' 'nonce-%1s'", $nonce), // safe variant
|
||||
"style-src 'self' 'unsafe-inline'", // unsafe variant
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
// "form-action 'self'", // safe
|
||||
"font-src 'self' data:",
|
||||
sprintf("connect-src 'self' %s", $trackingScriptSrc),
|
||||
sprintf("img-src 'self' data: 'nonce-%1s' ", $nonce),
|
||||
@@ -70,16 +70,17 @@ class SecureHeaders
|
||||
|
||||
// overrule in development mode
|
||||
if (true === config('firefly.is_local_dev')) {
|
||||
$ip = '192.168.96.165';
|
||||
$csp = [
|
||||
"default-src 'none'",
|
||||
"object-src 'none'",
|
||||
sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s'", $nonce),
|
||||
// sprintf("style-src 'self' 'nonce-%1s' https://10.0.0.15:5173/", $nonce), // safe variant
|
||||
"style-src 'self' 'unsafe-inline' https://10.0.0.15:5173/", // unsafe variant
|
||||
sprintf("style-src 'self' 'unsafe-inline' https://%s:5173/", $ip), // unsafe variant
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"font-src 'self' data: https://10.0.0.15:5173/",
|
||||
sprintf("connect-src 'self' %s https://10.0.0.15:5173/ wss://10.0.0.15:5173/", $trackingScriptSrc),
|
||||
sprintf("font-src 'self' data: https://%s:5173/", $ip),
|
||||
sprintf('connect-src \'self\' %1$s https://%2$s:5173/ wss://%2$s:5173/', $trackingScriptSrc, $ip),
|
||||
sprintf("img-src 'self' data: 'nonce-%1s'", $nonce),
|
||||
"manifest-src 'self'",
|
||||
];
|
||||
|
||||
@@ -59,7 +59,7 @@ class UpdatesAccountInformation implements ShouldQueue
|
||||
/** @var RuleAction $action */
|
||||
foreach ($rule->ruleActions as $action) {
|
||||
// fix name:
|
||||
if ($oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
|
||||
if (array_key_exists('name', $oldData) && $oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
|
||||
Log::debug(sprintf('Rule action #%d "%s" has old account name, replace with new.', $action->id, $action->action_type));
|
||||
$action->action_value = $account->name;
|
||||
$action->save();
|
||||
@@ -105,21 +105,25 @@ class UpdatesAccountInformation implements ShouldQueue
|
||||
/** @var RuleTrigger $trigger */
|
||||
foreach ($rule->ruleTriggers as $trigger) {
|
||||
// fix name:
|
||||
if ($oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
|
||||
if (array_key_exists('name', $oldData) && $oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account name, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->name;
|
||||
$trigger->save();
|
||||
++$fixed;
|
||||
}
|
||||
// fix IBAN:
|
||||
if ($oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
if (array_key_exists('iban', $oldData) && $oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account IBAN, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->iban;
|
||||
$trigger->save();
|
||||
++$fixed;
|
||||
}
|
||||
// fix account number: // account_number
|
||||
if ($oldData['account_number'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
if (
|
||||
array_key_exists('account_number', $oldData)
|
||||
&& $oldData['account_number'] === $trigger->trigger_value
|
||||
&& in_array($trigger->trigger_type, $numberFields, true)
|
||||
) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account account_number, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->iban;
|
||||
$trigger->save();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* ProcessesExchangeRates.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\Listeners\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessesExchangeRates
|
||||
{
|
||||
public function handle(CreatedCurrencyExchangeRate|DestroyedCurrencyExchangeRate|UpdatedCurrencyExchangeRate $event): void
|
||||
{
|
||||
Preferences::mark();
|
||||
Cache::clear();
|
||||
if ($event instanceof DestroyedCurrencyExchangeRate) {
|
||||
$this->handleCurrency($event->userGroup, $event->from);
|
||||
$this->handleCurrency($event->userGroup, $event->to);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency);
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->toCurrency);
|
||||
}
|
||||
|
||||
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s.', $currency->code));
|
||||
|
||||
$calculator->recalculateForGroupAndCurrency($userGroup, $currency);
|
||||
// $calculator->recalculateForGroup($userGroup);
|
||||
|
||||
return;
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -25,46 +25,17 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Listeners\System;
|
||||
|
||||
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RecalculatesPrimaryCurrencyAmounts
|
||||
{
|
||||
public function handle(UserGroupChangedPrimaryCurrency $event): void
|
||||
{
|
||||
// Reset the primary currency amounts for all objects that have it.
|
||||
Log::debug('Resetting primary currency amounts for all objects.');
|
||||
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetPiggyBanks($event->userGroup);
|
||||
$this->resetBudgets($event->userGroup);
|
||||
$this->resetTransactions($event->userGroup);
|
||||
Log::debug('Have now reset all primary amounts to NULL.');
|
||||
// fire laravel command to recalculate them all.
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug('Will now convert amounts to primary currency.');
|
||||
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
$calculator->recalculate();
|
||||
|
||||
@@ -72,87 +43,4 @@ class RecalculatesPrimaryCurrencyAmounts
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string) $autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string) $limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
{
|
||||
if ('' !== (string) $piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string) $account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string) $event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null])
|
||||
;
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ class CurrencyExchangeRate extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -31,6 +31,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property User $user
|
||||
*/
|
||||
class GroupMembership extends Model
|
||||
{
|
||||
use ReturnsIntegerIdTrait;
|
||||
|
||||
@@ -32,6 +32,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use function Safe\json_decode;
|
||||
use function Safe\json_encode;
|
||||
|
||||
/**
|
||||
* @property TransactionJournal $transactionJournal
|
||||
*/
|
||||
class TransactionJournalMeta extends Model
|
||||
{
|
||||
use ReturnsIntegerIdTrait;
|
||||
|
||||
@@ -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'))
|
||||
;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Attachment;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
@@ -37,6 +39,7 @@ use Illuminate\Support\Collection;
|
||||
* @method getUser()
|
||||
* @method checkUserGroupAccess(UserRoleEnum $role)
|
||||
* @method setUserGroupById(int $userGroupId)
|
||||
* @method setUser(null|Authenticatable|User $user)
|
||||
*/
|
||||
interface AttachmentRepositoryInterface
|
||||
{
|
||||
|
||||
@@ -638,9 +638,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
|
||||
|
||||
/** @var Bill $bill */
|
||||
foreach ($bills as $bill) {
|
||||
// Log::debug(sprintf('Bill #%d ("%s")', $bill->id, $bill->name));
|
||||
/** @var Collection $set */
|
||||
$set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']);
|
||||
$currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency;
|
||||
$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,
|
||||
'name' => $currency->name,
|
||||
@@ -649,13 +650,14 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
|
||||
'decimal_places' => $currency->decimal_places,
|
||||
'sum' => '0',
|
||||
];
|
||||
$setAmount = '0';
|
||||
// Log::debug(sprintf('Created a new array for currency #%d', $currency->id));
|
||||
|
||||
/** @var TransactionJournal $transactionJournal */
|
||||
foreach ($set as $transactionJournal) {
|
||||
// grab currency from transaction.
|
||||
$transactionCurrency = $transactionJournal->transactionCurrency;
|
||||
$return[(int) $transactionCurrency->id] ??= [
|
||||
// grab currency from journal.
|
||||
$transactionCurrency = $transactionJournal->transactionCurrency;
|
||||
$currencyId = (int) $transactionCurrency->id;
|
||||
$return[$currencyId] ??= [
|
||||
'id' => (string) $transactionCurrency->id,
|
||||
'name' => $transactionCurrency->name,
|
||||
'symbol' => $transactionCurrency->symbol,
|
||||
@@ -663,19 +665,12 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
|
||||
'decimal_places' => $transactionCurrency->decimal_places,
|
||||
'sum' => '0',
|
||||
];
|
||||
|
||||
$amountFromJournal = Amount::getAmountFromJournalObject($transactionJournal);
|
||||
// Log::debug(sprintf('Created a (new) array for currency #%d', $currencyId));
|
||||
// Log::debug(sprintf('Amount to add is %s', $amountFromJournal));
|
||||
// get currency from transaction as well.
|
||||
$return[(int) $transactionCurrency->id]['sum'] = bcadd(
|
||||
$return[(int) $transactionCurrency->id]['sum'],
|
||||
Amount::getAmountFromJournalObject($transactionJournal)
|
||||
);
|
||||
|
||||
// $setAmount = bcadd($setAmount, Amount::getAmountFromJournalObject($transactionJournal));
|
||||
$return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $amountFromJournal);
|
||||
}
|
||||
|
||||
// Log::debug(sprintf('Bill #%d ("%s") with %d transaction(s) and sum %s %s', $bill->id, $bill->name, $set->count(), $currency->code, $setAmount));
|
||||
// $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $setAmount);
|
||||
// Log::debug(sprintf('Total sum is now %s', $return[$currency->id]['sum']));
|
||||
}
|
||||
// remove empty sets
|
||||
$final = [];
|
||||
|
||||
@@ -368,6 +368,15 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
|
||||
|
||||
public function getBudgets(): Collection
|
||||
{
|
||||
if (null === $this->user) {
|
||||
return $this->userGroup
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
return $this->user
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
|
||||
@@ -25,7 +25,10 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Repositories\PeriodStatistic;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\PeriodStatistic;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
|
||||
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -38,7 +41,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
|
||||
{
|
||||
use UserGroupTrait;
|
||||
|
||||
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection
|
||||
public function allInRangeForModel(Account|Category|Tag $model, Carbon $start, Carbon $end): Collection
|
||||
{
|
||||
return $model->primaryPeriodStatistics()->where('start', '>=', $start)->where('end', '<=', $end)->get();
|
||||
}
|
||||
|
||||
@@ -25,13 +25,16 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Repositories\PeriodStatistic;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\PeriodStatistic;
|
||||
use FireflyIII\Models\Tag;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface PeriodStatisticRepositoryInterface
|
||||
{
|
||||
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection;
|
||||
public function allInRangeForModel(Account|Category|Tag $model, Carbon $start, Carbon $end): Collection;
|
||||
|
||||
public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection;
|
||||
|
||||
@@ -41,6 +44,8 @@ interface PeriodStatisticRepositoryInterface
|
||||
|
||||
public function deleteStatisticsForPrefix(string $prefix, Collection $dates): void;
|
||||
|
||||
public function deleteStatisticsForType(string $class, Collection $objects, Collection $dates): void;
|
||||
|
||||
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection;
|
||||
|
||||
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Services\Internal\Recalculate;
|
||||
|
||||
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
|
||||
use FireflyIII\Handlers\Observer\TransactionObserver;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AutoBudget;
|
||||
@@ -36,14 +37,16 @@ use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\Builder as DatabaseBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -57,14 +60,52 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var UserGroupRepositoryInterface $repository */
|
||||
$repository = app(UserGroupRepositoryInterface::class);
|
||||
Preferences::mark();
|
||||
|
||||
/** @var UserGroup $userGroup */
|
||||
foreach ($repository->getAll() as $userGroup) {
|
||||
Log::debug('Resetting primary currency amounts for all objects.');
|
||||
$this->resetGenericTables($userGroup);
|
||||
$this->resetPiggyBanks($userGroup);
|
||||
$this->resetBudgets($userGroup);
|
||||
$this->resetTransactions($userGroup);
|
||||
Log::debug('Have now reset all primary amounts to NULL.');
|
||||
$this->recalculateForGroup($userGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculateAccounts($userGroup, $currency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
public function recalculateForGroupAndCurrency(UserGroup $userGroup, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
if ($limitCurrency->id === $currency->id) {
|
||||
Log::debug(sprintf('Can skip recalculation because user requested the same currencies (%s).', $limitCurrency->code));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recalculateAccountsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
}
|
||||
|
||||
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
@@ -86,7 +127,10 @@ class PrimaryAmountRecalculationService
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
@@ -96,9 +140,42 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
private function recalculateAccounts(UserGroup $userGroup): void
|
||||
private function calculateTransactionsForCurrency(UserGroup $userGroup, TransactionCurrency $currency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
$set = $userGroup
|
||||
Log::debug(sprintf('Now in calculateTransactionsForCurrency(#%d, %s, %s)', $userGroup->id, $currency->code, $limitCurrency->code));
|
||||
// custom query because of the potential size of this update.
|
||||
$set = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (DatabaseBuilder $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
|
||||
});
|
||||
})
|
||||
// must be in the limit currency.
|
||||
->where('transactions.transaction_currency_id', $limitCurrency->id)
|
||||
->orWhere('transactions.foreign_currency_id', $limitCurrency->id)
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
$transaction?->touch();
|
||||
}
|
||||
TransactionObserver::$recalculate = true;
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
private function collectAccounts(UserGroup $userGroup): Collection
|
||||
{
|
||||
return $userGroup
|
||||
->accounts()
|
||||
->where(static function (EloquentBuilder $q): void {
|
||||
$q->whereNotNull('virtual_balance');
|
||||
@@ -113,14 +190,59 @@ class PrimaryAmountRecalculationService
|
||||
})
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccounts(#%d, %s)', $userGroup->id, $groupCurrency->code));
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$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));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
$account->touch();
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccountsForCurrency(UserGroup $userGroup, TransactionCurrency $groupCurrency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccountsForCurrency(#%d, %s, %s)', $userGroup->id, $groupCurrency->code, $limitCurrency->code));
|
||||
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$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));
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($limitCurrency->id !== $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is NOT in limit currency %s, skip.', $account->name, $limitCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
// TODO it is bad form to call an event from an event but OK.
|
||||
event(new UpdatedExistingAccount($account, []));
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void
|
||||
{
|
||||
$set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
|
||||
@@ -180,21 +302,6 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
|
||||
}
|
||||
|
||||
private function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
$this->recalculateAccounts($userGroup);
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
|
||||
{
|
||||
$set = $piggyBank->piggyBankEvents()->get();
|
||||
@@ -204,6 +311,9 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d piggy bank events.', $set->count()));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method collects ALL piggy banks, but only processes those that do not have the userGroup's primary currency.
|
||||
*/
|
||||
private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
$converter = new ExchangeRateConverter();
|
||||
@@ -233,4 +343,104 @@ class PrimaryAmountRecalculationService
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string) $autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string) $limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetGenericTables(UserGroup $userGroup): void
|
||||
{
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
{
|
||||
if ('' !== (string) $piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string) $account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string) $event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null])
|
||||
;
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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__));
|
||||
@@ -695,6 +700,7 @@ class JournalUpdateService
|
||||
$source = $this->getSourceTransaction();
|
||||
$dest = $this->getDestinationTransaction();
|
||||
$foreignCurrency = $source->foreignCurrency;
|
||||
$oldForeignCurrency = $foreignCurrency;
|
||||
$originalSourceAmount = $source->foreign_amount;
|
||||
|
||||
// find currency in data array
|
||||
@@ -773,11 +779,15 @@ class JournalUpdateService
|
||||
$this->transactionJournal,
|
||||
'update_foreign_amount',
|
||||
[
|
||||
'currency_symbol' => $recordCurrency->symbol,
|
||||
'decimal_places' => $recordCurrency->decimal_places,
|
||||
'currency_symbol' => $oldForeignCurrency?->symbol,
|
||||
'decimal_places' => $oldForeignCurrency?->decimal_places,
|
||||
'amount' => $originalSourceAmount,
|
||||
],
|
||||
['currency_symbol' => $recordCurrency->symbol, 'decimal_places' => $recordCurrency->decimal_places, 'amount' => $value]
|
||||
[
|
||||
'currency_symbol' => $recordCurrency->symbol,
|
||||
'decimal_places' => $recordCurrency->decimal_places,
|
||||
'amount' => $value,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,21 +225,27 @@ class Amount
|
||||
*/
|
||||
public function getAmountFromJournalObject(TransactionJournal $journal): string
|
||||
{
|
||||
// Log::debug(sprintf('Get amount from journal #%d', $journal->id));
|
||||
$convertToPrimary = $this->convertToPrimary();
|
||||
$currency = $this->getPrimaryCurrency();
|
||||
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
|
||||
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'native_amount' : 'amount';
|
||||
|
||||
/** @var null|Transaction $sourceTransaction */
|
||||
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
|
||||
if (null === $sourceTransaction) {
|
||||
// Log::debug('Return zero!');
|
||||
return '0';
|
||||
}
|
||||
$amount = $sourceTransaction->{$field} ?? '0';
|
||||
// Log::debug(sprintf('Amount is %s', $amount));
|
||||
if ((int) $sourceTransaction->foreign_currency_id === $currency->id) {
|
||||
// use foreign amount instead!
|
||||
$amount = (string) $sourceTransaction->foreign_amount; // hard coded to be foreign amount.
|
||||
|
||||
// Log::debug(sprintf('Amount is now %s', $amount));
|
||||
}
|
||||
|
||||
// Log::debug(sprintf('Final return is %s', $amount));
|
||||
return $amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -171,7 +171,7 @@ class FrontpageChartGenerator
|
||||
$direction = $array['sum_float'] < 0 ? 'spent' : 'earned';
|
||||
$key = sprintf('%s-%d', $direction, $array['currency_id']);
|
||||
$category = $array['name'];
|
||||
$amount = $array['sum_float'] < 0 ? $array['sum_float'] * -1 : $array['sum_float'];
|
||||
$amount = $array['sum_float'] < 0 ? -$array['sum_float'] : $array['sum_float'];
|
||||
$currencyData[$key]['entries'][$category] = $amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Csv\AbstractCsv;
|
||||
use League\Csv\CannotInsertRecord;
|
||||
use League\Csv\Exception;
|
||||
use League\Csv\Writer;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
@@ -284,7 +284,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -353,7 +353,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -412,7 +412,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -457,7 +457,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -537,7 +537,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -704,7 +704,7 @@ class ExportDataGenerator
|
||||
}
|
||||
}
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -851,7 +851,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -883,7 +883,7 @@ class ExportDataGenerator
|
||||
*/
|
||||
private function exportTags(): string
|
||||
{
|
||||
$header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level'];
|
||||
$header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description']; // 'latitude', 'longitude', 'zoom_level'
|
||||
|
||||
$tagRepos = app(TagRepositoryInterface::class);
|
||||
$tagRepos->setUser($this->user);
|
||||
@@ -900,14 +900,14 @@ class ExportDataGenerator
|
||||
$tag->tag,
|
||||
$tag->date?->format('Y-m-d'),
|
||||
$tag->description,
|
||||
$tag->latitude,
|
||||
$tag->longitude,
|
||||
$tag->zoomLevel,
|
||||
// $tag->latitude,
|
||||
// $tag->longitude,
|
||||
// $tag->zoomLevel,
|
||||
];
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
@@ -1103,7 +1103,7 @@ class ExportDataGenerator
|
||||
}
|
||||
|
||||
// load the CSV document from a string
|
||||
$csv = AbstractCsv::fromString();
|
||||
$csv = Writer::fromString();
|
||||
|
||||
// insert the header
|
||||
try {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ trait UserNavigation
|
||||
|
||||
return redirect(route('index'));
|
||||
}
|
||||
|
||||
/** @var TransactionJournal $journal */
|
||||
$journal = $transaction->transactionJournal;
|
||||
|
||||
/** @var null|Transaction $other */
|
||||
|
||||
@@ -27,8 +27,10 @@ namespace FireflyIII\Support\Request;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidDateException;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Steam;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -36,6 +38,8 @@ use function Safe\preg_replace;
|
||||
|
||||
/**
|
||||
* Trait ConvertsDataTypes
|
||||
*
|
||||
* @method UserGroup validateUserGroup(Request $request)
|
||||
*/
|
||||
trait ConvertsDataTypes
|
||||
{
|
||||
@@ -231,7 +235,12 @@ trait ConvertsDataTypes
|
||||
*/
|
||||
public function stringWithNewlines(string $field): string
|
||||
{
|
||||
return (string) $this->clearStringKeepNewlines((string) ($this->get($field) ?? ''));
|
||||
$entry = $this->get($field);
|
||||
if (!is_scalar($entry)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) $this->clearStringKeepNewlines((string) $entry);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,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;
|
||||
|
||||
@@ -81,7 +81,7 @@ class AccountTransformer extends AbstractTransformer
|
||||
}
|
||||
|
||||
// no order for some accounts:
|
||||
if (!in_array(strtolower($accountType), ['liability', 'liabilities', 'asset'], true)) {
|
||||
if (!in_array(strtolower($accountType), ['liability', 'liabilities', 'asset'], strict: true)) {
|
||||
$order = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,9 +106,6 @@ class User extends Authenticatable
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to accounts.
|
||||
*/
|
||||
public function accounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Account::class);
|
||||
@@ -527,7 +524,7 @@ class User extends Authenticatable
|
||||
$userGroup->id,
|
||||
$userGroup->title
|
||||
));
|
||||
if (in_array($membership->userRole->title, $dbRolesTitles, true)) {
|
||||
if (in_array($membership->userRole->title, $dbRolesTitles, strict: true)) {
|
||||
Log::debug(sprintf('Return true, found role "%s"', $membership->userRole->title));
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use FireflyIII\Http\Middleware\EncryptCookies;
|
||||
use FireflyIII\Http\Middleware\Installer;
|
||||
use FireflyIII\Http\Middleware\InterestingMessage;
|
||||
use FireflyIII\Http\Middleware\IsAdmin;
|
||||
use FireflyIII\Http\Middleware\IsAdminApi;
|
||||
use FireflyIII\Http\Middleware\Range;
|
||||
use FireflyIII\Http\Middleware\RedirectIfAuthenticated;
|
||||
use FireflyIII\Http\Middleware\SecureHeaders;
|
||||
@@ -157,7 +158,7 @@ $app = Application::configure(basePath: dirname(__DIR__))
|
||||
// This middleware is added to ensure that the user is not only logged in and
|
||||
// authenticated (with MFA and everything), but also admin.
|
||||
$middleware->appendToGroup('api-admin', [
|
||||
IsAdmin::class,
|
||||
IsAdminApi::class,
|
||||
]);
|
||||
$middleware->appendToGroup('admin', [
|
||||
IsAdmin::class,
|
||||
|
||||
87
changelog.md
87
changelog.md
@@ -3,7 +3,45 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 6.5.5 - 2026-03-15
|
||||
## 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. -->
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 11964](https://github.com/firefly-iii/firefly-iii/issues/11964) ("Left to spend" is not taking into account non-main currency withdrawals (when displaying in primary currency)) reported by @absdjfh
|
||||
- [Issue 11966](https://github.com/firefly-iii/firefly-iii/issues/11966) (Error when trying to export data in CSV (Export data via front end)) reported by @jgmm81
|
||||
- [Issue 11969](https://github.com/firefly-iii/firefly-iii/issues/11969) (Problem found when editing a multi-currency record, as well as details in the "Audit log entries") reported by @jgmm81
|
||||
- [PR 11974](https://github.com/firefly-iii/firefly-iii/pull/11974) (Fix typo in SMTP server comment in .env.example) reported by @NorskNoobing
|
||||
- [Discussion 11977](https://github.com/orgs/firefly-iii/discussions/11977) (CSP header `form-action 'self'` prevents form submission because it's a redirect) started by @superrio0187
|
||||
- [Issue 11978](https://github.com/firefly-iii/firefly-iii/issues/11978) (Tags not associated with any record display incorrect information) reported by @jgmm81
|
||||
- [Issue 11982](https://github.com/firefly-iii/firefly-iii/issues/11982) (Foreign currency account value in primary currency does not update after changing exchange rates) reported by @gattacus
|
||||
- Remove old `zoomLevel` / `zoom_level` database references for tags, since they are no longer queries anyway.
|
||||
|
||||
### API
|
||||
|
||||
- [Issue 11976](https://github.com/firefly-iii/firefly-iii/issues/11976) (New lines are removed from rule description when created using API POST) reported by @AlexRNL
|
||||
|
||||
## v6.5.6 - 2026-03-16
|
||||
|
||||
<!-- summary: This release takes note of some security issues, and fixes interesting bugs. -->
|
||||
|
||||
@@ -16,7 +54,41 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Lots of code cleanup and small quality issues fixed.
|
||||
|
||||
### Fixed
|
||||
- [Issue 11803](https://github.com/firefly-iii/firefly-iii/issues/11803) (Monthly Left budget not correct) reported by @fabienfitoussi
|
||||
- [Issue 11641](https://github.com/firefly-iii/firefly-iii/issues/11641) (Annual budget “Remaining” resets in subsequent months) reported by @maxwell5555
|
||||
- [Discussion 11879](https://github.com/orgs/firefly-iii/discussions/11879) (Searching for accounts should include inactive accounts?) started by @b-ryan
|
||||
- [Issue 11916](https://github.com/firefly-iii/firefly-iii/issues/11916) (Balance is not recalculated when multiple transactions are selected and then deleted) reported by @elp3dr0
|
||||
- [Discussion 11936](https://github.com/orgs/firefly-iii/discussions/11936) (Links in emails don't link to correct domain) started by @SamLMB
|
||||
- [Issue 11944](https://github.com/firefly-iii/firefly-iii/issues/11944) (Stale available_budgets rows prevent disabling a currency after switching default) reported by @k-leveller
|
||||
- [Issue 11953](https://github.com/firefly-iii/firefly-iii/issues/11953) ("Actions" buttons no longer appears after selecting multiple transactions) reported by @crtxcr
|
||||
- [Issue 11954](https://github.com/firefly-iii/firefly-iii/issues/11954) (Search results are not shown after loading) reported by @fabienfitoussi
|
||||
|
||||
### Security
|
||||
|
||||
- Credits go to Igor for finding some interesting issues in Firefly III. They have been fixed.
|
||||
|
||||
> [!NOTE]
|
||||
> As AI-code scanning tools like Claude and Co-Pilot get more advanced, many (new) issues are being reported through (semi-)automated means. I have updated [the security policy](https://github.com/firefly-iii/firefly-iii/security/policy) to reflect my stance on this. The following security related issues no longer need reporting:
|
||||
|
||||
- It is possible to point webhooks to private or internal IPs.
|
||||
- You can see all transaction link types.
|
||||
- `unsafe-inline` is allowed for CSS, which means you can overrule the layout if you manage to get CSS on the page.
|
||||
|
||||
## v6.5.5 - 2026-03-15
|
||||
|
||||
<!-- summary: This release takes note of some security issues, and fixes interesting bugs. -->
|
||||
|
||||
### Added
|
||||
|
||||
- Add the ability for Fosstodon posts to read a summary of the changelog.
|
||||
|
||||
### Changed
|
||||
|
||||
- Lots of code cleanup and small quality issues fixed.
|
||||
|
||||
### Fixed
|
||||
- [Issue 11803](https://github.com/firefly-iii/firefly-iii/issues/11803) (Monthly Left budget not correct) reported by @fabienfitoussi
|
||||
- [Issue 11641](https://github.com/firefly-iii/firefly-iii/issues/11641) (Annual budget “Remaining” resets in subsequent months) reported by @maxwell5555
|
||||
- [Discussion 11879](https://github.com/orgs/firefly-iii/discussions/11879) (Searching for accounts should include inactive accounts?) started by @b-ryan
|
||||
- [Issue 11916](https://github.com/firefly-iii/firefly-iii/issues/11916) (Balance is not recalculated when multiple transactions are selected and then deleted) reported by @elp3dr0
|
||||
- [Discussion 11936](https://github.com/orgs/firefly-iii/discussions/11936) (Links in emails don't link to correct domain) started by @SamLMB
|
||||
@@ -24,17 +96,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Security
|
||||
|
||||
- Credits go to Igor for finding some interesting issues in Firefly III. They have been fixed.
|
||||
|
||||
> [!NOTE]
|
||||
> A lot of people have access to the new Claude model that has enhanced code scanning capabilities. Many new issues are being reported through (semi-)automated means. I will update the security policy to reflect this. The following security related issues no longer need reporting:
|
||||
> As AI-code scanning tools like Claude and Co-Pilot get more advanced, many (new) issues are being reported through (semi-)automated means. I have updated [the security policy](https://github.com/firefly-iii/firefly-iii/security/policy) to reflect my stance on this. The following security related issues no longer need reporting:
|
||||
|
||||
- It is possible to point webhooks to private or internal IPs.
|
||||
- You can see all transaction link types. If you are an owner, you can also delete them.
|
||||
- You can see all transaction link types.
|
||||
- `unsafe-inline` is allowed for CSS, which means you can overrule the layout if you manage to get CSS on the page.
|
||||
- You can make the rule engine print internal enums.
|
||||
|
||||
### API
|
||||
|
||||
- Initial release.
|
||||
|
||||
## v6.5.4 - 2026-03-06
|
||||
|
||||
@@ -2622,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",
|
||||
|
||||
818
composer.lock
generated
818
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user