mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-06-15 12:07:09 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c30f11bc | |||
| fea97efdbf | |||
| fe0e8796ca | |||
| e83c5b9f86 | |||
| 9558f05947 | |||
| f3d6bb0fb5 | |||
| 57010cd2e0 | |||
| 9436eeacaf | |||
| 7ddf395ea9 | |||
| 492c55bd76 | |||
| 894dea5c9c | |||
| fecf12790d | |||
| 883c083860 | |||
| e059753c43 | |||
| 0bac0aaaee | |||
| 2a68c48e2a | |||
| c394034876 | |||
| 7bd91048ea | |||
| d64bca7700 | |||
| 7d768cfa23 | |||
| fd50fbf193 | |||
| 134c232f45 | |||
| ce603f50d8 | |||
| 3b7bff4c57 | |||
| 1485f99579 | |||
| 7d24783c49 | |||
| c4ee3598e1 | |||
| 8cf8e91448 | |||
| 28f2de0df7 | |||
| 86adf11263 | |||
| 045f875041 | |||
| f781e9f2b6 | |||
| 5eb52812f0 | |||
| 916abd8464 | |||
| 6baca9510f | |||
| 5c22a40849 | |||
| 51e994056a | |||
| 984e735bc1 | |||
| c1a271d9c4 | |||
| 0f9a2f010c | |||
| ed9557aaa0 | |||
| 1cc471fcc4 | |||
| 6c27424bfe | |||
| 38576f7fe0 | |||
| 5b80a5bdbe | |||
| 0202f4abd9 | |||
| 615d568479 | |||
| 2ace0d3f23 | |||
| 42204f8dc1 | |||
| cfac8fa569 | |||
| 04704392f3 | |||
| 4b9bbc9d6a | |||
| f24f535d39 | |||
| 3a9ac03358 | |||
| 4ac7fec5f6 | |||
| b6759c3fa0 | |||
| 4a83b1e3e5 | |||
| 4b701dfc4c | |||
| b2997d0a5a | |||
| fd8722e401 | |||
| e46153330a | |||
| 525f0c752a | |||
| 13e4160e85 | |||
| 7806d63f91 | |||
| 8c620b6536 | |||
| e47ce30579 | |||
| 7e6eadc047 | |||
| dae4f6f351 | |||
| 8c8af51bc4 | |||
| a5e1cba39c | |||
| 96d56ad723 | |||
| e4b1c3045e | |||
| e974594fe3 | |||
| c93a2dc23a | |||
| 639efee78a | |||
| eb4971fec6 | |||
| 0c63a3380d | |||
| edd30b2d42 | |||
| a4f6c2b748 | |||
| 5fc90e0f76 | |||
| e8ab7d8a93 | |||
| e73d04bc0f | |||
| c0eca4298a | |||
| dafb43393a | |||
| ba8155a391 | |||
| 9e4329ebfc | |||
| 60e2645e54 | |||
| 45528cf7d3 |
Generated
+84
-76
@@ -294,42 +294,42 @@
|
||||
},
|
||||
{
|
||||
"name": "ergebnis/agent-detector",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ergebnis/agent-detector.git",
|
||||
"reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64"
|
||||
"reference": "e211f17928c8b95a51e06040792d57f5462fb271"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64",
|
||||
"reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64",
|
||||
"url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271",
|
||||
"reference": "e211f17928c8b95a51e06040792d57f5462fb271",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.50.0",
|
||||
"ergebnis/composer-normalize": "^2.51.0",
|
||||
"ergebnis/license": "^2.7.0",
|
||||
"ergebnis/php-cs-fixer-config": "^6.60.2",
|
||||
"ergebnis/phpstan-rules": "^2.13.1",
|
||||
"ergebnis/phpunit-slow-test-detector": "^2.24.0",
|
||||
"ergebnis/rector-rules": "^1.16.0",
|
||||
"ergebnis/rector-rules": "^1.18.1",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"infection/infection": "^0.26.6",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^2.1.46",
|
||||
"phpstan/phpstan": "^2.1.54",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.4",
|
||||
"phpstan/phpstan-phpunit": "^2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.10",
|
||||
"phpunit/phpunit": "^9.6.34",
|
||||
"rector/rector": "^2.4.1"
|
||||
"rector/rector": "^2.4.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.0-dev"
|
||||
"dev-main": "1.2-dev"
|
||||
},
|
||||
"composer-normalize": {
|
||||
"indent-size": 2,
|
||||
@@ -359,7 +359,7 @@
|
||||
"security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md",
|
||||
"source": "https://github.com/ergebnis/agent-detector"
|
||||
},
|
||||
"time": "2026-04-10T13:45:13+00:00"
|
||||
"time": "2026-05-07T08:19:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "evenement/evenement",
|
||||
@@ -471,16 +471,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.95.1",
|
||||
"version": "v3.95.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065"
|
||||
"reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a9727678fbd12997f1d9de8f4a37824ed9df1065",
|
||||
"reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a28d88a5e172b27e78d0816992b15a9df3da20f1",
|
||||
"reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -512,8 +512,8 @@
|
||||
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"facile-it/paraunit": "^1.3.1 || ^2.8.0",
|
||||
"infection/infection": "^0.32.6",
|
||||
"facile-it/paraunit": "^1.3.1 || ^2.11.0",
|
||||
"infection/infection": "^0.32.7",
|
||||
"justinrainbow/json-schema": "^6.8.0",
|
||||
"keradus/cli-executor": "^2.3",
|
||||
"mikey179/vfsstream": "^1.6.12",
|
||||
@@ -564,7 +564,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.1"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -572,7 +572,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-12T17:00:09+00:00"
|
||||
"time": "2026-05-15T09:20:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
@@ -1255,16 +1255,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
"version": "8.1.0",
|
||||
"version": "8.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||
"reference": "9c957d730257f49c873f3761674559bd90098a7d"
|
||||
"reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/9c957d730257f49c873f3761674559bd90098a7d",
|
||||
"reference": "9c957d730257f49c873f3761674559bd90098a7d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b36d33b6e796513de7cb7df053afb3f55eefcd47",
|
||||
"reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1277,7 +1277,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.1-dev"
|
||||
"dev-main": "8.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1310,7 +1310,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
||||
"security": "https://github.com/sebastianbergmann/diff/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/8.1.0"
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/8.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1330,20 +1330,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-05T12:02:33+00:00"
|
||||
"time": "2026-05-15T04:58:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
|
||||
"reference": "3156577f46a38aa1b9323aad223de7a9cd426782"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782",
|
||||
"reference": "3156577f46a38aa1b9323aad223de7a9cd426782",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1400,7 +1400,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1420,20 +1420,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-13T12:07:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.6.0",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1446,7 +1446,7 @@
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1471,7 +1471,7 @@
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1482,25 +1482,29 @@
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
"time": "2026-04-13T15:52:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6"
|
||||
"reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6",
|
||||
"reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f",
|
||||
"reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1552,7 +1556,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.8"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1572,20 +1576,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-04-18T13:51:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
"version": "v3.6.0",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
|
||||
"reference": "59eb412e93815df44f05f342958efa9f46b1e586"
|
||||
"reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
|
||||
"reference": "59eb412e93815df44f05f342958efa9f46b1e586",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32",
|
||||
"reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1599,7 +1603,7 @@
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1632,7 +1636,7 @@
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
|
||||
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1643,25 +1647,29 @@
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
"time": "2026-01-05T13:30:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "66b769ae743ce2d13e435528fbef4af03d623e5a"
|
||||
"reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a",
|
||||
"reference": "66b769ae743ce2d13e435528fbef4af03d623e5a",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/224db910898ce1317b892a9a1338f1f8f17eb7c7",
|
||||
"reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1698,7 +1706,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1718,7 +1726,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-11T16:39:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
@@ -2440,16 +2448,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
|
||||
"reference": "26d89e459f037d2873300605d0a07e7a8ef84db0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/26d89e459f037d2873300605d0a07e7a8ef84db0",
|
||||
"reference": "26d89e459f037d2873300605d0a07e7a8ef84db0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2481,7 +2489,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2501,20 +2509,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-11T16:56:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.6.1",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
|
||||
"reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a",
|
||||
"reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2532,7 +2540,7 @@
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -2568,7 +2576,7 @@
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2588,7 +2596,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-15T11:30:57+00:00"
|
||||
"time": "2026-03-28T09:44:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
@@ -2658,16 +2666,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
|
||||
"reference": "39be2ad058a3c0bd558edca23e65f009865d75ff"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff",
|
||||
"reference": "39be2ad058a3c0bd558edca23e65f009865d75ff",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2724,7 +2732,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2744,7 +2752,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-13T12:07:53+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
@@ -12,6 +12,14 @@ body:
|
||||
required: true
|
||||
- label: I've used [the search](https://github.com/firefly-iii/firefly-iii/issues?q=is%3Aissue) and this has not been requested before.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Use of AI
|
||||
description: AI agents like Claude and CoPilot are not reliable tools. Do not use them.
|
||||
options:
|
||||
- label: I'm a real person and wrote this bug without assistance from AI.
|
||||
required: true
|
||||
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# [Contributing guidelines](https://docs.firefly-iii.org/explanation/support/#contributing-code)
|
||||
# Contributing guidelines
|
||||
|
||||
[Contributing guidelines](https://docs.firefly-iii.org/explanation/support/#contributing-code)
|
||||
This repository is part of "Firefly III", by being either a main repository for code, a supporting repository with guidelines and documentation, or a repository with tools and secondary code.
|
||||
|
||||
The [contribution guidelines](https://docs.firefly-iii.org/explanation/contributing/) for this repository are the same as they are for ALL Firefly III related repositories, and they can be found in [the Firefly III documentation](https://docs.firefly-iii.org/explanation/contributing/).
|
||||
|
||||
@@ -38,9 +38,11 @@ Example: Fixes #1234. See also #3456.
|
||||
|
||||
#### AI usage disclosure
|
||||
<!--
|
||||
If AI tools were involved in creating this PR, please check all boxes that apply
|
||||
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
|
||||
|
||||
If you remove or skip this disclosure, your PR may be ignored.
|
||||
-->
|
||||
I used AI assistance for:
|
||||
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
|
||||
|
||||
+10
-1
@@ -3,9 +3,18 @@
|
||||
Firefly III is an application to manage your personal finances. As such, the developer has adopted this security
|
||||
disclosure and response policy to ensure that critical issues are responsibly handled.
|
||||
|
||||
## AI-generated security advisories
|
||||
|
||||
> [!WARNING]
|
||||
> Due to a large number of irrelevant, noisy and uninformed AI-generated security advisories coming my way, reporting any the following security issues may result in a permanent ban from the Firefly III organization on GitHub.
|
||||
|
||||
1. Any SSRF in any user provided URL field (webhooks, ntfy, SimpleFIN, Slack). It's by design that users may set-up any URL they want, be it internal, private or non-existing.
|
||||
2. Any XSS issue without a viable attack tree. If you can find a spot where Firefly III or the associated tools render unescaped data, it's not a security issue unless you can show me an actual attack that gets that data into the system.
|
||||
3. Any issue that is not true. AI models have already *hallucinated* security issues in Firefly III. They've referred to **non-existing** functions, templates and files. Including line numbers and code excerpts. Validate your findings before you report them to me.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest Firefly III release is maintained. Applicable fixes, including security fixes, will not backported to
|
||||
Only the latest Firefly III release is maintained. Applicable fixes, including security fixes, will not be backported to
|
||||
older release branches. Please refer to [releases.md](https://github.com/firefly-iii/firefly-iii/blob/main/releases.md) for details.
|
||||
|
||||
## Reporting a vulnerability - private disclosure process
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Dependency review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: 'PRs - Check for AI disclosure'
|
||||
|
||||
# the workflow to execute on is comments that are newly created
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened ]
|
||||
|
||||
# permissions needed for reacting to IssueOps commands on issues and PRs
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
respond:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
BODY=$(gh pr view $NUMBER --json body)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
# $BODY must contain one of these four uses.
|
||||
if [[ $BODY != *"Code generation"* &&
|
||||
$BODY != *"Test/benchmark generation"* &&
|
||||
$BODY != *"Documentation"* &&
|
||||
$BODY != *"Research and understanding"* &&
|
||||
$BODY != *"I used AI assistance for"* ]]; then
|
||||
MESSAGE="Hi there!
|
||||
|
||||
This is an automated reply. \`Share and enjoy\`
|
||||
|
||||
You triggered an automated reply, because it seems you removed or changed the AI assistance disclosure from the PR template. Without a valid disclosure, your PR cannot be processed.
|
||||
|
||||
Even if you did not use AI, this disclosure must be present. Please reply to your PR and explain your use of AI in any or all of the following areas:
|
||||
|
||||
1. Code generation (e.g., when writing an implementation or fixing a bug)
|
||||
2. Test/benchmark generation
|
||||
3. Documentation (including examples)
|
||||
4. Research and understanding
|
||||
|
||||
There cannot be interaction with your PR without this disclosure.
|
||||
|
||||
If the disclosure is present but the bot did not pick up on it, please accept my apologies for the intrusion. Contrary to other bots, this one is just a simple \`bash\` script and it may be wrong."
|
||||
|
||||
gh pr comment "$NUMBER" --body "$MESSAGE"
|
||||
echo "Triggered on AI disclosure missing."
|
||||
exit 0
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -99,11 +99,6 @@ jobs:
|
||||
env:
|
||||
FIREFLY_III_ROOT: /github/workspace
|
||||
GH_TOKEN: ''
|
||||
- name: Setup Mago
|
||||
uses: nhedger/setup-mago@v1
|
||||
with:
|
||||
version: "latest"
|
||||
working-directory: "."
|
||||
- name: Run CI
|
||||
run: |
|
||||
cp .env.example .env
|
||||
@@ -113,17 +108,19 @@ jobs:
|
||||
|
||||
# format code.
|
||||
echo "Will now run Mago Format"
|
||||
mago format
|
||||
./vendor/bin/mago format
|
||||
sudo chown -R runner:docker resources/lang
|
||||
echo "Will now run PHPCS"
|
||||
.ci/phpcs.sh
|
||||
|
||||
# lint and check
|
||||
echo "Will now run Mago Lint"
|
||||
mago lint
|
||||
./vendor/bin/mago lint
|
||||
echo "Will now run PHPstan"
|
||||
.ci/phpstan.sh
|
||||
rm .env
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Crowdin action
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
|
||||
@@ -179,9 +179,9 @@ final class CategoryController extends Controller
|
||||
|
||||
// order by amount
|
||||
usort($return, static fn (array $a, array $b): int => ((float) $a['entries']['spent'] + (float) $a['entries']['earned'])
|
||||
< ((float) $b['entries']['spent'] + (float) $b['entries']['earned'])
|
||||
? 1
|
||||
: -1);
|
||||
< ((float) $b['entries']['spent'] + (float) $b['entries']['earned'])
|
||||
? 1
|
||||
: -1);
|
||||
|
||||
return response()->json($this->clean($return));
|
||||
}
|
||||
|
||||
@@ -158,7 +158,10 @@ final class TagController extends Controller
|
||||
'currency_id' => (string) $foreignCurrencyId,
|
||||
'currency_code' => $journal['foreign_currency_code'],
|
||||
];
|
||||
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
|
||||
$response[$foreignKey]['difference'] = bcadd(
|
||||
(string) $response[$foreignKey]['difference'],
|
||||
Steam::positive($journal['foreign_amount'])
|
||||
);
|
||||
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,10 @@ final class TagController extends Controller
|
||||
'currency_id' => (string) $foreignCurrencyId,
|
||||
'currency_code' => $journal['foreign_currency_code'],
|
||||
];
|
||||
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
|
||||
$response[$foreignKey]['difference'] = bcadd(
|
||||
(string) $response[$foreignKey]['difference'],
|
||||
Steam::positive($journal['foreign_amount'])
|
||||
);
|
||||
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ final class DestroyController extends Controller
|
||||
|
||||
public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
|
||||
{
|
||||
$first = Carbon::create(1970, 1, 1);
|
||||
$this->repository->deleteRates($from, $to);
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request), $first));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
@@ -74,7 +75,7 @@ final class DestroyController extends Controller
|
||||
if (!$exchangeRate instanceof CurrencyExchangeRate) {
|
||||
throw new FireflyException('Bla');
|
||||
}
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request), $date));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
@@ -85,7 +86,7 @@ final class DestroyController extends Controller
|
||||
$to = $exchangeRate->toCurrency;
|
||||
$this->repository->deleteRate($exchangeRate);
|
||||
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request), $exchangeRate->date));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,9 @@ final class StoreController extends Controller
|
||||
$transactionGroup = $this->groupRepository->store($data);
|
||||
} catch (DuplicateTransactionException $e) {
|
||||
Log::warning('Caught a duplicate transaction. Return error message.');
|
||||
$validator = Validator::make(['transactions' => [['description' => $e->getMessage()]]], ['transactions.0.description' => new IsDuplicateTransaction()]);
|
||||
$validator = Validator::make(['transactions' => [['description' => $e->getMessage()]]], [
|
||||
'transactions.0.description' => new IsDuplicateTransaction(),
|
||||
]);
|
||||
|
||||
throw new ValidationException($validator);
|
||||
} catch (FireflyException $e) {
|
||||
|
||||
@@ -158,7 +158,7 @@ final class ConfigurationController extends Controller
|
||||
$enableExternalRates = FireflyConfig::get('enable_external_rates', false);
|
||||
$allowWebhooks = FireflyConfig::get('allow_webhooks', false);
|
||||
$enableBatchProcessing = FireflyConfig::get('enable_batch_processing', false);
|
||||
$validUrlProtocols = FireflyConfig::get('valid_url_protocols', 'http,https');
|
||||
$validUrlProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'));
|
||||
|
||||
return [
|
||||
'is_demo_site' => $isDemoSite?->data,
|
||||
@@ -171,7 +171,7 @@ final class ConfigurationController extends Controller
|
||||
'enable_external_rates' => $enableExternalRates?->data,
|
||||
'allow_webhooks' => $allowWebhooks?->data,
|
||||
'enable_batch_processing' => $enableBatchProcessing?->data,
|
||||
'valid_url_protocols' => $validUrlProtocols->data ?? 'http,https',
|
||||
'valid_url_protocols' => $validUrlProtocols->data ?? config('firefly.valid_url_protocols'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
@@ -37,7 +38,8 @@ class DestroyedCurrencyExchangeRate extends Event
|
||||
public function __construct(
|
||||
public TransactionCurrency $from,
|
||||
public TransactionCurrency $to,
|
||||
public UserGroup $userGroup
|
||||
public UserGroup $userGroup,
|
||||
public Carbon $date
|
||||
) {
|
||||
Log::debug(sprintf('DestroyedCurrencyExchangeRate(%s, %s) Event', $from->code, $to->code));
|
||||
}
|
||||
|
||||
@@ -245,7 +245,10 @@ class PiggyBankFactory
|
||||
);
|
||||
|
||||
// validate amount:
|
||||
if (array_key_exists('target_amount', $piggyBankData) && '' === (string) $piggyBankData['target_amount']) {
|
||||
if (array_key_exists('target_amount', $piggyBankData) && '' === trim((string) $piggyBankData['target_amount'])) {
|
||||
$piggyBankData['target_amount'] = '0';
|
||||
}
|
||||
if (!array_key_exists('target_amount', $piggyBankData)) {
|
||||
$piggyBankData['target_amount'] = '0';
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class PiggyBankObserver
|
||||
}
|
||||
|
||||
$params = new ConversionParameters();
|
||||
$params->user = $piggyBank->accounts()->first()?->user;
|
||||
$params->user = $piggyBank->accounts()->first()->user;
|
||||
$params->model = $piggyBank;
|
||||
$params->originalCurrency = $piggyBank->transactionCurrency;
|
||||
$params->amountField = 'target_amount';
|
||||
|
||||
@@ -40,8 +40,7 @@ class ReportHelper implements ReportHelperInterface
|
||||
/**
|
||||
* ReportHelper constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var BudgetRepositoryInterface The budget repository */
|
||||
public function __construct(/** @var BudgetRepositoryInterface The budget repository */
|
||||
protected BudgetRepositoryInterface $budgetRepository
|
||||
) {}
|
||||
|
||||
|
||||
@@ -255,7 +255,10 @@ final class IndexController extends Controller
|
||||
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
|
||||
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
|
||||
if ($count > 0) {
|
||||
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
|
||||
$avg = bcdiv(
|
||||
bcadd((string) $bill['amount_min'], (string) $bill['amount_max']),
|
||||
'2'
|
||||
);
|
||||
$avg = bcmul($avg, (string) $count);
|
||||
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
|
||||
Log::debug(
|
||||
|
||||
@@ -198,7 +198,13 @@ final class BudgetLimitController extends Controller
|
||||
if ($request->expectsJson()) {
|
||||
$array = $limit->toArray();
|
||||
// add some extra metadata:
|
||||
$spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);
|
||||
$spentArr = $this->opsRepository->sumExpenses(
|
||||
$limit->start_date,
|
||||
$limit->end_date,
|
||||
null,
|
||||
new Collection()->push($budget),
|
||||
$currency
|
||||
);
|
||||
$array['spent'] = $spentArr[$currency->id]['sum'] ?? '0';
|
||||
$array['left_formatted'] = Amount::formatAnything($limit->transactionCurrency, bcadd($array['spent'], (string) $array['amount']));
|
||||
$array['amount_formatted'] = Amount::formatAnything($limit->transactionCurrency, $limit['amount']);
|
||||
|
||||
@@ -245,13 +245,8 @@ final class IndexController extends Controller
|
||||
$inPast = $limitPeriod->startsBefore(now()) && $limitPeriod->endsBefore(now());
|
||||
$currency = $limit->transactionCurrency ?? $primaryCurrency;
|
||||
$amount = Steam::bcround($limit->amount, $currency->decimal_places);
|
||||
$spent = $this->opsRepository->sumExpenses(
|
||||
$limit->start_date,
|
||||
$limit->end_date,
|
||||
null,
|
||||
new Collection()->push($budget),
|
||||
$currency
|
||||
);
|
||||
$spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);
|
||||
|
||||
$spentAmount = $spent[$currency->id]['sum'] ?? '0';
|
||||
$array['budgeted'][] = [
|
||||
'id' => $limit->id,
|
||||
@@ -289,7 +284,10 @@ final class IndexController extends Controller
|
||||
|
||||
if (array_key_exists($currency->id, $spentArr) && array_key_exists('sum', $spentArr[$currency->id])) {
|
||||
$array['spent'][$currency->id]['spent'] = $spentArr[$currency->id]['sum'];
|
||||
$array['spent'][$currency->id]['spent_outside'] = bcmul(bcsub($spentInLimits[$currency->id], $spentArr[$currency->id]['sum']), '-1');
|
||||
$array['spent'][$currency->id]['spent_outside'] = Steam::negative(bcsub(
|
||||
$spentInLimits[$currency->id],
|
||||
$spentArr[$currency->id]['sum']
|
||||
));
|
||||
$array['spent'][$currency->id]['currency_id'] = $currency->id;
|
||||
$array['spent'][$currency->id]['currency_symbol'] = $currency->symbol;
|
||||
$array['spent'][$currency->id]['currency_decimal_places'] = $currency->decimal_places;
|
||||
|
||||
@@ -589,7 +589,6 @@ final class AccountController extends Controller
|
||||
Log::debug('End of chart loop.');
|
||||
// second loop (yes) to create nice array with info! Yay!
|
||||
$chartData = [];
|
||||
|
||||
foreach ($return as $key => $info) {
|
||||
if ('balance' !== $key && 'pc_balance' !== $key) {
|
||||
// assume it's a currency:
|
||||
@@ -608,6 +607,11 @@ final class AccountController extends Controller
|
||||
$info['currency_code'] = $this->primaryCurrency->code;
|
||||
$info['label'] = sprintf('%s (%s) (%s)', $account->name, (string) trans('firefly.sum'), $this->primaryCurrency->symbol);
|
||||
}
|
||||
// do not add pc_balance to the array if the account is in the primary currency anyway,
|
||||
// and it has no currency balances.
|
||||
if (2 === count(array_keys($return)) && 'pc_balance' === $key && $accountCurrency->id === $this->primaryCurrency->id) {
|
||||
continue;
|
||||
}
|
||||
$chartData[] = $info;
|
||||
}
|
||||
|
||||
|
||||
@@ -539,7 +539,13 @@ final class BudgetController extends Controller
|
||||
}
|
||||
|
||||
// get spent amount in this period for this currency.
|
||||
$sum = $this->opsRepository->sumExpenses($currentStart, $currentEnd, $accounts, new Collection()->push($budget), $currency);
|
||||
$sum = $this->opsRepository->sumExpenses(
|
||||
$currentStart,
|
||||
$currentEnd,
|
||||
$accounts,
|
||||
new Collection()->push($budget),
|
||||
$currency
|
||||
);
|
||||
$amount = Steam::positive($sum[$currency->id]['sum'] ?? '0');
|
||||
$chartData[0]['entries'][$title] = Steam::bcround($amount, $currency->decimal_places);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Controllers\Profile;
|
||||
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -144,6 +145,7 @@ final class OAuthController extends Controller
|
||||
->where('expires_at', '>', Date::now())
|
||||
->get()
|
||||
->filter(fn (#[SensitiveParameter] Token $token) => $token->client->hasGrantType('personal_access'))
|
||||
->values()
|
||||
;
|
||||
|
||||
return response()->json($tokens);
|
||||
@@ -165,9 +167,10 @@ final class OAuthController extends Controller
|
||||
|
||||
public function storeClient(Request $request): JsonResponse
|
||||
{
|
||||
$validProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
|
||||
$this->validation->make($request->only(['name', 'redirect_uris', 'confidential']), [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'redirect_uris' => ['required', 'url'],
|
||||
'redirect_uris' => ['required', sprintf('url:%s', $validProtocols)],
|
||||
'confidential' => 'boolean',
|
||||
])->validate();
|
||||
|
||||
@@ -195,15 +198,15 @@ final class OAuthController extends Controller
|
||||
|
||||
public function updateClient(Request $request, string $clientId): Client|Response
|
||||
{
|
||||
$client = auth()->user()->oauthApps()->where('revoked', false)->find($clientId);
|
||||
|
||||
$client = auth()->user()->oauthApps()->where('revoked', false)->find($clientId);
|
||||
$validProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
|
||||
if (null === $client) {
|
||||
return new Response('', 404);
|
||||
}
|
||||
|
||||
$this->validation->make($request->only(['name', 'redirect_uris']), [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'redirect_uris' => ['required', 'url'],
|
||||
'redirect_uris' => ['required', sprintf('url:%s', $validProtocols)],
|
||||
])->validate();
|
||||
|
||||
$this->clients->update($client, $request->input('name'), explode(',', $request->input('redirect_uris'))); // FIXME replace
|
||||
|
||||
@@ -284,7 +284,7 @@ final class BudgetController extends Controller
|
||||
$cache->addProperty('budget-period-report');
|
||||
$cache->addProperty($accounts->pluck('id')->toArray());
|
||||
if ($cache->has()) {
|
||||
return $cache->get();
|
||||
// return $cache->get();
|
||||
}
|
||||
|
||||
$periods = Navigation::listOfPeriods($start, $end);
|
||||
@@ -292,7 +292,6 @@ final class BudgetController extends Controller
|
||||
|
||||
// list expenses for budgets in account(s)
|
||||
$expenses = $this->opsRepository->listExpenses($start, $end, $accounts);
|
||||
|
||||
$report = [];
|
||||
foreach ($expenses as $currency) {
|
||||
foreach ($currency['budgets'] as $budget) {
|
||||
@@ -300,9 +299,12 @@ final class BudgetController extends Controller
|
||||
foreach ($budget['transaction_journals'] as $journal) {
|
||||
// #10678
|
||||
// skip transactions between two asset / liability accounts.
|
||||
// #12223
|
||||
// must also be of the same type to be skipped
|
||||
if (
|
||||
in_array($journal['source_account_type'], config('firefly.valid_currency_account_types'), true)
|
||||
&& in_array($journal['destination_account_type'], config('firefly.valid_currency_account_types'), true)
|
||||
&& $journal['source_account_type'] === $journal['destination_account_type']
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,9 @@ class Authenticate
|
||||
/**
|
||||
* Create a new middleware instance.
|
||||
*/
|
||||
public function __construct(
|
||||
/**
|
||||
* The authentication factory instance.
|
||||
*/
|
||||
public function __construct(/**
|
||||
* The authentication factory instance.
|
||||
*/
|
||||
protected Auth $auth
|
||||
) {}
|
||||
|
||||
|
||||
@@ -42,10 +42,9 @@ class Binder
|
||||
/**
|
||||
* Binder constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
/**
|
||||
* The authentication factory instance.
|
||||
*/
|
||||
public function __construct(/**
|
||||
* The authentication factory instance.
|
||||
*/
|
||||
protected Auth $auth
|
||||
) {
|
||||
$this->binders = Domain::getBindables();
|
||||
|
||||
@@ -25,6 +25,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Rules\Admin\IsValidSlackOrDiscordUrl;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -65,9 +66,10 @@ class NotificationRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'slack_webhook_url' => ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()],
|
||||
'ntfy_server' => ['nullable', 'url', 'min:1'],
|
||||
$validProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
|
||||
$rules = [
|
||||
'slack_webhook_url' => ['nullable', sprintf('url:%s', $validProtocols), 'min:1', new IsValidSlackOrDiscordUrl()],
|
||||
'ntfy_server' => ['nullable', sprintf('url:%s', $validProtocols), 'min:1'],
|
||||
'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||
'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||
];
|
||||
|
||||
@@ -25,6 +25,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Rules\Admin\IsValidSlackOrDiscordUrl;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
@@ -39,9 +40,10 @@ class PreferencesRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'slack_webhook_url' => ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()],
|
||||
'ntfy_server' => ['nullable', 'url', 'min:1'],
|
||||
$validProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
|
||||
$rules = [
|
||||
'slack_webhook_url' => ['nullable', sprintf('url:%s', $validProtocols), 'min:1', new IsValidSlackOrDiscordUrl()],
|
||||
'ntfy_server' => ['nullable', sprintf('url:%s', $validProtocols), 'min:1'],
|
||||
'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||
'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||
];
|
||||
|
||||
@@ -122,7 +122,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
|
||||
// if has one, calculate expenses and use that as a base.
|
||||
$repository = app(OperationsRepositoryInterface::class);
|
||||
$repository->setUser($autoBudget->budget->user);
|
||||
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection()->push($autoBudget->budget), $autoBudget->transactionCurrency);
|
||||
$spent = $repository->sumExpenses(
|
||||
$previousStart,
|
||||
$previousEnd,
|
||||
null,
|
||||
new Collection()->push($autoBudget->budget),
|
||||
$autoBudget->transactionCurrency
|
||||
);
|
||||
$currencyId = $autoBudget->transaction_currency_id;
|
||||
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
|
||||
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
|
||||
@@ -212,7 +218,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
|
||||
// if has one, calculate expenses and use that as a base.
|
||||
$repository = app(OperationsRepositoryInterface::class);
|
||||
$repository->setUser($autoBudget->budget->user);
|
||||
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection()->push($autoBudget->budget), $autoBudget->transactionCurrency);
|
||||
$spent = $repository->sumExpenses(
|
||||
$previousStart,
|
||||
$previousEnd,
|
||||
null,
|
||||
new Collection()->push($autoBudget->budget),
|
||||
$autoBudget->transactionCurrency
|
||||
);
|
||||
$currencyId = $autoBudget->transaction_currency_id;
|
||||
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
|
||||
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Listeners\Model\CurrencyExchangeRate;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
@@ -42,20 +43,22 @@ class ProcessesExchangeRates
|
||||
Preferences::mark();
|
||||
Cache::clear();
|
||||
if ($event instanceof DestroyedCurrencyExchangeRate) {
|
||||
$this->handleCurrency($event->userGroup, $event->from);
|
||||
$this->handleCurrency($event->userGroup, $event->to);
|
||||
$this->handleCurrency($event->userGroup, $event->from, $event->date);
|
||||
$this->handleCurrency($event->userGroup, $event->to, $event->date);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency);
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->toCurrency);
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency, $event->rate->date);
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->toCurrency, $event->rate->date);
|
||||
}
|
||||
|
||||
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency, Carbon $date): void
|
||||
{
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
$calculator->setDate($date);
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s.', $currency->code));
|
||||
$date->startOfDay();
|
||||
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s after %s.', $currency->code, $date->format('Y-m-d')));
|
||||
|
||||
$calculator->recalculateForGroupAndCurrency($userGroup, $currency);
|
||||
// $calculator->recalculateForGroup($userGroup);
|
||||
|
||||
@@ -335,7 +335,9 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
|
||||
$limit = new BudgetLimit();
|
||||
$limit->budget()->associate($budget);
|
||||
$limit->start_date = $data['start_date']->format('Y-m-d');
|
||||
$limit->start_date_tz = $data['start_date']->format('e');
|
||||
$limit->end_date = $data['end_date']->format('Y-m-d');
|
||||
$limit->end_date_tz = $data['end_date']->format('e');
|
||||
$limit->amount = $data['amount'];
|
||||
$limit->generated = $data['generated'] ?? false;
|
||||
$limit->period = $data['period'] ?? '';
|
||||
|
||||
@@ -385,9 +385,9 @@ trait ModifiesPiggyBanks
|
||||
$piggyBank->target_date = $data['target_date'];
|
||||
$piggyBank->target_date_tz = $data['target_date']?->format('e');
|
||||
}
|
||||
if (array_key_exists('start_date', $data)) {
|
||||
if (array_key_exists('start_date', $data) && '' !== $data['start_date']) {
|
||||
$piggyBank->start_date = $data['start_date'];
|
||||
$piggyBank->start_date_tz = $data['target_date']?->format('e');
|
||||
$piggyBank->start_date_tz = $data['start_date']?->format('e');
|
||||
}
|
||||
$piggyBank->save();
|
||||
|
||||
|
||||
@@ -342,33 +342,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface, UserGroupInte
|
||||
return $piggyBank->piggyBankRepetitions()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggested amount the user should save per month, or "".
|
||||
*/
|
||||
public function getSuggestedMonthlyAmount(PiggyBank $piggyBank): string
|
||||
{
|
||||
$savePerMonth = '0';
|
||||
$currentAmount = $this->getCurrentAmount($piggyBank);
|
||||
if (null !== $piggyBank->target_date && $currentAmount < $piggyBank->target_amount) {
|
||||
$now = today(config('app.timezone'));
|
||||
$startDate = null !== $piggyBank->start_date && $piggyBank->start_date->gte($now) ? $piggyBank->start_date : $now;
|
||||
$diffInMonths = (int) $startDate->diffInMonths($piggyBank->target_date);
|
||||
$remainingAmount = bcsub((string) $piggyBank->target_amount, $currentAmount);
|
||||
|
||||
// more than 1 month to go and still need money to save:
|
||||
if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = bcdiv($remainingAmount, (string) $diffInMonths);
|
||||
}
|
||||
|
||||
// less than 1 month to go but still need money to save:
|
||||
if (0 === $diffInMonths && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = $remainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
return $savePerMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get for piggy account what is left to put in piggies.
|
||||
*/
|
||||
|
||||
@@ -111,11 +111,6 @@ interface PiggyBankRepositoryInterface
|
||||
|
||||
public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition;
|
||||
|
||||
/**
|
||||
* Returns the suggested amount the user should save per month, or "".
|
||||
*/
|
||||
public function getSuggestedMonthlyAmount(PiggyBank $piggyBank): string;
|
||||
|
||||
/**
|
||||
* Get for piggy account what is left to put in piggies.
|
||||
*/
|
||||
|
||||
@@ -166,7 +166,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
|
||||
$set = TransactionJournalLink::where(static function (Builder $q) use ($journals): void {
|
||||
$q->whereIn('source_id', $journals);
|
||||
$q->orWhereIn('destination_id', $journals);
|
||||
})->with(['source', 'destination', 'source.transactions'])->leftJoin('link_types', 'link_types.id', '=', 'journal_links.link_type_id')->get([
|
||||
})->with(['source', 'notes', 'destination', 'source.transactions'])->leftJoin('link_types', 'link_types.id', '=', 'journal_links.link_type_id')->get([
|
||||
'journal_links.*',
|
||||
'link_types.inward',
|
||||
'link_types.outward',
|
||||
@@ -191,6 +191,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
|
||||
'editable' => 1 === (int) $entry->editable,
|
||||
'amount' => $amount,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'notes' => null === $entry->notes->first() ? '' : $entry->notes->first()->text,
|
||||
];
|
||||
}
|
||||
if ($journalId === $entry->destination_id) {
|
||||
@@ -204,6 +205,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
|
||||
'editable' => 1 === (int) $entry->editable,
|
||||
'amount' => $amount,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'notes' => null === $entry->notes->first() ? '' : $entry->notes->first()->text,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Services\Internal\Recalculate;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
|
||||
use FireflyIII\Handlers\Observer\TransactionObserver;
|
||||
use FireflyIII\Models\Account;
|
||||
@@ -52,6 +53,13 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PrimaryAmountRecalculationService
|
||||
{
|
||||
private Carbon $date;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->date = Carbon::createFromDate(1970, 1, 1);
|
||||
}
|
||||
|
||||
public function recalculate(): void
|
||||
{
|
||||
if (false === FireflyConfig::get('enable_exchange_rates', config('cer.enabled'))->data) {
|
||||
@@ -106,12 +114,18 @@ class PrimaryAmountRecalculationService
|
||||
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
}
|
||||
|
||||
public function setDate(?Carbon $date): void
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
// 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('transaction_journals.date', '>=', $this->date)
|
||||
->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');
|
||||
@@ -147,6 +161,7 @@ class PrimaryAmountRecalculationService
|
||||
$set = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where('transaction_journals.date', '>=', $this->date)
|
||||
->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');
|
||||
@@ -279,7 +294,15 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
private function recalculateBudgetLimits(Budget $budget, TransactionCurrency $currency): void
|
||||
{
|
||||
$set = $budget->budgetlimits()->where('transaction_currency_id', '!=', $currency->id)->get();
|
||||
$set = $budget
|
||||
->budgetlimits()
|
||||
->where(function (EloquentBuilder $q): void {
|
||||
$q->where('budget_limits.start_date', '>=', $this->date);
|
||||
$q->orWhere('budget_limits.end_date', '<=', $this->date);
|
||||
})
|
||||
->where('transaction_currency_id', '!=', $currency->id)
|
||||
->get()
|
||||
;
|
||||
|
||||
/** @var BudgetLimit $limit */
|
||||
foreach ($set as $limit) {
|
||||
@@ -436,6 +459,7 @@ class PrimaryAmountRecalculationService
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where('transaction_journals.date', '>=', $this->date)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
|
||||
@@ -741,12 +741,12 @@ class JournalUpdateService
|
||||
if (null === $group || null === $this->transactionJournal) {
|
||||
return;
|
||||
}
|
||||
if (0 === bccomp($source->foreign_amount, $foreignAmount)) {
|
||||
if (0 === bccomp(Steam::positive($originalSourceAmount), Steam::positive($foreignAmount))) {
|
||||
Log::debug('Amount was not actually changed, return.');
|
||||
|
||||
return;
|
||||
}
|
||||
Log::debug('Amount was changed, needs audit log entry.');
|
||||
Log::debug(sprintf('Amount was changed (%s -> %s), needs audit log entry.', $originalSourceAmount, $foreignAmount));
|
||||
$transfer = TransactionTypeEnum::TRANSFER->value === $this->transactionJournal->transactionType->type;
|
||||
// $withdrawal = TransactionTypeEnum::WITHDRAWAL->value === $this->transactionJournal->transactionType->type;
|
||||
$deposit = TransactionTypeEnum::DEPOSIT->value === $this->transactionJournal->transactionType->type;
|
||||
|
||||
@@ -222,7 +222,14 @@ trait AugumentData
|
||||
$currentEnd->addMonth();
|
||||
}
|
||||
// primary currency amount.
|
||||
$expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, $this->convertToPrimary);
|
||||
$expenses = $opsRepository->sumExpenses(
|
||||
$currentStart,
|
||||
$currentEnd,
|
||||
null,
|
||||
$budgetCollection,
|
||||
$entry->transactionCurrency,
|
||||
$this->convertToPrimary
|
||||
);
|
||||
$spent = $expenses[$currency->id]['sum'] ?? '0';
|
||||
$entry->pc_spent = $spent;
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
|
||||
}
|
||||
|
||||
// get suggested per month.
|
||||
|
||||
$meta['save_per_month'] = Steam::bcround(
|
||||
$this->getSuggestedMonthlyAmount($this->date, $item->target_date, $meta['target_amount'], $meta['current_amount']),
|
||||
$currency->decimal_places
|
||||
@@ -301,23 +302,21 @@ class PiggyBankEnrichment implements EnrichmentInterface
|
||||
if (null === $targetAmount || !$targetDate instanceof Carbon || !$startDate instanceof Carbon) {
|
||||
return '0';
|
||||
}
|
||||
$savePerMonth = '0';
|
||||
if (1 === bccomp($targetAmount, $currentAmount)) {
|
||||
$now = today(config('app.timezone'));
|
||||
$diffInMonths = (int) $startDate->diffInMonths($targetDate);
|
||||
$diffInMonths = ceil($startDate->diffInMonths($targetDate));
|
||||
$remainingAmount = bcsub($targetAmount, $currentAmount);
|
||||
|
||||
// more than 1 month to go and still need money to save:
|
||||
if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = bcdiv($remainingAmount, (string) $diffInMonths);
|
||||
return bcdiv($remainingAmount, (string) $diffInMonths);
|
||||
}
|
||||
|
||||
// less than 1 month to go but still need money to save:
|
||||
if (0 === $diffInMonths && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = $remainingAmount;
|
||||
if (1 === bccomp($remainingAmount, '0')) {
|
||||
return $remainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
return $savePerMonth;
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +354,10 @@ class RecurringEnrichment implements EnrichmentInterface
|
||||
|
||||
/** @var RecurrenceRepetition $repetition */
|
||||
foreach ($set as $repetition) {
|
||||
$recurrence = $this->collection->filter(static fn (Recurrence $item): bool => (int) $item->id === (int) $repetition->recurrence_id)->first();
|
||||
$recurrence = $this->collection
|
||||
->filter(static fn (Recurrence $item): bool => (int) $item->id === (int) $repetition->recurrence_id)
|
||||
->first()
|
||||
;
|
||||
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
|
||||
$recurrenceId = (int) $repetition->recurrence_id;
|
||||
$repId = (int) $repetition->id;
|
||||
|
||||
@@ -141,6 +141,9 @@ class AccountBalanceCalculator
|
||||
foreach ($set as $entry) {
|
||||
// Log::debug(sprintf('Processing transaction #%d with currency #%d and amount %s', $entry->id, $entry->transaction_currency_id, Steam::bcround($entry->amount, 2)));
|
||||
// start with empty array:
|
||||
$entry->account_id = (int) $entry->account_id;
|
||||
$entry->transaction_currency_id = (int) $entry->transaction_currency_id;
|
||||
|
||||
$balances[$entry->account_id] ??= [];
|
||||
$balances[$entry->account_id][$entry->transaction_currency_id] ??= [
|
||||
self::getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore),
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v6.6.2 - 2026-04-28
|
||||
|
||||
<!-- summary: This releases fixes a security issue and some small UI issues. Please upgrade at your earliest convenience. -->
|
||||
|
||||
### Added
|
||||
|
||||
- [PR 12179](https://github.com/firefly-iii/firefly-iii/pull/12179) (implement password validation JS script) reported by @tasnim0tantawi
|
||||
- [PR 12182](https://github.com/firefly-iii/firefly-iii/pull/12182) (fix shrinked sidebar expanding when navigating by clicking on icons) reported by @tasnim0tantawi
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 12169](https://github.com/firefly-iii/firefly-iii/issues/12169) (The 'Running balance' column is not showing the respective calculation instantly for new records that use 'Rules') reported by @jgmm81
|
||||
- [Issue 12186](https://github.com/firefly-iii/firefly-iii/issues/12186) (Set a year validator accepted by the system when saving or editing a transaction) reported by @jgmm81
|
||||
|
||||
### Security
|
||||
|
||||
- Fixed an issue where oAuth tokens could be generated before you confirmed your 2FA state. This would allow access to your data when your password was stolen, despite you having MFA enabled.
|
||||
|
||||
## v6.6.1 - 2026-04-19
|
||||
|
||||
<!-- summary: This releases upgrades many dependencies and will invalidate all of your OAuth-tokens and clients. -->
|
||||
|
||||
+3
-2
@@ -111,7 +111,9 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3",
|
||||
"carthage-software/mago": "^1.24.0",
|
||||
"driftingly/rector-laravel": "^2.0",
|
||||
"ergebnis/phpstan-rules": "^2",
|
||||
"fakerphp/faker": "1.*",
|
||||
"filp/whoops": "2.*",
|
||||
"fruitcake/laravel-debugbar": "^4.0",
|
||||
@@ -124,8 +126,7 @@
|
||||
"phpstan/phpstan-strict-rules": "^2",
|
||||
"phpunit/phpunit": "^13",
|
||||
"rector/rector": "^2.3",
|
||||
"thecodingmachine/phpstan-safe-rule": "^1.4",
|
||||
"ergebnis/phpstan-rules": "^2"
|
||||
"thecodingmachine/phpstan-safe-rule": "^1.4"
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-php54": "*",
|
||||
|
||||
Generated
+383
-227
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
use Pdo\Mysql;
|
||||
|
||||
use function Safe\parse_url;
|
||||
|
||||
@@ -52,22 +53,22 @@ $mySqlSSLOptions = [];
|
||||
$useSSL = env_default_when_empty(env('MYSQL_USE_SSL'), false);
|
||||
if (false !== $useSSL && null !== $useSSL && '' !== $useSSL) {
|
||||
if (null !== $mysql_ssl_ca_dir) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $mysql_ssl_ca_dir;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_CAPATH] = $mysql_ssl_ca_dir;
|
||||
}
|
||||
if (null !== $mysql_ssl_ca_file) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_CA] = $mysql_ssl_ca_file;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_CA] = $mysql_ssl_ca_file;
|
||||
}
|
||||
if (null !== $mysql_ssl_cert) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_CERT] = $mysql_ssl_cert;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_CERT] = $mysql_ssl_cert;
|
||||
}
|
||||
if (null !== $mysql_ssl_key) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_KEY] = $mysql_ssl_key;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_KEY] = $mysql_ssl_key;
|
||||
}
|
||||
if (null !== $mysql_ssl_ciphers) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $mysql_ssl_ciphers;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_CIPHER] = $mysql_ssl_ciphers;
|
||||
}
|
||||
if (null !== $mysql_ssl_verify) {
|
||||
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $mysql_ssl_verify;
|
||||
$mySqlSSLOptions[Mysql::ATTR_SSL_VERIFY_SERVER_CERT] = $mysql_ssl_verify;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -78,8 +78,8 @@ return [
|
||||
'running_balance_column' => (bool)env_default_when_empty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
|
||||
// see cer.php for exchange rates feature flag.
|
||||
],
|
||||
'version' => 'develop/2026-04-27',
|
||||
'build_time' => 1777263537,
|
||||
'version' => 'develop/2026-05-17',
|
||||
'build_time' => 1778986654,
|
||||
'api_version' => '2.1.0', // field is no longer used.
|
||||
'db_version' => 28, // field is no longer used.
|
||||
|
||||
@@ -198,6 +198,7 @@ return [
|
||||
'default_locale' => env_default_when_empty(env('DEFAULT_LOCALE'), 'equal'),
|
||||
|
||||
// account types that may have or set a currency
|
||||
|
||||
'valid_currency_account_types' => [
|
||||
AccountTypeEnum::ASSET->value,
|
||||
AccountTypeEnum::LOAN->value,
|
||||
@@ -224,7 +225,7 @@ return [
|
||||
'available_dark_modes' => ['light', 'dark', 'browser'],
|
||||
'bill_reminder_periods' => [90, 30, 14, 7, 0],
|
||||
'valid_view_ranges' => ['1D', '1W', '1M', '3M', '6M', '1Y'],
|
||||
'valid_url_protocols' => env_default_when_empty(env('VALID_URL_PROTOCOLS'), 'http,https,ftp,ftps,mailto'), // no longer used, only for default.
|
||||
'valid_url_protocols' => env_default_when_empty(env('VALID_URL_PROTOCOLS'), 'http,https,ftp,ftps,mailto,abacusfiiiapp'), // no longer used, only for default.
|
||||
'allowedMimes' => [
|
||||
// plain files
|
||||
'text/plain',
|
||||
|
||||
Generated
+305
-241
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -7,7 +7,7 @@
|
||||
"resources/assets/v2"
|
||||
],
|
||||
"devDependencies": {
|
||||
"postcss": "^8.4.47"
|
||||
"postcss": "^8.5.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"patch-package": "^8.0.1"
|
||||
|
||||
@@ -100,29 +100,29 @@ $(function () {
|
||||
// show rule triggers
|
||||
$('.rule-triggers-show').click(function (e) {
|
||||
var obj = $(e.currentTarget);
|
||||
$('.rule-trigger-list[data-id="' + obj.data('id') + '"]').show();
|
||||
$('.rule-triggers-show[data-id="' + obj.data('id') + '"]').hide();
|
||||
$('.rule-trigger-list[data-id="' + obj.data('id') + '"]').removeClass('hidden');
|
||||
$('.rule-triggers-show[data-id="' + obj.data('id') + '"]').addClass('hidden');
|
||||
});
|
||||
|
||||
$('.rule-trigger-list').each(function(i,v) {
|
||||
var obj = $(v);
|
||||
if(obj.data('count') > 2) {
|
||||
obj.hide();
|
||||
$('.rule-triggers-show[data-id="' + obj.data('id') + '"]').show();
|
||||
obj.addClass('hidden');
|
||||
$('.rule-triggers-show[data-id="' + obj.data('id') + '"]').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
// show rule actions
|
||||
$('.rule-actions-show').click(function (e) {
|
||||
var obj = $(e.currentTarget);
|
||||
$('.rule-action-list[data-id="' + obj.data('id') + '"]').show();
|
||||
$('.rule-actions-show[data-id="' + obj.data('id') + '"]').hide();
|
||||
$('.rule-action-list[data-id="' + obj.data('id') + '"]').removeClass('hidden');
|
||||
$('.rule-actions-show[data-id="' + obj.data('id') + '"]').addClass('hidden');
|
||||
});
|
||||
|
||||
$('.rule-action-list').each(function(i,v) {
|
||||
var obj = $(v);
|
||||
if(obj.data('count') > 1) {
|
||||
obj.hide();
|
||||
$('.rule-actions-show[data-id="' + obj.data('id') + '"]').show();
|
||||
obj.addClass('hidden');
|
||||
$('.rule-actions-show[data-id="' + obj.data('id') + '"]').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -194,9 +194,9 @@ function testRuleTriggers(e) {
|
||||
// Show warning if appropriate
|
||||
if (data.warning) {
|
||||
modal.find(".transaction-warning .warning-contents").text(data.warning);
|
||||
modal.find(".transaction-warning").show();
|
||||
modal.find(".transaction-warning").removeClass('hidden');
|
||||
} else {
|
||||
modal.find(".transaction-warning").hide();
|
||||
modal.find(".transaction-warning").addClass('hidden');
|
||||
}
|
||||
|
||||
// Show the modal dialog
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"font-awesome": "^4.7.0",
|
||||
"jquery": "^3",
|
||||
"laravel-mix": "^6.0",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss": "^8.5.14",
|
||||
"uiv": "^1.4",
|
||||
"vue": "^2.7",
|
||||
"vue-i18n": "^8",
|
||||
|
||||
@@ -442,11 +442,16 @@ export default {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
if (typeof error.response.data === 'object') {
|
||||
form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
for (const [key, value] of Object.entries(error.response.data.errors)) {
|
||||
console.log(`${key}: ${value}`);
|
||||
form.errors.push(value);
|
||||
}
|
||||
} else {
|
||||
form.errors = ['Something went wrong. Please try again.'];
|
||||
}
|
||||
console.log(form.errors);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ export default {
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
console.log('ready()');
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
@@ -220,6 +221,7 @@ export default {
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
console.log('mounted()');
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
@@ -228,6 +230,7 @@ export default {
|
||||
* Prepare the component.
|
||||
*/
|
||||
prepareComponent() {
|
||||
console.log('prepareComponent()');
|
||||
this.getTokens();
|
||||
// this.getScopes();
|
||||
|
||||
@@ -251,8 +254,10 @@ export default {
|
||||
* Get all of the personal access tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
console.log('getTokens()');
|
||||
axios.get('./oauth/personal-access-tokens')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
this.tokens = response.data;
|
||||
});
|
||||
},
|
||||
@@ -261,6 +266,7 @@ export default {
|
||||
* Get all the available scopes.
|
||||
*/
|
||||
getScopes() {
|
||||
console.log('getScopes()');
|
||||
axios.get('./oauth/scopes')
|
||||
.then(response => {
|
||||
this.scopes = response.data;
|
||||
@@ -271,6 +277,7 @@ export default {
|
||||
* Show the form for creating new tokens.
|
||||
*/
|
||||
showCreateTokenForm() {
|
||||
console.log('showCreateTokenForm()');
|
||||
$('#modal-create-token').modal('show');
|
||||
},
|
||||
|
||||
@@ -278,12 +285,14 @@ export default {
|
||||
* Create a new personal access token.
|
||||
*/
|
||||
store() {
|
||||
console.log('store()');
|
||||
this.accessToken = null;
|
||||
|
||||
this.form.errors = [];
|
||||
|
||||
axios.post('./oauth/personal-access-tokens', this.form)
|
||||
.then(response => {
|
||||
console.log('Successful POST new token, reset form content.');
|
||||
this.form.name = '';
|
||||
this.form.scopes = [];
|
||||
this.form.errors = [];
|
||||
@@ -293,6 +302,7 @@ export default {
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Bad POST new token, show error.');
|
||||
if (typeof error.response.data === 'object') {
|
||||
this.form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
} else {
|
||||
@@ -323,6 +333,7 @@ export default {
|
||||
* Show the given access token to the user.
|
||||
*/
|
||||
showAccessToken(accessToken) {
|
||||
console.log('showAccessToken');
|
||||
$('#modal-create-token').modal('hide');
|
||||
|
||||
this.accessToken = accessToken;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"firefly": {
|
||||
"explain_pats": "Personal Access Tokens are long lived (with a maximum of 1 year) keys that allow direct and unlimited access to your Firefly III data. Tools like the Firefly III Data Importer and the Firefly III integration in Home Assistant use such tokens to connect to Firefly III and do their thing. When you create a token, it is only visible once. The token is also very long.",
|
||||
"profile_oauth_clients_explain": "An OAuth client can be used to connect \"smart\" applications to Firefly III: applications that are capable of redirecting you to your Firefly III, get your permission, and return you back. The Firefly III Data Importer is such an application. OAuth clients can be generated with or without a \"secret\". This secret is used to authenticate the client. Since not all clients are capable of storing the secret, so you have the option to generate a client without one.",
|
||||
"regenerate_secret": "Regenerate secret",
|
||||
"explain_pats": "\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c\u662f\u4e00\u79cd\u957f\u671f\u6709\u6548\uff08\u6700\u957f1\u5e74\uff09\u7684\u4ee4\u724c\uff0c\u53ef\u8ba9\u60a8\u76f4\u63a5\u3001\u65e0\u9650\u5236\u5730\u8bbf\u95ee Firefly III \u6570\u636e\u3002\u8bf8\u5982 `Firefly III Data Importer` \u4ee5\u53ca Home Assistant \u4e2d\u7684 Firefly III \u96c6\u6210\u7b49\u5de5\u5177\uff0c\u6b63\u662f\u4f7f\u7528\u6b64\u7c7b\u4ee4\u724c\u8fde\u63a5\u81f3 Firefly III \u5e76\u5b8c\u6210\u5404\u81ea\u7684\u4efb\u52a1\u3002\u521b\u5efa\u4ee4\u724c\u65f6\uff0c\u5b83\u4ec5\u4f1a\u663e\u793a\u4e00\u6b21\u3002\u4ee4\u724c\u5b57\u7b26\u4e32\u901a\u5e38\u4f1a\u5f88\u957f\u3002",
|
||||
"profile_oauth_clients_explain": "OAuth\u5ba2\u6237\u7aef\u53ef\u7528\u4e8e\u5c06\u201c\u667a\u80fd\u201d\u5e94\u7528\u8fde\u63a5\u5230 Firefly III\uff1a\u5b83\u4eec\u80fd\u591f\u5c06\u60a8\u91cd\u5b9a\u5411\u5230\u60a8\u7684Firefly III\uff0c\u83b7\u53d6\u60a8\u7684\u6388\u6743\uff0c\u7136\u540e\u8fd4\u56de\u5e94\u7528\u3002`Firefly III Data Importer`\u5c31\u662f\u8fd9\u6837\u7684\u5e94\u7528\u3002\u751f\u6210OAuth\u5ba2\u6237\u7aef\u65f6\u53ef\u4ee5\u9009\u62e9\u662f\u5426\u9644\u5e26\u201c\u5bc6\u94a5\uff08secret\uff09\u201d\uff0c\u5b83\u7528\u4e8e\u9a8c\u8bc1\u5ba2\u6237\u7aef\u3002\u5e76\u975e\u6240\u6709\u5ba2\u6237\u7aef\u90fd\u80fd\u591f\u5b89\u5168\u5b58\u50a8\u5bc6\u94a5\uff0c\u6240\u4ee5\u60a8\u53ef\u4ee5\u9009\u62e9\u751f\u6210\u4e00\u4e2a\u4e0d\u5e26\u5bc6\u94a5\u7684\u5ba2\u6237\u7aef\u3002",
|
||||
"regenerate_secret": "\u91cd\u65b0\u751f\u6210\u5bc6\u94a5",
|
||||
"administrations_page_title": "\u8d22\u52a1\u7ba1\u7406",
|
||||
"administrations_index_menu": "\u8d22\u52a1\u7ba1\u7406",
|
||||
"expires_at": "\u8fc7\u671f\u4e8e",
|
||||
"temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.",
|
||||
"administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.",
|
||||
"temp_administrations_introduction": "Firefly III \u4e0d\u4e45\u5c06\u652f\u6301\u7ba1\u7406\u591a\u4e2a\u8d22\u52a1\u7ba1\u7406\u3002\u76ee\u524d\u60a8\u53ea\u6709\u4e00\u4e2a\u3002\u60a8\u53ef\u4ee5\u8bbe\u7f6e\u8fd9\u4e2a\u8d22\u52a1\u7ba1\u7406\u7684\u540d\u79f0\u53ca\u5176\u4e3b\u8981\u8d27\u5e01\u3002\u8fd9\u53d6\u4ee3\u4e86\u4e4b\u524d\u201c\u9ed8\u8ba4\u8d27\u5e01\u201d\u7684\u8bbe\u7f6e\u3002\u8be5\u8bbe\u7f6e\u73b0\u5728\u5df2\u4e0e\u8d22\u52a1\u7ba1\u7406\u6302\u94a9\uff0c\u6bcf\u4e2a\u8d22\u52a1\u7ba1\u7406\u53ef\u4ee5\u6709\u4e0d\u540c\u8bbe\u7f6e\u3002",
|
||||
"administration_currency_form_help": "\u5982\u679c\u60a8\u66f4\u6539\u4e3b\u8981\u8d27\u5e01\uff0c\u9875\u9762\u52a0\u8f7d\u53ef\u80fd\u9700\u8981\u5f88\u957f\u65f6\u95f4\uff0c\u56e0\u4e3a\u4ea4\u6613\u53ef\u80fd\u9700\u8981\u8f6c\u6362\u4e3a\u60a8\u7684(\u65b0)\u4e3b\u8981\u8d27\u5e01\u3002",
|
||||
"administrations_page_edit_sub_title_js": "\u7f16\u8f91\u8d22\u52a1\u7ba1\u7406{title}",
|
||||
"table": "\u8868\u683c",
|
||||
"welcome_back": "\u4eca\u5929\u7406\u8d22\u4e86\u5417\uff1f",
|
||||
@@ -75,8 +75,8 @@
|
||||
"profile_whoops": "\u5f88\u62b1\u6b49\uff01",
|
||||
"profile_something_wrong": "\u53d1\u751f\u9519\u8bef\uff01",
|
||||
"profile_try_again": "\u53d1\u751f\u9519\u8bef\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5\u3002",
|
||||
"profile_oauth_clients": "OAuth Clients and Applications",
|
||||
"profile_oauth_no_clients": "You have not created any OAuth clients or applications.",
|
||||
"profile_oauth_clients": "OAuth \u5ba2\u6237\u7aef\u548c\u5e94\u7528\u7a0b\u5e8f",
|
||||
"profile_oauth_no_clients": "\u60a8\u8fd8\u6ca1\u6709\u521b\u5efa\u4efb\u4f55 OAuth \u5ba2\u6237\u7aef\u6216\u5e94\u7528\u7a0b\u5e8f\u3002",
|
||||
"profile_oauth_clients_header": "\u5ba2\u6237\u7aef",
|
||||
"profile_oauth_client_id": "\u5ba2\u6237\u7aef ID",
|
||||
"profile_oauth_client_name": "\u540d\u79f0",
|
||||
@@ -86,7 +86,7 @@
|
||||
"profile_oauth_edit_client": "\u7f16\u8f91\u5ba2\u6237\u7aef",
|
||||
"profile_oauth_name_help": "\u60a8\u7684\u7528\u6237\u53ef\u4ee5\u8bc6\u522b\u5e76\u4fe1\u4efb\u7684\u4fe1\u606f",
|
||||
"profile_oauth_redirect_url": "\u8df3\u8f6c\u7f51\u5740",
|
||||
"profile_oauth_clients_external_auth": "Please note that if you're using an external authentication provider like Authelia, OAuth Clients will not work. You can use Personal Access Tokens only.",
|
||||
"profile_oauth_clients_external_auth": "\u8bf7\u6ce8\u610f\uff1a\u5982\u679c\u60a8\u6b63\u5728\u4f7f\u7528\u5982 Authelia \u7684\u5916\u90e8\u8eab\u4efd\u9a8c\u8bc1\u63d0\u4f9b\u5546\uff0cOAuth \u5ba2\u6237\u7aef\u5c06\u65e0\u6cd5\u4f7f\u7528\u3002\u60a8\u53ea\u80fd\u4f7f\u7528\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c\u3002",
|
||||
"profile_oauth_redirect_url_help": "\u60a8\u7684\u5e94\u7528\u7a0b\u5e8f\u7684\u6388\u6743\u56de\u8c03\u7f51\u5740",
|
||||
"profile_authorized_apps": "\u5df2\u6388\u6743\u5e94\u7528",
|
||||
"profile_authorized_clients": "\u5df2\u6388\u6743\u5ba2\u6237\u7aef",
|
||||
@@ -104,24 +104,24 @@
|
||||
"piggy_bank": "\u5b58\u94b1\u7f50",
|
||||
"profile_oauth_client_secret_title": "\u5ba2\u6237\u7aef\u5bc6\u94a5",
|
||||
"profile_oauth_client_secret_expl": "\u8bf7\u59a5\u5584\u4fdd\u5b58\u60a8\u7684\u65b0\u5ba2\u6237\u7aef\u7684\u5bc6\u94a5\uff0c\u6b64\u5bc6\u94a5\u4ec5\u4f1a\u5728\u8fd9\u91cc\u5c55\u793a\u4e00\u6b21\u3002\u60a8\u73b0\u5728\u5df2\u53ef\u4ee5\u4f7f\u7528\u6b64\u5bc6\u94a5\u8fdb\u884c API \u8bf7\u6c42\u3002",
|
||||
"profile_oauth_confidential": "Keep a secret?",
|
||||
"profile_oauth_confidential_help": "Can the application you're using this for keep a secret? The Firefly III Data Importer CANNOT keep a secret, so UNCHECK the box. In other cases, it's up to you.",
|
||||
"profile_oauth_confidential": "\u9644\u5e26\u5bc6\u94a5\uff1f",
|
||||
"profile_oauth_confidential_help": "\u60a8\u8981\u8fde\u63a5\u7684\u5e94\u7528\u80fd\u5426\u5b89\u5168\u4fdd\u5b58\u5bc6\u94a5\uff1f`Firefly III Data Importer`\u65e0\u6cd5\u4fdd\u5b58\u5bc6\u94a5\uff0c\u56e0\u6b64\u8bf7\u53d6\u6d88\u52fe\u9009\u6b64\u6846\u3002\u5176\u4ed6\u60c5\u51b5\u7531\u60a8\u81ea\u884c\u51b3\u5b9a\u3002",
|
||||
"multi_account_warning_unknown": "\u6839\u636e\u60a8\u521b\u5efa\u7684\u4ea4\u6613\u7c7b\u578b\uff0c\u540e\u7eed\u62c6\u5206\u7684\u6765\u6e90\u548c\/\u6216\u76ee\u6807\u8d26\u6237\u53ef\u80fd\u88ab\u4ea4\u6613\u7684\u9996\u7b14\u62c6\u5206\u7684\u914d\u7f6e\u6240\u8986\u76d6\u3002",
|
||||
"multi_account_warning_withdrawal": "\u8bf7\u6ce8\u610f\uff0c\u540e\u7eed\u62c6\u5206\u7684\u6765\u6e90\u8d26\u6237\u5c06\u4f1a\u88ab\u652f\u51fa\u7684\u9996\u7b14\u62c6\u5206\u7684\u914d\u7f6e\u6240\u8986\u76d6\u3002",
|
||||
"multi_account_warning_deposit": "\u8bf7\u6ce8\u610f\uff0c\u540e\u7eed\u62c6\u5206\u7684\u76ee\u6807\u8d26\u6237\u5c06\u4f1a\u88ab\u6536\u5165\u7684\u9996\u7b14\u62c6\u5206\u7684\u914d\u7f6e\u6240\u8986\u76d6\u3002",
|
||||
"multi_account_warning_transfer": "\u8bf7\u6ce8\u610f\uff0c\u540e\u7eed\u62c6\u5206\u7684\u6765\u6e90\u548c\u76ee\u6807\u8d26\u6237\u5c06\u4f1a\u88ab\u8f6c\u8d26\u7684\u9996\u7b14\u62c6\u5206\u7684\u914d\u7f6e\u6240\u8986\u76d6\u3002",
|
||||
"webhook_trigger_ANY": "After any event",
|
||||
"webhook_trigger_ANY": "\u5728\u4efb\u4f55\u4e8b\u4ef6\u4e4b\u540e",
|
||||
"webhook_trigger_STORE_TRANSACTION": "\u4ea4\u6613\u521b\u5efa\u540e",
|
||||
"webhook_trigger_UPDATE_TRANSACTION": "\u4ea4\u6613\u66f4\u65b0\u540e",
|
||||
"webhook_trigger_DESTROY_TRANSACTION": "\u4ea4\u6613\u5220\u9664\u540e",
|
||||
"webhook_trigger_STORE_BUDGET": "After budget creation",
|
||||
"webhook_trigger_UPDATE_BUDGET": "After budget update",
|
||||
"webhook_trigger_DESTROY_BUDGET": "After budget delete",
|
||||
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change",
|
||||
"webhook_trigger_STORE_BUDGET": "\u9884\u7b97\u521b\u5efa\u540e",
|
||||
"webhook_trigger_UPDATE_BUDGET": "\u9884\u7b97\u66f4\u65b0\u540e",
|
||||
"webhook_trigger_DESTROY_BUDGET": "\u9884\u7b97\u5220\u9664\u540e",
|
||||
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "\u9884\u7b97\u91d1\u989d\u53d8\u66f4\u540e",
|
||||
"webhook_response_TRANSACTIONS": "\u4ea4\u6613\u8be6\u60c5",
|
||||
"webhook_response_RELEVANT": "Relevant details",
|
||||
"webhook_response_RELEVANT": "\u76f8\u5173\u660e\u7ec6",
|
||||
"webhook_response_ACCOUNTS": "\u8d26\u6237\u8be6\u60c5",
|
||||
"webhook_response_NONE": "No details",
|
||||
"webhook_response_NONE": "\u65e0\u8be6\u7ec6\u4fe1\u606f",
|
||||
"webhook_delivery_JSON": "JSON",
|
||||
"actions": "\u64cd\u4f5c",
|
||||
"meta_data": "\u540e\u8bbe\u8d44\u6599",
|
||||
@@ -163,7 +163,7 @@
|
||||
"url": "\u7f51\u5740",
|
||||
"active": "\u542f\u7528",
|
||||
"interest_date": "\u5229\u606f\u65e5\u671f",
|
||||
"administration_currency": "Primary currency",
|
||||
"administration_currency": "\u4e3b\u8981\u8d27\u5e01",
|
||||
"title": "\u6807\u9898",
|
||||
"date": "\u65e5\u671f",
|
||||
"book_date": "\u767b\u8bb0\u65e5\u671f",
|
||||
@@ -183,7 +183,7 @@
|
||||
"list": {
|
||||
"title": "\u6807\u9898",
|
||||
"active": "\u662f\u5426\u542f\u7528\uff1f",
|
||||
"primary_currency": "Primary currency",
|
||||
"primary_currency": "\u4e3b\u8981\u8d27\u5e01",
|
||||
"trigger": "\u89e6\u53d1\u6761\u4ef6",
|
||||
"response": "\u7b54\u590d",
|
||||
"delivery": "\u4ea4\u4ed8",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"laravel-vite-plugin": "^3",
|
||||
"patch-package": "^8",
|
||||
"sass": "^1",
|
||||
"vite": "=8.0.8",
|
||||
"vite": "=8.0.11",
|
||||
"vite-plugin-manifest-sri": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1398,6 +1398,7 @@ return [
|
||||
'pref_locale' => 'Locale settings',
|
||||
'pref_languages_help' => 'Firefly III supports several languages. Which one do you prefer?',
|
||||
'pref_locale_help' => 'Firefly III allows you to set other local settings, like how currencies, numbers and dates are formatted. Entries in this list may not be supported by your system. Firefly III doesn\'t have the correct date settings for every locale; contact me for improvements.',
|
||||
'pref_locale_exception' => 'Firefly III can only change how you view dates. Entering a date will always be formatted according to your browser settings, not Firefly III settings. If your browser is set to "English (US)" you must always enter the date formatted as mm/dd/yyyy. No setting in Firefly III can change that.',
|
||||
'pref_locale_no_demo' => 'This feature won\'t work for the demo user.',
|
||||
'pref_convert_to_primary' => 'Display amounts in your primary currency',
|
||||
'pref_convert_to_primary_help' => 'This option will make Firefly III try to display and show your primary currency in as many places as possible, converting amounts where necessary. This sacrifices accuracy for ease of use, because conversion is not always exact. Please verify that Firefly III has the necessary conversion rates on the "exchange rates"-page.',
|
||||
@@ -1621,7 +1622,7 @@ return [
|
||||
'secure_pw_ff' => 'Do you use the same password all over the internet? If one site loses your password, hackers have access to all your data. Firefly III relies on you to choose a strong and unique password to protect your financial records.',
|
||||
'secure_pw_check_box' => 'To help you do that Firefly III can check if the password you want to use has been stolen in the past. If this is the case, Firefly III advises you NOT to use that password.',
|
||||
'secure_pw_working_title' => 'How does it work?',
|
||||
'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to <a href="https://www.troyhunt.com/introducing-306-million-freely-downloadable-pwned-passwords/">the website of Troy Hunt</a> to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest <a href="https://pages.nist.gov/800-63-3/sp800-63b.html">NIST Special Publication</a> on this subject.',
|
||||
'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to <a href="https://haveibeenpwned.com/API/v3#PwnedPasswords">the website of Troy Hunt</a> to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest <a href="https://pages.nist.gov/800-63-3/sp800-63b.html">NIST Special Publication</a> on this subject.',
|
||||
'secure_pw_should' => 'Should I check the box?',
|
||||
'secure_pw_long_password' => 'Yes. Always verify your password is safe.',
|
||||
'command_line_token' => 'Command line token',
|
||||
@@ -1813,6 +1814,13 @@ return [
|
||||
'options' => 'Options',
|
||||
|
||||
// budgets:
|
||||
'spent_this_period' => 'Spent on this budget',
|
||||
'spent_this_period_per_day' => 'Spent on this budget per day (:days day(s))',
|
||||
'spent_in_budget_limit_outside_period' => 'Spent on this budget, but NOT in this period',
|
||||
'spent_in_budget_limit_outside_period_per_day' => 'Spent on this budget, but NOT in this period per day (:days day(s))',
|
||||
'left_in_budget_limit_overview' => 'Left in this budget',
|
||||
'left_in_budget_limit_per_day' => 'Left in this budget per day (:days day(s))',
|
||||
'nothing_left_in_budget' => 'The budget is now empty',
|
||||
'daily_budgets' => 'Daily budgets',
|
||||
'weekly_budgets' => 'Weekly budgets',
|
||||
'monthly_budgets' => 'Monthly budgets',
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<input type="checkbox" id="verify_password" checked name="verify_password" value="1">
|
||||
<input type="checkbox" id="verify_password" name="verify_password" value="1">
|
||||
<label for="verify_password">
|
||||
{{ trans('form.verify_password') }}
|
||||
<a href="#"
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
{% for budgetLimit in budget.budgeted %}
|
||||
<span class="left_span" data-currency="{{ budgetLimit.currency_id }}" data-limit="{{ budgetLimit.id }}" data-value="{{ budgetLimit.left }}" class="amount_left">
|
||||
{# the amount left #}
|
||||
{{ formatAmountBySymbol(budgetLimit.left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}
|
||||
<span title="{{ 'left_in_budget_limit_overview'|_ }}">{{ formatAmountBySymbol(budgetLimit.left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}</span>
|
||||
|
||||
{# if the budget limit is in the past, this is not interesting. #}
|
||||
{# if there is nothing left, this is not interesting. #}
|
||||
{% if not budgetLimit.in_past and -1 == bccomp('0',budgetLimit.left) %}
|
||||
{% if 0 == budgetLimit.active_days_left %}
|
||||
({{ formatAmountBySymbol(budgetLimit.left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.left_in_budget_limit_per_day', {days: 0}) }}">({{ formatAmountBySymbol(budgetLimit.left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
{% else %}
|
||||
({{ formatAmountBySymbol(budgetLimit.left / budgetLimit.active_days_left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.left_in_budget_limit_per_day', {days: budgetLimit.active_days_left}) }}">({{ formatAmountBySymbol(budgetLimit.left / budgetLimit.active_days_left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# if there is nothing left, just format 0.00 #}
|
||||
{% if not budgetLimit.in_past and -1 != bccomp('0',budgetLimit.left) %}
|
||||
({{ formatAmountBySymbol('0', budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
<span title="{{ 'nothing_left_in_budget'|_ }}">({{ formatAmountBySymbol('0', budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})</span>
|
||||
{% endif %}
|
||||
</span><br />
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{# this is spent in budget limits: #}
|
||||
{% for budgetLimit in budget.budgeted %}
|
||||
{{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}
|
||||
<span title="{{ 'spent_this_period'|_ }}">{{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}</span>
|
||||
{% if 0 == budgetLimit.active_days_passed %}
|
||||
({{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.spent_this_period_per_day', {days: 0}) }}">({{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})</span>
|
||||
{% else %}
|
||||
({{ formatAmountBySymbol(budgetLimit.spent / budgetLimit.active_days_passed, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.spent_this_period_per_day', {days: budgetLimit.active_days_passed}) }}">({{ formatAmountBySymbol(budgetLimit.spent / budgetLimit.active_days_passed, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})</span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endfor %}
|
||||
@@ -12,11 +12,11 @@
|
||||
{# this is spent NOT in budget limits: #}
|
||||
{% for spent in budget.spent %}
|
||||
{% if 0 != bccomp('0', spent.spent_outside) %}
|
||||
{{ formatAmountBySymbol(spent.spent_outside, spent.currency_symbol, spent.currency_decimal_places) }}
|
||||
<span title="{{ 'spent_in_budget_limit_outside_period'|_ }}">{{ formatAmountBySymbol(spent.spent_outside, spent.currency_symbol, spent.currency_decimal_places) }}</span>
|
||||
{% if 0 == activeDaysPassed %}
|
||||
({{ formatAmountBySymbol(spent.spent_outside, spent.currency_symbol, spent.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.spent_in_budget_limit_outside_period_per_day', {days: 0}) }}">({{ formatAmountBySymbol(spent.spent_outside, spent.currency_symbol, spent.currency_decimal_places) }})</span>
|
||||
{% else %}
|
||||
({{ formatAmountBySymbol(spent.spent_outside / activeDaysPassed, spent.currency_symbol, spent.currency_decimal_places) }})
|
||||
<span title="{{ trans('firefly.spent_in_budget_limit_outside_period_per_day', {days: activeDaysPassed}) }}">({{ formatAmountBySymbol(spent.spent_outside / activeDaysPassed, spent.currency_symbol, spent.currency_decimal_places) }})</span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{# language #}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_languages'|_ }}</h3>
|
||||
<p class="text-info">{{ 'pref_languages_help'|_ }}</p>
|
||||
<p>{{ 'pref_languages_help'|_ }}</p>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<select class="form-control" id="lang_holder" name="language">
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<p class="text-info">
|
||||
<p>
|
||||
<br/>
|
||||
{{ 'pref_languages_locale'|_ }}
|
||||
</p>
|
||||
@@ -73,7 +73,8 @@
|
||||
{% if not isDocker %}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_locale'|_ }}</h3>
|
||||
<p class="text-info">{{ 'pref_locale_help'|_ }}</p>
|
||||
<p>{{ 'pref_locale_help'|_ }}</p>
|
||||
<p class="text-warning">{{ 'pref_locale_exception'|_ }}</p>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<select class="form-control" id="locale_holder" name="locale">
|
||||
@@ -104,7 +105,7 @@
|
||||
{# fiscal year #}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_custom_fiscal_year'|_ }}</h3>
|
||||
<p class="text-info">
|
||||
<p>
|
||||
{{ 'pref_custom_fiscal_year_help'|_ }}
|
||||
</p>
|
||||
{% set isCustomFiscalYear = customFiscalYear == 1 ? true : false %}
|
||||
@@ -116,7 +117,7 @@
|
||||
{% if fireflyiiiconfig('enable_exchange_rates', true) %}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_convert_to_primary'|_ }}</h3>
|
||||
<p class="text-info">
|
||||
<p>
|
||||
{{ 'pref_convert_to_primary_help'|_ }}
|
||||
</p>
|
||||
{{ ExpandedForm.checkbox('convertToPrimary','1',convertToPrimary,{ 'label' : 'pref_convert_primary_help'|_ }) }}
|
||||
@@ -125,7 +126,7 @@
|
||||
{# conversion back to primary currency #}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_anonymous'|_ }}</h3>
|
||||
<p class="text-info">
|
||||
<p>
|
||||
{{ 'pref_anonymous_help'|_ }}
|
||||
</p>
|
||||
{{ ExpandedForm.checkbox('anonymous','1',anonymous,{ 'label' : 'pref_anonymous_label'|_ }) }}
|
||||
@@ -138,7 +139,7 @@
|
||||
{# transaction preferences #}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_optional_fields_transaction'|_ }}</h3>
|
||||
<p class="text-info">
|
||||
<p>
|
||||
{{ 'pref_optional_fields_transaction_help'|_ }}
|
||||
</p>
|
||||
<h4>{{ 'optional_tj_date_fields'|_ }}</h4>
|
||||
@@ -169,7 +170,7 @@
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_home_screen_accounts'|_ }}</h3>
|
||||
<p class="text-info">{{ 'pref_home_screen_accounts_help'|_ }}</p>
|
||||
<p>{{ 'pref_home_screen_accounts_help'|_ }}</p>
|
||||
{% for type, accounts in groupedAccounts %}
|
||||
<strong>{{ type }}</strong>
|
||||
{% for id, name in accounts %}
|
||||
@@ -206,7 +207,7 @@
|
||||
{# view range #}
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'pref_view_range'|_ }}</h3>
|
||||
<p class="text-info">{{ 'pref_view_range_help'|_ }}</p>
|
||||
<p>{{ 'pref_view_range_help'|_ }}</p>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
@@ -311,12 +312,12 @@
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'list_page_size_title'|_ }}</h3>
|
||||
<p class="text-info">{{ 'list_page_size_help'|_ }}</p>
|
||||
<p>{{ 'list_page_size_help'|_ }}</p>
|
||||
{{ ExpandedForm.integer('listPageSize',listPageSize,{'label' : 'list_page_size_label'|_}) }}
|
||||
</div>
|
||||
<div class="preferences-box">
|
||||
<h3>{{ 'dark_mode_preference'|_ }}</h3>
|
||||
<p class="text-info">{{ 'dark_mode_preference_help'|_ }}</p>
|
||||
<p>{{ 'dark_mode_preference_help'|_ }}</p>
|
||||
{% for mode in availableDarkModes %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
|
||||
@@ -487,6 +487,9 @@
|
||||
{% if '' != link.foreign_amount %}
|
||||
({{ link.foreign_amount|raw }})
|
||||
{% endif %}
|
||||
{% if link.notes != "" %}
|
||||
({{ link.notes }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -387,6 +387,9 @@ Route::group(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
// exchange rates controller
|
||||
Route::group(
|
||||
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\ExchangeRates', 'prefix' => 'exchange-rates', 'as' => 'exchange-rates.'],
|
||||
|
||||
Reference in New Issue
Block a user