Compare commits

...

45 Commits

Author SHA1 Message Date
github-actions[bot]
cd0c342131 Merge pull request #11840 from firefly-iii/release-1772348895
🤖 Automatically merge the PR into the develop branch.
2026-03-01 08:08:24 +01:00
JC5
bb51baaa38 🤖 Auto commit for release 'develop' on 2026-03-01 2026-03-01 08:08:15 +01:00
James Cole
6bae8ab70a Fix date range issue. 2026-03-01 06:52:26 +01:00
James Cole
88030784a5 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-03-01 06:40:03 +01:00
James Cole
53b733fddb Merge pull request #11837 from dakennguyen/convert-primary-currency-transaction-charts
Convert to primary currency for transaction charts
2026-03-01 06:39:37 +01:00
James Cole
ca89159ccd Merge pull request #11836 from dakennguyen/convert-primary-currency-report-period-chart
Convert to primary currency for reportPeriodChart
2026-03-01 06:39:11 +01:00
James Cole
a5db3dd2e9 Merge pull request #11825 from mgrove36/develop
Fix account transaction type filtering
2026-03-01 06:37:29 +01:00
James Cole
b299465fb2 Merge branch 'develop' into develop
Signed-off-by: James Cole <james@firefly-iii.org>
2026-03-01 06:37:15 +01:00
James Cole
12a877d489 Fix #11812 2026-03-01 06:35:39 +01:00
Khoa Nguyen
7f64251a55 Convert to primary currency for transaction charts 2026-02-28 15:43:58 +01:00
Khoa Nguyen
c06d8263d8 Convert to primary currency for reportPeriodChart 2026-02-28 14:55:00 +01:00
James Cole
5451328ea6 Merge pull request #11835 from dakennguyen/convert-primary-currency-tag-report
Convert to primary currency for tag charts
2026-02-28 13:25:02 +01:00
Khoa Nguyen
3e5ef0b431 Convert to primary currency for tag charts 2026-02-28 13:19:58 +01:00
James Cole
87fb1fcc92 Merge pull request #11833 from dakennguyen/convert-primary-currency
Convert to primary currency for category charts
2026-02-28 12:36:13 +01:00
Khoa Nguyen
85da46243b Convert to primary currency for category charts 2026-02-28 11:04:53 +01:00
github-actions[bot]
c44711e9bd Merge pull request #11831 from firefly-iii/release-1772261367
🤖 Automatically merge the PR into the develop branch.
2026-02-28 07:49:34 +01:00
JC5
18161450e4 🤖 Auto commit for release 'develop' on 2026-02-28 2026-02-28 07:49:27 +01:00
James Cole
b48b2a411a Remove amount when nothing left. 2026-02-28 07:45:26 +01:00
github-actions[bot]
453332eae0 Merge pull request #11830 from firefly-iii/release-1772260516
🤖 Automatically merge the PR into the develop branch.
2026-02-28 07:35:25 +01:00
JC5
4407456167 🤖 Auto commit for release 'develop' on 2026-02-28 2026-02-28 07:35:16 +01:00
James Cole
2842432204 Clean up budget amounts. 2026-02-28 07:29:44 +01:00
James Cole
842ec6da47 Add new function to twig. 2026-02-28 07:20:22 +01:00
James Cole
ceb5873ba7 Do not break arrays 2026-02-28 07:17:53 +01:00
James Cole
6cfd8273fe Fix middleware for admin area. 2026-02-28 06:15:28 +01:00
github-actions[bot]
bd14273201 Merge pull request #11828 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2026-02-27 21:55:42 +01:00
github-actions[bot]
2a6ba8f00f Merge pull request #11827 from firefly-iii/release-1772225731
🤖 Automatically merge the PR into the develop branch.
2026-02-27 21:55:38 +01:00
JC5
5986137bb0 🤖 Auto commit for release 'v6.5.1' on 2026-02-27 2026-02-27 21:55:31 +01:00
github-actions[bot]
1f5962cfbc Merge pull request #11826 from firefly-iii/release-1772225098
🤖 Automatically merge the PR into the develop branch.
2026-02-27 21:45:08 +01:00
JC5
f40cb96906 🤖 Auto commit for release 'develop' on 2026-02-27 2026-02-27 21:44:58 +01:00
James Cole
92b1079eb1 Expand changelog. 2026-02-27 21:39:35 +01:00
James Cole
555060ead9 Fix URL, thanks @fabienfitoussi 2026-02-27 21:33:27 +01:00
James Cole
d88b728073 Expand middleware. 2026-02-27 21:29:49 +01:00
James Cole
5255a17b38 Fix an issue using an intentionally vague description. 2026-02-27 21:29:38 +01:00
James Cole
dad596dc26 Fix #11750 2026-02-27 21:27:18 +01:00
James Cole
b357a5d5ea Fix null pointer in available budget calculation. 2026-02-27 21:25:33 +01:00
Matthew Grove
0c0736d336 Fix account transaction type filtering
Address issue #11822
2026-02-27 13:44:21 +00:00
James Cole
ae6adf90ee Fix https://github.com/firefly-iii/firefly-iii/issues/11817 2026-02-26 20:47:10 +01:00
James Cole
4f0b1c9914 Merge branch 'main' into develop 2026-02-25 06:51:21 +01:00
James Cole
dc718d6b89 Merge pull request #11808 from CinnamonPyro/feature/add-currency-THB
Add Thai baht to Currency Seeder
2026-02-25 06:29:50 +01:00
Cinnamon Pyro
5252ceeaee Add Thai baht to Currency Seeder
Signed-off-by: Cinnamon Pyro <69516214+CinnamonPyro@users.noreply.github.com>
2026-02-25 11:11:25 +07:00
James Cole
56a1eb6515 Fix echo statement for Data importer version
Signed-off-by: James Cole <james@firefly-iii.org>
2026-02-24 08:07:45 +01:00
github-actions[bot]
62ef3966ed Merge pull request #11799 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2026-02-23 20:18:47 +01:00
github-actions[bot]
241d8fd921 Merge pull request #11798 from firefly-iii/release-1771874314
🤖 Automatically merge the PR into the develop branch.
2026-02-23 20:18:41 +01:00
JC5
9028898232 🤖 Auto commit for release 'v6.5.0' on 2026-02-23 2026-02-23 20:18:34 +01:00
James Cole
bf12a5631d Expand changelog. 2026-02-23 20:13:47 +01:00
41 changed files with 844 additions and 662 deletions

View File

@@ -1264,16 +1264,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
"reference": "488285876e807a4777f074041d8bb508623419fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"url": "https://api.github.com/repos/symfony/console/zipball/488285876e807a4777f074041d8bb508623419fa",
"reference": "488285876e807a4777f074041d8bb508623419fa",
"shasum": ""
},
"require": {
@@ -1330,7 +1330,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.4"
"source": "https://github.com/symfony/console/tree/v8.0.6"
},
"funding": [
{
@@ -1350,7 +1350,7 @@
"type": "tidelift"
}
],
"time": "2026-01-13T13:06:50+00:00"
"time": "2026-02-25T16:59:43+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1582,16 +1582,16 @@
},
{
"name": "symfony/filesystem",
"version": "v8.0.1",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d"
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
"shasum": ""
},
"require": {
@@ -1628,7 +1628,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v8.0.1"
"source": "https://github.com/symfony/filesystem/tree/v8.0.6"
},
"funding": [
{
@@ -1648,20 +1648,20 @@
"type": "tidelift"
}
],
"time": "2025-12-01T09:13:36+00:00"
"time": "2026-02-25T16:59:43+00:00"
},
{
"name": "symfony/finder",
"version": "v8.0.5",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"shasum": ""
},
"require": {
@@ -1696,7 +1696,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.5"
"source": "https://github.com/symfony/finder/tree/v8.0.6"
},
"funding": [
{
@@ -1716,7 +1716,7 @@
"type": "tidelift"
}
],
"time": "2026-01-26T15:08:38+00:00"
"time": "2026-01-29T09:41:02+00:00"
},
{
"name": "symfony/options-resolver",
@@ -2588,16 +2588,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "758b372d6882506821ed666032e43020c4f57194"
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -2654,7 +2654,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.4"
"source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -2674,7 +2674,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-02-09T10:14:57+00:00"
}
],
"packages-dev": [],

View File

@@ -41,7 +41,7 @@ jobs:
DDNOV="${DDNOV:1}"
echo "Firefly III version is ${{ steps.ff3version.outputs.release }}, without v is $FFNOV"
echo "Data importer version is ${{ steps.ff3version.outputs.release }}, without v is $FFNOV"
echo "Data importer version is ${{ steps.ff3version.outputs.release }}, without v is $DDNOV"
# user includes no debug info at all, and does not mention current version.
# user includes no debug info at all, but does mention current version

View File

@@ -4,6 +4,8 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2026
- Matthew Grove
- Cinnamon Pyro
- R1DEN
- RiDEN
- Khoa Nguyen

View File

@@ -25,7 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Account;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Generic\PaginationDateRangeRequest;
use FireflyIII\Api\V1\Requests\Models\Transaction\ListRequest;
use FireflyIII\Api\V1\Requests\PaginationRequest;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
@@ -126,17 +126,18 @@ class ListController extends Controller
/**
* Show all transaction groups related to the account.
*/
public function transactions(PaginationDateRangeRequest $request, Account $account): JsonResponse
public function transactions(ListRequest $request, Account $account): JsonResponse
{
['limit' => $limit, 'page' => $page, 'start' => $start, 'end' => $end, 'types' => $types] = $request->attributes->all();
$manager = $this->getManager();
['limit' => $limit, 'page' => $page, 'start' => $start, 'end' => $end, 'type' => $type] = $request->attributes->all();
$types = $this->mapTransactionTypes($type ?? 'default');
$manager = $this->getManager();
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUser($admin)->setAccounts(new Collection()->push($account))->withAPIInformation()->setLimit($limit)->setPage($page)->setTypes($types);
if (null !== $start) {
$collector->setStart($start);
@@ -145,18 +146,18 @@ class ListController extends Controller
$collector->setEnd($end);
}
$paginator = $collector->getPaginatedGroups();
$paginator = $collector->getPaginatedGroups();
$paginator->setPath(route('api.v1.accounts.transactions', [$account->id]).$this->buildParams());
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$transactions = $enrichment->enrich($paginator->getCollection());
$transactions = $enrichment->enrich($paginator->getCollection());
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -74,8 +74,8 @@ class UpdateController extends Controller
*/
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
Log::debug('Now in update routine for transaction group');
$data = $request->getAll();
Log::debug('Now in update routine for transaction group', $data);
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
@@ -92,6 +92,7 @@ class UpdateController extends Controller
$flags->applyRules = $applyRules;
$flags->fireWebhooks = $fireWebhooks;
$flags->recalculateCredit = $runRecalculations;
$flags->batchSubmission = $data['batch_submission'] ?? false;
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());

View File

@@ -31,6 +31,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class BatchController extends Controller
{
@@ -52,19 +53,25 @@ class BatchController extends Controller
public function finishBatch(Request $request): JsonResponse
{
Log::debug('Now in finishBatch.');
$journals = $this->repository->getUncompletedJournals();
if (0 === count($journals)) {
Log::debug('Counted zero journals, return.');
return response()->json([], 204);
}
Log::debug(sprintf('Counted %d journals.', count($journals)));
/** @var TransactionJournal $first */
$first = $journals->first();
$group = $first?->transactionGroup;
if (null === $group) {
Log::debug('First group is NULL.');
return response()->json([], 204);
}
$flags = new TransactionGroupEventFlags();
$flags->applyRules = 'true' === $request->get('apply_rules');
$flags->applyRules = 'true' === $request->input('apply_rules');
event(new UserRequestedBatchProcessing($flags));
// event(new CreatedSingleTransactionGroup($group, $flags));

View File

@@ -603,7 +603,7 @@ class GroupCollector implements GroupCollectorInterface
public function setSearchWords(array $array): GroupCollectorInterface
{
if (0 === count($array)) {
Log::debug('No words in array');
// Log::debug('No words in array');
return $this;
}

View File

@@ -46,6 +46,9 @@ use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Spatie\Period\Boundaries;
use Spatie\Period\Period;
use Spatie\Period\Precision;
/**
* Class BudgetLimitController
@@ -268,10 +271,16 @@ class BudgetLimitController extends Controller
$budgetLimit->transactionCurrency
);
$daysLeft = $this->activeDaysLeft($limit->start_date, $limit->end_date);
$limitPeriod = Period::make($limit->start_date, $limit->end_date, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$inPast = $limitPeriod->startsBefore(now()) && $limitPeriod->endsBefore(now());
// create aray.
$array['spent'] = $spentArr[$budgetLimit->transactionCurrency->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']);
$array['days_left'] = (string) $daysLeft;
$array['in_past'] = $inPast;
$array['left_per_day'] = 0 === $daysLeft
? bcadd((string) $array['spent'], (string) $array['amount'])
: bcdiv(bcadd((string) $array['spent'], (string) $array['amount']), $array['days_left']);

View File

@@ -47,6 +47,9 @@ use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Spatie\Period\Boundaries;
use Spatie\Period\Period;
use Spatie\Period\Precision;
/**
* Class IndexController
@@ -117,6 +120,10 @@ final class IndexController extends Controller
$prevLoop = $this->getPreviousPeriods($start, $range);
$nextLoop = $this->getNextPeriods($start, $range);
// number of days for consistent budgeting.
$activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
$activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.
// get all available budgets:
$availableBudgets = $this->getAllAvailableBudgets($start, $end);
// get all active budgets:
@@ -133,9 +140,6 @@ final class IndexController extends Controller
$spent = $spentArr[$this->primaryCurrency->id]['sum'] ?? '0';
unset($spentArr);
}
// number of days for consistent budgeting.
$activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
$activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.
// get all inactive budgets, and simply list them:
$inactive = $this->repository->getInactiveBudgets();
@@ -221,44 +225,68 @@ final class IndexController extends Controller
// complement budget with budget limits in range, and expenses in currency X in range.
/** @var Budget $current */
foreach ($collection as $current) {
Log::debug(sprintf('Working on budget #%d ("%s")', $current->id, $current->name));
$array = $current->toArray();
foreach ($collection as $budget) {
Log::debug(sprintf('Working on budget #%d ("%s")', $budget->id, $budget->name));
$array = $budget->toArray();
$array['spent'] = [];
$array['spent_total'] = [];
$array['budgeted'] = [];
$array['attachments'] = $this->repository->getAttachments($current);
$array['auto_budget'] = $this->repository->getAutoBudget($current);
$budgetLimits = $this->blRepository->getBudgetLimits($current, $start, $end);
$array['attachments'] = $this->repository->getAttachments($budget);
$array['auto_budget'] = $this->repository->getAutoBudget($budget);
$budgetLimits = $this->blRepository->getBudgetLimits($budget, $start, $end);
$spentInLimits = [];
/** @var BudgetLimit $limit */
foreach ($budgetLimits as $limit) {
Log::debug(sprintf('Working on budget limit #%d', $limit->id));
$currency = $limit->transactionCurrency ?? $primaryCurrency;
$amount = Steam::bcround($limit->amount, $currency->decimal_places);
$array['budgeted'][] = [
// number of days for consistent budgeting.
$activeDaysPassed = $this->activeDaysPassed($limit->start_date, $limit->end_date); // see method description.
$activeDaysLeft = $this->activeDaysLeft($limit->start_date, $limit->end_date); // see method description.
$limitPeriod = Period::make($limit->start_date, $limit->end_date, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$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);
$spentAmount = $spent[$currency->id]['sum'] ?? '0';
$array['budgeted'][] = [
'id' => $limit->id,
'amount' => $amount,
'notes' => $this->blRepository->getNoteText($limit),
'start_date' => $limit->start_date->isoFormat($this->monthAndDayFormat),
'end_date' => $limit->end_date->isoFormat($this->monthAndDayFormat),
'in_range' => $limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end),
'in_past' => $inPast,
'total_days' => $limit->start_date->diffInDays($limit->end_date) + 1,
'currency_id' => $currency->id,
'currency_symbol' => $currency->symbol,
'currency_name' => $currency->name,
'currency_decimal_places' => $currency->decimal_places,
'spent' => $spentAmount,
'left' => bcadd($amount, $spentAmount),
'active_days_passed' => $activeDaysPassed,
'active_days_left' => $activeDaysLeft,
];
$spentInLimits[$currency->id] = array_key_exists($currency->id, $spentInLimits)
? bcadd($spentInLimits[$currency->id], $spentAmount)
: $spentAmount;
Log::debug(sprintf('The amount budgeted for budget limit #%d is %s %s', $limit->id, $currency->code, $amount));
Log::debug(sprintf('spentInLimits[%s] is now %s', $currency->code, $spentInLimits[$currency->id]));
}
// #10463
Log::debug('Looping currencies');
/** @var TransactionCurrency $currency */
foreach ($currencies as $currency) {
$spentArr = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($current), $currency);
$spentInLimits[$currency->id] = array_key_exists($currency->id, $spentInLimits) ? $spentInLimits[$currency->id] : '0';
$spentArr = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($budget), $currency);
Log::debug(sprintf('Working on currency %s, spentInLimits is %s', $currency->code, $spentInLimits[$currency->id]));
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]['currency_id'] = $currency->id;
$array['spent'][$currency->id]['currency_symbol'] = $currency->symbol;
$array['spent'][$currency->id]['currency_decimal_places'] = $currency->decimal_places;

View File

@@ -34,10 +34,10 @@ use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Chart\Category\FrontpageChartGenerator;
use FireflyIII\Support\Chart\Category\WholePeriodChartGenerator;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\AugumentData;
use FireflyIII\Support\Http\Controllers\ChartGeneration;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use FireflyIII\Support\Http\Controllers\ResolvesJournalAmountAndCurrency;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Psr\Container\ContainerExceptionInterface;
@@ -51,6 +51,7 @@ class CategoryController extends Controller
use AugumentData;
use ChartGeneration;
use DateCalculation;
use ResolvesJournalAmountAndCurrency;
protected GeneratorInterface $generator;
@@ -267,7 +268,8 @@ class CategoryController extends Controller
// loop income and expenses for this category.:
$outSet = $expenses[$currencyId]['categories'][$categoryId] ?? ['transaction_journals' => []];
foreach ($outSet['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currencyInfo);
$amount = $journalData['amount'];
$date = $journal['date']->isoFormat($format);
$chartData[$outKey]['entries'][$date] ??= '0';
@@ -276,7 +278,8 @@ class CategoryController extends Controller
$inSet = $income[$currencyId]['categories'][$categoryId] ?? ['transaction_journals' => []];
foreach ($inSet['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currencyInfo);
$amount = $journalData['amount'];
$date = $journal['date']->isoFormat($format);
$chartData[$inKey]['entries'][$date] ??= '0';
$chartData[$inKey]['entries'][$date] = bcadd($amount, $chartData[$inKey]['entries'][$date]);

View File

@@ -29,8 +29,8 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\Category\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\AugumentData;
use FireflyIII\Support\Http\Controllers\ResolvesJournalAmountAndCurrency;
use FireflyIII\Support\Http\Controllers\TransactionCalculation;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
@@ -43,6 +43,7 @@ use Illuminate\Support\Collection;
class CategoryReportController extends Controller
{
use AugumentData;
use ResolvesJournalAmountAndCurrency;
use TransactionCalculation;
private GeneratorInterface $generator;
@@ -73,15 +74,15 @@ class CategoryReportController extends Controller
/** @var array $category */
foreach ($currency['categories'] as $category) {
foreach ($category['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['budget_name'] ?? trans('firefly.no_budget');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -100,15 +101,15 @@ class CategoryReportController extends Controller
foreach ($spent as $currency) {
/** @var array $category */
foreach ($currency['categories'] as $category) {
$title = sprintf('%s (%s)', $category['name'], $currency['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
];
foreach ($category['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$title = sprintf('%s (%s)', $category['name'], $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -127,15 +128,15 @@ class CategoryReportController extends Controller
foreach ($earned as $currency) {
/** @var array $category */
foreach ($currency['categories'] as $category) {
$title = sprintf('%s (%s)', $category['name'], $currency['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
];
foreach ($category['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$title = sprintf('%s (%s)', $category['name'], $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -155,15 +156,15 @@ class CategoryReportController extends Controller
/** @var array $category */
foreach ($currency['categories'] as $category) {
foreach ($category['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['destination_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -183,15 +184,15 @@ class CategoryReportController extends Controller
/** @var array $category */
foreach ($currency['categories'] as $category) {
foreach ($category['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['destination_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -210,26 +211,25 @@ class CategoryReportController extends Controller
// loop expenses.
foreach ($spent as $currency) {
// add things to chart Data for each currency:
$spentKey = sprintf('%d-spent', $currency['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.spent_in_specific_category', ['category' => $category->name]),
$currency['currency_name']
),
'type' => 'bar',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_id' => $currency['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
foreach ($currency['categories'] as $currentCategory) {
foreach ($currentCategory['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$spentKey = sprintf('%d-spent', $journalData['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.spent_in_specific_category', ['category' => $category->name]),
$journalData['currency_name']
),
'type' => 'bar',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
'currency_id' => $journalData['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
$key = $journal['date']->isoFormat($format);
$amount = Steam::positive($journal['amount']);
$chartData[$spentKey]['entries'][$key] ??= '0';
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount);
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $journalData['amount']);
}
}
}
@@ -237,26 +237,25 @@ class CategoryReportController extends Controller
// loop income.
foreach ($earned as $currency) {
// add things to chart Data for each currency:
$spentKey = sprintf('%d-earned', $currency['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.earned_in_specific_category', ['category' => $category->name]),
$currency['currency_name']
),
'type' => 'bar',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_id' => $currency['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
foreach ($currency['categories'] as $currentCategory) {
foreach ($currentCategory['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$spentKey = sprintf('%d-earned', $journalData['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.earned_in_specific_category', ['category' => $category->name]),
$journalData['currency_name']
),
'type' => 'bar',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
'currency_id' => $journalData['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
$key = $journal['date']->isoFormat($format);
$amount = Steam::positive($journal['amount']);
$chartData[$spentKey]['entries'][$key] ??= '0';
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount);
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $journalData['amount']);
}
}
}
@@ -276,15 +275,15 @@ class CategoryReportController extends Controller
/** @var array $category */
foreach ($currency['categories'] as $category) {
foreach ($category['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['source_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -304,15 +303,15 @@ class CategoryReportController extends Controller
/** @var array $category */
foreach ($currency['categories'] as $category) {
foreach ($category['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['source_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}

View File

@@ -36,6 +36,7 @@ use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\BasicDataSupport;
use FireflyIII\Support\Http\Controllers\ChartGeneration;
use FireflyIII\Support\Http\Controllers\ResolvesJournalAmountAndCurrency;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -47,6 +48,7 @@ class ReportController extends Controller
{
use BasicDataSupport;
use ChartGeneration;
use ResolvesJournalAmountAndCurrency;
protected GeneratorInterface $generator;
@@ -149,7 +151,7 @@ class ReportController extends Controller
$cache->addProperty($end);
$cache->addProperty($this->convertToPrimary);
if ($cache->has()) {
return response()->json($cache->get());
// return response()->json($cache->get());
}
Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
@@ -177,21 +179,13 @@ class ReportController extends Controller
/** @var array $journal */
foreach ($journals as $journal) {
$period = $journal['date']->format($format);
$currencyId = (int) $journal['currency_id'];
$currencySymbol = (string) $journal['currency_symbol'];
$currencyCode = (string) $journal['currency_code'];
$currencyName = (string) $journal['currency_name'];
$currencyDecimalPlaces = (int) $journal['currency_decimal_places'];
$amount = (string) $journal['amount'];
if ($this->convertToPrimary && null !== $this->primaryCurrency && $journal['currency_id'] !== $this->primaryCurrency->id) {
$currencyId = $this->primaryCurrency->id;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyCode = $this->primaryCurrency->code;
$currencyName = $this->primaryCurrency->name;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
$amount = $journal['foreign_currency_id'] === $this->primaryCurrency->id ? $journal['foreign_amount'] : $journal['pc_amount'];
}
$journalData = $this->resolveJournalAmountAndCurrency($journal, $journal);
$currencyId = $journalData['currency_id'];
$currencySymbol = $journalData['currency_symbol'];
$currencyCode = $journalData['currency_code'];
$currencyName = $journalData['currency_name'];
$currencyDecimalPlaces = $journalData['currency_decimal_places'];
$amount = $journalData['amount'];
$data[$currencyId] ??= [
'currency_id' => $currencyId,
@@ -203,7 +197,6 @@ class ReportController extends Controller
$data[$currencyId][$period] ??= ['period' => $period, 'spent' => '0', 'earned' => '0'];
// in our outgoing?
$key = 'spent';
$amount = Steam::positive($amount);
// deposit = incoming
// transfer or reconcile or opening balance, and these accounts are the destination.
@@ -226,7 +219,7 @@ class ReportController extends Controller
/** @var array $currency */
foreach ($data as $currency) {
Log::debug(sprintf('Now processing currency "%s"', $currency['currency_name']));
Log::debug(sprintf('Now processing currency %s', $currency['currency_code']));
$income = [
'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]),
'type' => 'bar',
@@ -254,7 +247,11 @@ class ReportController extends Controller
if ('1Y' === $preferredRange) {
$currentEnd = Navigation::endOfPeriod($currentEnd, $preferredRange);
}
Log::debug('Start of sub-loop');
// 2026-03-01 similar fix for monthly ranges.
if ('1M' === $preferredRange) {
$currentEnd = Navigation::endOfPeriod($currentEnd, $preferredRange);
}
Log::debug(sprintf('Start of sub-loop, current end is %s', $currentEnd->toW3cString()));
while ($currentStart <= $currentEnd) {
Log::debug(sprintf('Current start: %s', $currentStart->toW3cString()));
$key = $currentStart->format($format);

View File

@@ -29,9 +29,8 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Tag\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\AugumentData;
use FireflyIII\Support\Http\Controllers\TransactionCalculation;
use FireflyIII\Support\Http\Controllers\ResolvesJournalAmountAndCurrency;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
@@ -41,13 +40,11 @@ use Illuminate\Support\Collection;
class TagReportController extends Controller
{
use AugumentData;
use TransactionCalculation;
use ResolvesJournalAmountAndCurrency;
/** @var GeneratorInterface Chart generation methods. */
protected $generator;
private GeneratorInterface $generator;
/** @var OperationsRepositoryInterface */
private $opsRepository;
private OperationsRepositoryInterface $opsRepository;
/**
* TagReportController constructor.
@@ -55,10 +52,8 @@ class TagReportController extends Controller
public function __construct()
{
parent::__construct();
// create chart generator:
$this->generator = app(GeneratorInterface::class);
$this->middleware(function ($request, $next) {
$this->generator = app(GeneratorInterface::class);
$this->opsRepository = app(OperationsRepositoryInterface::class);
return $next($request);
@@ -75,15 +70,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['budget_name'] ?? trans('firefly.no_budget');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -103,15 +98,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['category_name'] ?? trans('firefly.no_category');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -131,15 +126,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['category_name'] ?? trans('firefly.no_category');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -159,15 +154,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['destination_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -187,15 +182,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['destination_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -218,22 +213,25 @@ class TagReportController extends Controller
// loop expenses.
foreach ($spent as $currency) {
// add things to chart Data for each currency:
$spentKey = sprintf('%d-spent', $currency['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf('%s (%s)', (string) trans('firefly.spent_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name']),
'type' => 'bar',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_id' => $currency['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
foreach ($currency['tags'] as $currentTag) {
foreach ($currentTag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$spentKey = sprintf('%d-spent', $journalData['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.spent_in_specific_tag', ['tag' => $tag->tag]),
$journalData['currency_name']
),
'type' => 'bar',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
'currency_id' => $journalData['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
$key = $journal['date']->isoFormat($format);
$amount = Steam::positive($journal['amount']);
$chartData[$spentKey]['entries'][$key] ??= '0';
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount);
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $journalData['amount']);
}
}
}
@@ -241,22 +239,25 @@ class TagReportController extends Controller
// loop income.
foreach ($earned as $currency) {
// add things to chart Data for each currency:
$spentKey = sprintf('%d-earned', $currency['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf('%s (%s)', (string) trans('firefly.earned_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name']),
'type' => 'bar',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_id' => $currency['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
foreach ($currency['tags'] as $currentTag) {
foreach ($currentTag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$spentKey = sprintf('%d-earned', $journalData['currency_id']);
$chartData[$spentKey] ??= [
'label' => sprintf(
'%s (%s)',
(string) trans('firefly.earned_in_specific_tag', ['tag' => $tag->tag]),
$journalData['currency_name']
),
'type' => 'bar',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
'currency_id' => $journalData['currency_id'],
'entries' => $this->makeEntries($start, $end),
];
$key = $journal['date']->isoFormat($format);
$amount = Steam::positive($journal['amount']);
$chartData[$spentKey]['entries'][$key] ??= '0';
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount);
$chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $journalData['amount']);
}
}
}
@@ -276,15 +277,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['source_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -304,15 +305,15 @@ class TagReportController extends Controller
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$objectName = $journal['source_account_name'] ?? trans('firefly.empty');
$title = sprintf('%s (%s)', $objectName, $currency['currency_name']);
$title = sprintf('%s (%s)', $objectName, $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -331,15 +332,15 @@ class TagReportController extends Controller
foreach ($spent as $currency) {
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
$title = sprintf('%s (%s)', $tag['name'], $currency['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
];
foreach ($tag['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$title = sprintf('%s (%s)', $tag['name'], $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}
@@ -357,15 +358,15 @@ class TagReportController extends Controller
foreach ($earned as $currency) {
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
$title = sprintf('%s (%s)', $tag['name'], $currency['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'],
];
foreach ($tag['transaction_journals'] as $journal) {
$amount = Steam::positive($journal['amount']);
$result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
$journalData = $this->resolveJournalAmountAndCurrency($journal, $currency);
$title = sprintf('%s (%s)', $tag['name'], $journalData['currency_name']);
$result[$title] ??= [
'amount' => '0',
'currency_symbol' => $journalData['currency_symbol'],
'currency_code' => $journalData['currency_code'],
];
$result[$title]['amount'] = bcadd($result[$title]['amount'], $journalData['amount']);
}
}
}

View File

@@ -30,6 +30,7 @@ use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Http\Controllers\ResolvesJournalAmountAndCurrency;
use Illuminate\Http\JsonResponse;
/**
@@ -37,6 +38,8 @@ use Illuminate\Http\JsonResponse;
*/
class TransactionController extends Controller
{
use ResolvesJournalAmountAndCurrency;
/** @var GeneratorInterface Chart generation methods. */
protected $generator;
@@ -57,6 +60,7 @@ class TransactionController extends Controller
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('chart.transactions.budgets');
if ($cache->has()) {
return response()->json($cache->get());
@@ -74,14 +78,15 @@ class TransactionController extends Controller
// group by category.
/** @var array $journal */
foreach ($result as $journal) {
$resolved = $this->resolveJournalAmountAndCurrency($journal);
$budget = $journal['budget_name'] ?? (string) trans('firefly.no_budget');
$title = sprintf('%s (%s)', $budget, $journal['currency_symbol']);
$title = sprintf('%s (%s)', $budget, $resolved['currency_symbol']);
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_symbol' => $resolved['currency_symbol'],
'currency_code' => $resolved['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
$data[$title]['amount'] = bcadd($data[$title]['amount'], $resolved['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
$cache->store($chart);
@@ -98,6 +103,7 @@ class TransactionController extends Controller
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($objectType);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('chart.transactions.categories');
if ($cache->has()) {
return response()->json($cache->get());
@@ -124,14 +130,15 @@ class TransactionController extends Controller
// group by category.
/** @var array $journal */
foreach ($result as $journal) {
$resolved = $this->resolveJournalAmountAndCurrency($journal);
$category = $journal['category_name'] ?? (string) trans('firefly.no_category');
$title = sprintf('%s (%s)', $category, $journal['currency_symbol']);
$title = sprintf('%s (%s)', $category, $resolved['currency_symbol']);
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_symbol' => $resolved['currency_symbol'],
'currency_code' => $resolved['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
$data[$title]['amount'] = bcadd($data[$title]['amount'], $resolved['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
$cache->store($chart);
@@ -148,6 +155,7 @@ class TransactionController extends Controller
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($objectType);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('chart.transactions.destinations');
if ($cache->has()) {
return response()->json($cache->get());
@@ -174,14 +182,15 @@ class TransactionController extends Controller
// group by category.
/** @var array $journal */
foreach ($result as $journal) {
$resolved = $this->resolveJournalAmountAndCurrency($journal);
$name = $journal['destination_account_name'];
$title = sprintf('%s (%s)', $name, $journal['currency_symbol']);
$title = sprintf('%s (%s)', $name, $resolved['currency_symbol']);
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_symbol' => $resolved['currency_symbol'],
'currency_code' => $resolved['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
$data[$title]['amount'] = bcadd($data[$title]['amount'], $resolved['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
$cache->store($chart);
@@ -198,6 +207,7 @@ class TransactionController extends Controller
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($objectType);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('chart.transactions.sources');
if ($cache->has()) {
return response()->json($cache->get());
@@ -224,14 +234,15 @@ class TransactionController extends Controller
// group by category.
/** @var array $journal */
foreach ($result as $journal) {
$resolved = $this->resolveJournalAmountAndCurrency($journal);
$name = $journal['source_account_name'];
$title = sprintf('%s (%s)', $name, $journal['currency_symbol']);
$title = sprintf('%s (%s)', $name, $resolved['currency_symbol']);
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_symbol' => $resolved['currency_symbol'],
'currency_code' => $resolved['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
$data[$title]['amount'] = bcadd($data[$title]['amount'], $resolved['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
$cache->store($chart);

View File

@@ -54,9 +54,13 @@ trait SupportsGroupProcessingTrait
// create and fire rule engine.
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
foreach ($array as $journalId) {
$newRuleEngine->removeOperator('journal_id');
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalId]);
$newRuleEngine->fire();
}
Log::debug(sprintf('Done with processRules("%s") for %d journal(s)', $type, $set->count()));
}

View File

@@ -150,9 +150,6 @@ class RecalculatesPrimaryCurrencyAmounts
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (Builder $q): void {
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
if ('pgsql' !== config('database.default')) {
$q->orWhere('native_amount', '!=', '')->orWhere('native_foreign_amount', '!=', '');
}
})
->update(['native_amount' => null, 'native_foreign_amount' => null])
;

View File

@@ -78,8 +78,16 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface, UserGroupIn
// only a subset of the fields.
$journalId = (int) $journal['transaction_journal_id'];
$array[$currencyId]['categories'][0]['transaction_journals'][$journalId] = [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
'amount' => Steam::negative($journal['amount']),
'currency_id' => (int) $journal['currency_id'],
'currency_name' => (string) $journal['currency_name'],
'currency_symbol' => (string) $journal['currency_symbol'],
'currency_code' => (string) $journal['currency_code'],
'currency_decimal_places' => (int) $journal['currency_decimal_places'],
'foreign_currency_id' => (int) ($journal['foreign_currency_id'] ?? 0),
'foreign_amount' => isset($journal['foreign_amount']) ? Steam::negative((string) $journal['foreign_amount']) : null,
'pc_amount' => isset($journal['pc_amount']) ? Steam::negative((string) $journal['pc_amount']) : null,
'date' => $journal['date'],
];
}
@@ -124,8 +132,16 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface, UserGroupIn
// only a subset of the fields.
$journalId = (int) $journal['transaction_journal_id'];
$array[$currencyId]['categories'][0]['transaction_journals'][$journalId] = [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
'amount' => Steam::positive($journal['amount']),
'currency_id' => (int) $journal['currency_id'],
'currency_name' => (string) $journal['currency_name'],
'currency_symbol' => (string) $journal['currency_symbol'],
'currency_code' => (string) $journal['currency_code'],
'currency_decimal_places' => (int) $journal['currency_decimal_places'],
'foreign_currency_id' => (int) ($journal['foreign_currency_id'] ?? 0),
'foreign_amount' => isset($journal['foreign_amount']) ? Steam::positive((string) $journal['foreign_amount']) : null,
'pc_amount' => isset($journal['pc_amount']) ? Steam::positive((string) $journal['pc_amount']) : null,
'date' => $journal['date'],
];
}

View File

@@ -153,6 +153,14 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$journalId = (int) $journal['transaction_journal_id'];
$array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [
'amount' => Steam::negative($journal['amount']),
'currency_id' => (int) $journal['currency_id'],
'currency_name' => (string) $journal['currency_name'],
'currency_symbol' => (string) $journal['currency_symbol'],
'currency_code' => (string) $journal['currency_code'],
'currency_decimal_places' => (int) $journal['currency_decimal_places'],
'foreign_currency_id' => (int) ($journal['foreign_currency_id'] ?? 0),
'foreign_amount' => isset($journal['foreign_amount']) ? Steam::negative((string) $journal['foreign_amount']) : null,
'pc_amount' => isset($journal['pc_amount']) ? Steam::negative((string) $journal['pc_amount']) : null,
'date' => $journal['date'],
'source_account_id' => (string) $journal['source_account_id'],
'budget_name' => $journal['budget_name'],
@@ -223,6 +231,14 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$journalId = (int) $journal['transaction_journal_id'];
$array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [
'amount' => Steam::positive($journal['amount']),
'currency_id' => (int) $journal['currency_id'],
'currency_name' => (string) $journal['currency_name'],
'currency_symbol' => (string) $journal['currency_symbol'],
'currency_code' => (string) $journal['currency_code'],
'currency_decimal_places' => (int) $journal['currency_decimal_places'],
'foreign_currency_id' => (int) ($journal['foreign_currency_id'] ?? 0),
'foreign_amount' => isset($journal['foreign_amount']) ? Steam::positive((string) $journal['foreign_amount']) : null,
'pc_amount' => isset($journal['pc_amount']) ? Steam::positive((string) $journal['pc_amount']) : null,
'date' => $journal['date'],
'source_account_id' => (string) $journal['source_account_id'],
'destination_account_id' => (string) $journal['destination_account_id'],

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Services\FireflyIIIOrg\Update;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Facades\Log;
@@ -180,7 +181,7 @@ class GitHubUpdateRequest implements UpdateRequestInterface
private function getReleases(): array
{
$client = new Client();
$opts = ['headers' => ['User-Agent' => 'FireflyIII/'.config('firefly.version')]];
$opts = ['timeout' => 5.0, 'headers' => ['User-Agent' => 'FireflyIII/'.config('firefly.version')]];
$return = [];
$body = '';
if ($this->localDebug && file_exists('json.json')) {
@@ -189,7 +190,7 @@ class GitHubUpdateRequest implements UpdateRequestInterface
if (!$this->localDebug) {
try {
$res = $client->get('https://api.github.com/repos/firefly-iii/firefly-iii/releases', $opts);
} catch (ClientException $e) {
} catch (ClientException|Exception $e) {
Log::error($e->getMessage());
return [];

View File

@@ -88,107 +88,7 @@ class ExportDataGenerator
private bool $exportTransactions = false;
private Carbon $start;
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private UserGroup $userGroup;
public function __construct()
{

View File

@@ -98,8 +98,10 @@ trait GetConfigurationData
$title = sprintf('%s - %s', $start->isoFormat($this->monthAndDayFormat), $end->isoFormat($this->monthAndDayFormat));
$isCustom = true === session('is_custom_range', false);
$today = today(config('app.timezone'));
$ranges = [// first range is the current range:
$title => [$start, $end]];
$ranges = [
// first range is the current range:
$title => [$start, $end],
];
Log::debug(sprintf('dateRange: the date range in the session is"%s" - "%s"', $start->format('Y-m-d'), $end->format('Y-m-d')));
// when current range is a custom range, add the current period as the next range.

View File

@@ -0,0 +1,65 @@
<?php
/**
* ResolvesJournalAmountAndCurrency.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers;
use FireflyIII\Support\Facades\Steam;
trait ResolvesJournalAmountAndCurrency
{
/**
* Normalize journal currency metadata and positive amount, honoring primary currency conversion.
*/
protected function resolveJournalAmountAndCurrency(array $journal, ?array $currency = null): array
{
$currency ??= $journal;
$currencyId = (int) ($journal['currency_id'] ?? $currency['currency_id']);
$currencyName = (string) ($journal['currency_name'] ?? $currency['currency_name']);
$currencySymbol = (string) ($journal['currency_symbol'] ?? $currency['currency_symbol']);
$currencyCode = (string) ($journal['currency_code'] ?? $currency['currency_code']);
$currencyDecimalPlaces = (int) ($journal['currency_decimal_places'] ?? $currency['currency_decimal_places'] ?? 2);
$amount = (string) $journal['amount'];
if ($this->convertToPrimary && null !== $this->primaryCurrency && $currencyId !== $this->primaryCurrency->id) {
$currencyId = $this->primaryCurrency->id;
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyCode = $this->primaryCurrency->code;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
$amount = (int) ($journal['foreign_currency_id'] ?? 0) === $this->primaryCurrency->id
? (string) ($journal['foreign_amount'] ?? '0')
: (string) ($journal['pc_amount'] ?? '0');
}
return [
'currency_id' => $currencyId,
'currency_name' => $currencyName,
'currency_symbol' => $currencySymbol,
'currency_code' => $currencyCode,
'currency_decimal_places' => $currencyDecimalPlaces,
'amount' => Steam::positive($amount),
];
}
}

View File

@@ -150,7 +150,7 @@ class AvailableBudgetCalculator
$item->start_date->format('Y-m-d'),
$item->end_date->format('Y-m-d')
));
$this->abRepository->recalculateAmount($availableBudget);
$this->abRepository->recalculateAmount($item);
}
if (!$this->create) {
Log::debug('Can stop here. have not been asked to create an available budget.');

View File

@@ -453,7 +453,7 @@ class Navigation
$diff = $start->diffInMonths($end, true);
// increment by month (for year)
if ($diff >= 1.0001 && $diff < 12.001) {
$increment = 'addMonth';
$increment = 'addMonthsNoOverflow';
$displayFormat = (string) trans('config.month_js');
}
@@ -464,12 +464,15 @@ class Navigation
}
$begin = clone $start;
$entries = [];
Log::debug(sprintf('listOfPeriods start of loop (end: %s).', $end->format('Y-m-d H:i:s')));
while ($begin < $end) {
Log::debug(sprintf('Begin is now %s.', $begin->format('Y-m-d H:i:s')));
$formatted = $begin->format($format);
$displayed = $begin->isoFormat($displayFormat);
$entries[$formatted] = $displayed;
$begin->{$increment}(); // @phpstan-ignore-line
}
Log::debug('listOfPeriods end of loop.');
return $entries;
}

View File

@@ -178,7 +178,7 @@ class OperatorQuerySearch implements SearchInterface
/** @var QueryParserInterface $parser */
$parser = app(QueryParserInterface::class);
Log::debug(sprintf('Using %s as implementation for QueryParserInterface', $parser::class));
// Log::debug(sprintf('Using %s as implementation for QueryParserInterface', $parser::class));
try {
$parsedQuery = $parser->parse($query);
@@ -189,7 +189,7 @@ class OperatorQuerySearch implements SearchInterface
throw new FireflyException(sprintf('Invalid search value "%s". See the logs.', e($query)), 0, $e);
}
Log::debug(sprintf('Found %d node(s) at top-level', count($parsedQuery->getNodes())));
// Log::debug(sprintf('Found %d node(s) at top-level', count($parsedQuery->getNodes())));
$this->handleSearchNode($parsedQuery, $parsedQuery->isProhibited(false));
// add missing information
@@ -329,7 +329,7 @@ class OperatorQuerySearch implements SearchInterface
*/
private function handleSearchNode(Node $node, bool $flipProhibitedFlag): void
{
Log::debug(sprintf('Now in handleSearchNode(%s)', $node::class));
// Log::debug(sprintf('Now in handleSearchNode(%s)', $node::class));
switch (true) {
case $node instanceof StringNode:

View File

@@ -145,7 +145,7 @@ class QueryParser implements QueryParserInterface
$skipNext = true;
}
if ('' !== $tokenUnderConstruction && !$skipNext) { // @phpstan-ignore-line
Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction));
// Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction));
// If we meet a colon with a left-hand side string, we know we're in a field and are about to set up the value
$fieldName = $tokenUnderConstruction;
$tokenUnderConstruction = '';

View File

@@ -68,12 +68,13 @@ class General extends AbstractExtension
$this->getRootSearchOperator(),
$this->carbonize(),
$this->fireflyIIIConfig(),
$this->bccomp(),
];
}
/**
* Will return "active" when a part of the route matches the argument.
* ie. "accounts" will match "accounts.index".
* i.e. "accounts" will match "accounts.index".
*/
protected function activeRoutePartial(): TwigFunction
{
@@ -89,6 +90,10 @@ class General extends AbstractExtension
});
}
/**
* Will return "active" when a part of the route matches the argument.
* ie. "accounts" will match "accounts.index".
*/
/**
* This function will return "active" when the current route matches the first argument (even partly)
* but, the variable $objectType has been set and matches the second argument.
@@ -189,6 +194,13 @@ class General extends AbstractExtension
});
}
protected function bccomp(): TwigFunction
{
return new TwigFunction('bccomp', static function (string $left, string $right): int {
return bccomp($left, $right, 12);
});
}
protected function carbonize(): TwigFunction
{
return new TwigFunction('carbonize', static fn (string $date): Carbon => new Carbon($date, config('app.timezone')));

View File

@@ -52,6 +52,8 @@ interface RuleEngineInterface
*/
public function getResults(): int;
public function removeOperator(string $type): void;
public function setRefreshTriggers(bool $refreshTriggers): void;
/**

View File

@@ -38,6 +38,7 @@ use FireflyIII\TransactionRules\Factory\ActionFactory;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Override;
/**
* Class SearchRuleEngine
@@ -45,6 +46,7 @@ use Illuminate\Support\Facades\Log;
class SearchRuleEngine implements RuleEngineInterface
{
private readonly Collection $groups;
private array $operators = [];
// always collect the triggers from the database, unless indicated otherwise.
private bool $refreshTriggers = true;
@@ -133,6 +135,21 @@ class SearchRuleEngine implements RuleEngineInterface
return count($this->resultCount);
}
#[Override]
public function removeOperator(string $type): void
{
$new = [];
foreach ($this->operators as $operator) {
if ($type === $operator['type']) {
Log::debug(sprintf('Removing operator "%s"', $type));
continue;
}
$new[] = $operator;
}
$this->operators = $new;
}
public function setRefreshTriggers(bool $refreshTriggers): void
{
$this->refreshTriggers = $refreshTriggers;
@@ -331,6 +348,7 @@ class SearchRuleEngine implements RuleEngineInterface
$searchEngine->setLimit(31337);
$searchEngine->setDate($date);
Log::debug('Search array', $searchArray);
foreach ($searchArray as $type => $searches) {
foreach ($searches as $value) {
$query = sprintf('%s:%s', $type, $value);
@@ -356,15 +374,7 @@ class SearchRuleEngine implements RuleEngineInterface
}
if (!$group->relationLoaded('rules')) {
Log::debug('Group rules have NOT been pre-loaded, load them NOW.');
$rules = $group
->rules()
->orderBy('rules.order', 'ASC')
// ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
// ->where('rule_triggers.trigger_type', 'user_action')
// ->where('rule_triggers.trigger_value', 'store-journal')
->where('rules.active', true)
->get(['rules.*'])
;
$rules = $group->rules()->orderBy('rules.order', 'ASC')->where('rules.active', true)->get(['rules.*']);
}
Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $rules->count()));
@@ -458,6 +468,10 @@ class SearchRuleEngine implements RuleEngineInterface
Log::debug('Found a journal_id trigger with 1 journal, true.');
$journalTrigger = true;
}
if ('journal_id' === $triggerName && is_string($values) && !str_contains($values, ',')) {
Log::debug('Found a journal_id trigger with 1 journal, true.');
$journalTrigger = true;
}
if (in_array($triggerName, ['date_is', 'date', 'on', 'date_before', 'before', 'date_after', 'after'], true)) {
Log::debug('Found a date related trigger, set to true.');
$dateTrigger = true;

View File

@@ -156,12 +156,13 @@ $app = Application::configure(basePath: dirname(__DIR__))
]);
// This middleware is added to ensure that the user is not only logged in and
// authenticated (with MFA and everything), but also admin.
$middleware->appendToGroup('api-admin', [
IsAdmin::class,
]);
$middleware->appendToGroup('admin', [
Authenticate::class,
MFAMiddleware::class,
IsAdmin::class,
Range::class,
InterestingMessage::class,
InterestingMessage::class
]);
// if the user is not logged in, this group applies.

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 6.5.1 - 2026-02-28
> [!IMPORTANT]
> This releases also fixes a security issue, relevant only if you have multiple users using your Firefly III instance. Upgrading is recommended.
### Added
- [PR 11808](https://github.com/firefly-iii/firefly-iii/pull/11808) (Add Thai baht to Currency Seeder) reported by @CinnamonPyro
### Fixed
- [Issue 11817](https://github.com/firefly-iii/firefly-iii/issues/11817) (500 Error if internet is inaccessible while checking for updates) reported by @NoiTheCat
- [Issue 11814](https://github.com/firefly-iii/firefly-iii/issues/11814) (Budget : error with CRON after switch user range view) reported by @fabienfitoussi
- [Issue 11750](https://github.com/firefly-iii/firefly-iii/issues/11750) (500 error when creating first user with USD balance (works after refresh)) reported by @pinalgirkar
### Security
- Security issue where any authenticated user with API access also has read access to the `/api/v1/users` endpoint. Authenticated users would be able to see other user's email addresses, blocked status and roles, even when not admin. No actual financial data was exposed, just the user's info itself.
### API
- Added extra checks to the `/api/v1/users` endpoints.
## v6.5.0 - 2026-02-20
> [!IMPORTANT]
@@ -24,6 +48,8 @@ And yes, despite my goal not to change things, some very clever users (that's yo
- [Discussion 11685](https://github.com/orgs/firefly-iii/discussions/11685) (Yearly budget best practices) started by @molnarti
- [Issue 11778](https://github.com/firefly-iii/firefly-iii/issues/11778) (API update rule trigger only accepts "store-journal") reported by @jhns-de
- [Issue 11785](https://github.com/firefly-iii/firefly-iii/issues/11785) (The `/api/v1/chart/account/overview` endpoint returns incorrect balances when `period` is set to anything larger than `1D` (e.g. `1W`, `1M`).) reported by @R1DEN
- [Issue 11792](https://github.com/firefly-iii/firefly-iii/issues/11792) (Uploading attachment to Piggy Bank causes "Attempt to read property 'user' on null" error) reported by @MrWuTalk2022
- [Issue 11795](https://github.com/firefly-iii/firefly-iii/issues/11795) (`pc_amount` always equals raw `amount` — operator precedence bug in TransactionGroupTransformer) reported by @R1DEN
- Test notification was broken for system owners.
## v6.4.23 - 2026-02-20

272
composer.lock generated
View File

@@ -1878,16 +1878,16 @@
},
{
"name": "laravel/framework",
"version": "v12.52.0",
"version": "v12.53.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7"
"reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/d5511fa74f4608dbb99864198b1954042aa8d5a7",
"reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7",
"url": "https://api.github.com/repos/laravel/framework/zipball/f57f035c0d34503d9ff30be76159bb35a003cd1f",
"reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f",
"shasum": ""
},
"require": {
@@ -2096,7 +2096,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-02-17T17:07:04+00:00"
"time": "2026-02-24T14:35:15+00:00"
},
{
"name": "laravel/passport",
@@ -2235,16 +2235,16 @@
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.9",
"version": "v2.0.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "8f631589ab07b7b52fead814965f5a800459cb3e"
"reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e",
"reference": "8f631589ab07b7b52fead814965f5a800459cb3e",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669",
"reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669",
"shasum": ""
},
"require": {
@@ -2292,7 +2292,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2026-02-03T06:55:34+00:00"
"time": "2026-02-20T19:59:49+00:00"
},
{
"name": "laravel/slack-notification-channel",
@@ -2895,16 +2895,16 @@
},
{
"name": "league/flysystem",
"version": "3.31.0",
"version": "3.32.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff"
"reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
"reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725",
"reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725",
"shasum": ""
},
"require": {
@@ -2972,9 +2972,9 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.31.0"
"source": "https://github.com/thephpleague/flysystem/tree/3.32.0"
},
"time": "2026-01-23T15:38:47+00:00"
"time": "2026-02-25T17:01:41+00:00"
},
{
"name": "league/flysystem-local",
@@ -5058,16 +5058,16 @@
},
{
"name": "predis/predis",
"version": "v3.4.0",
"version": "v3.4.1",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa"
"reference": "0850f2f36ee179f0ff96c92c750e1366c6cd754c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/1183f5732e6b10efd33f64984a96726eaecb59aa",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa",
"url": "https://api.github.com/repos/predis/predis/zipball/0850f2f36ee179f0ff96c92c750e1366c6cd754c",
"reference": "0850f2f36ee179f0ff96c92c750e1366c6cd754c",
"shasum": ""
},
"require": {
@@ -5109,7 +5109,7 @@
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.4.0"
"source": "https://github.com/predis/predis/tree/v3.4.1"
},
"funding": [
{
@@ -5117,7 +5117,7 @@
"type": "github"
}
],
"time": "2026-02-11T17:30:28+00:00"
"time": "2026-02-23T19:51:21+00:00"
},
{
"name": "psr/cache",
@@ -6366,16 +6366,16 @@
},
{
"name": "symfony/cache",
"version": "v8.0.5",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "92e9960386c7e01f58198038c199d522959a843c"
"reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/92e9960386c7e01f58198038c199d522959a843c",
"reference": "92e9960386c7e01f58198038c199d522959a843c",
"url": "https://api.github.com/repos/symfony/cache/zipball/59184fa14658d7724cd9b8743d91c1b1aa618bff",
"reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff",
"shasum": ""
},
"require": {
@@ -6442,7 +6442,7 @@
"psr6"
],
"support": {
"source": "https://github.com/symfony/cache/tree/v8.0.5"
"source": "https://github.com/symfony/cache/tree/v8.0.6"
},
"funding": [
{
@@ -6462,7 +6462,7 @@
"type": "tidelift"
}
],
"time": "2026-01-27T16:18:07+00:00"
"time": "2026-02-21T23:29:37+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -6619,16 +6619,16 @@
},
{
"name": "symfony/console",
"version": "v7.4.4",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894"
"reference": "6d643a93b47398599124022eb24d97c153c12f27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
"reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
"url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27",
"reference": "6d643a93b47398599124022eb24d97c153c12f27",
"shasum": ""
},
"require": {
@@ -6693,7 +6693,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.4.4"
"source": "https://github.com/symfony/console/tree/v7.4.6"
},
"funding": [
{
@@ -6713,20 +6713,20 @@
"type": "tidelift"
}
],
"time": "2026-01-13T11:36:38+00:00"
"time": "2026-02-25T17:02:47+00:00"
},
{
"name": "symfony/css-selector",
"version": "v8.0.0",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b"
"reference": "2a178bf80f05dbbe469a337730eba79d61315262"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b",
"reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262",
"reference": "2a178bf80f05dbbe469a337730eba79d61315262",
"shasum": ""
},
"require": {
@@ -6762,7 +6762,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v8.0.0"
"source": "https://github.com/symfony/css-selector/tree/v8.0.6"
},
"funding": [
{
@@ -6782,7 +6782,7 @@
"type": "tidelift"
}
],
"time": "2025-10-30T14:17:19+00:00"
"time": "2026-02-17T13:07:04+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -7163,16 +7163,16 @@
},
{
"name": "symfony/finder",
"version": "v7.4.5",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
"reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
"shasum": ""
},
"require": {
@@ -7207,7 +7207,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.4.5"
"source": "https://github.com/symfony/finder/tree/v7.4.6"
},
"funding": [
{
@@ -7227,20 +7227,20 @@
"type": "tidelift"
}
],
"time": "2026-01-26T15:07:59+00:00"
"time": "2026-01-29T09:40:50+00:00"
},
{
"name": "symfony/http-client",
"version": "v8.0.5",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4"
"reference": "f425139487f904e198f99e3c416c79ed08cef3c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/f9fdd372473e66469c6d32a4ed12efcffdea38c4",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4",
"url": "https://api.github.com/repos/symfony/http-client/zipball/f425139487f904e198f99e3c416c79ed08cef3c3",
"reference": "f425139487f904e198f99e3c416c79ed08cef3c3",
"shasum": ""
},
"require": {
@@ -7303,7 +7303,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v8.0.5"
"source": "https://github.com/symfony/http-client/tree/v8.0.6"
},
"funding": [
{
@@ -7323,7 +7323,7 @@
"type": "tidelift"
}
],
"time": "2026-01-27T16:18:07+00:00"
"time": "2026-02-20T07:51:53+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -7405,16 +7405,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.4.5",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6"
"reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065",
"reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065",
"shasum": ""
},
"require": {
@@ -7463,7 +7463,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.5"
"source": "https://github.com/symfony/http-foundation/tree/v7.4.6"
},
"funding": [
{
@@ -7483,20 +7483,20 @@
"type": "tidelift"
}
],
"time": "2026-01-27T16:16:02+00:00"
"time": "2026-02-21T16:25:55+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.4.5",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "229eda477017f92bd2ce7615d06222ec0c19e82a"
"reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a",
"reference": "229eda477017f92bd2ce7615d06222ec0c19e82a",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83",
"reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83",
"shasum": ""
},
"require": {
@@ -7538,7 +7538,7 @@
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/css-selector": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0",
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
@@ -7582,7 +7582,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.4.5"
"source": "https://github.com/symfony/http-kernel/tree/v7.4.6"
},
"funding": [
{
@@ -7602,20 +7602,20 @@
"type": "tidelift"
}
],
"time": "2026-01-28T10:33:42+00:00"
"time": "2026-02-26T08:30:57+00:00"
},
{
"name": "symfony/mailer",
"version": "v7.4.4",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6"
"reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/7b750074c40c694ceb34cb926d6dffee231c5cd6",
"reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6",
"url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
"reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
"shasum": ""
},
"require": {
@@ -7666,7 +7666,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v7.4.4"
"source": "https://github.com/symfony/mailer/tree/v7.4.6"
},
"funding": [
{
@@ -7686,7 +7686,7 @@
"type": "tidelift"
}
],
"time": "2026-01-08T08:25:11+00:00"
"time": "2026-02-25T16:50:00+00:00"
},
{
"name": "symfony/mailgun-mailer",
@@ -7760,16 +7760,16 @@
},
{
"name": "symfony/mime",
"version": "v7.4.5",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148"
"reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148",
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148",
"url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f",
"reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f",
"shasum": ""
},
"require": {
@@ -7780,7 +7780,7 @@
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<5.2|>=6",
"phpdocumentor/reflection-docblock": "<5.2|>=7",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
@@ -7788,7 +7788,7 @@
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
@@ -7825,7 +7825,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.4.5"
"source": "https://github.com/symfony/mime/tree/v7.4.6"
},
"funding": [
{
@@ -7845,7 +7845,7 @@
"type": "tidelift"
}
],
"time": "2026-01-27T08:59:58+00:00"
"time": "2026-02-05T15:57:06+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8902,16 +8902,16 @@
},
{
"name": "symfony/routing",
"version": "v7.4.4",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "0798827fe2c79caeed41d70b680c2c3507d10147"
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147",
"reference": "0798827fe2c79caeed41d70b680c2c3507d10147",
"url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938",
"reference": "238d749c56b804b31a9bf3e26519d93b65a60938",
"shasum": ""
},
"require": {
@@ -8963,7 +8963,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v7.4.4"
"source": "https://github.com/symfony/routing/tree/v7.4.6"
},
"funding": [
{
@@ -8983,7 +8983,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:19:02+00:00"
"time": "2026-02-25T16:50:00+00:00"
},
{
"name": "symfony/service-contracts",
@@ -9074,16 +9074,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "758b372d6882506821ed666032e43020c4f57194"
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -9140,7 +9140,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.4"
"source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -9160,20 +9160,20 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-02-09T10:14:57+00:00"
},
{
"name": "symfony/translation",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10"
"reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/db70c8ce7db74fd2da7b1d268db46b2a8ce32c10",
"reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10",
"url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
"reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
"shasum": ""
},
"require": {
@@ -9233,7 +9233,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v8.0.4"
"source": "https://github.com/symfony/translation/tree/v8.0.6"
},
"funding": [
{
@@ -9253,7 +9253,7 @@
"type": "tidelift"
}
],
"time": "2026-01-13T13:06:50+00:00"
"time": "2026-02-17T13:07:04+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -9417,16 +9417,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.4.4",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "0e4769b46a0c3c62390d124635ce59f66874b282"
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282",
"reference": "0e4769b46a0c3c62390d124635ce59f66874b282",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291",
"reference": "045321c440ac18347b136c63d2e9bf28a2dc0291",
"shasum": ""
},
"require": {
@@ -9480,7 +9480,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.4.4"
"source": "https://github.com/symfony/var-dumper/tree/v7.4.6"
},
"funding": [
{
@@ -9500,7 +9500,7 @@
"type": "tidelift"
}
],
"time": "2026-01-01T22:13:48+00:00"
"time": "2026-02-15T10:53:20+00:00"
},
{
"name": "symfony/var-exporter",
@@ -10472,24 +10472,24 @@
},
{
"name": "fruitcake/laravel-debugbar",
"version": "v4.0.9",
"version": "v4.0.10",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/laravel-debugbar.git",
"reference": "4eee2f032172fd6548028395d7a1adbd8eae2ba0"
"reference": "96afd5efc93c2cb3140df356893381296259695b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/4eee2f032172fd6548028395d7a1adbd8eae2ba0",
"reference": "4eee2f032172fd6548028395d7a1adbd8eae2ba0",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/96afd5efc93c2cb3140df356893381296259695b",
"reference": "96afd5efc93c2cb3140df356893381296259695b",
"shasum": ""
},
"require": {
"illuminate/routing": "^11|^12",
"illuminate/session": "^11|^12",
"illuminate/support": "^11|^12",
"illuminate/routing": "^11|^12|^13.0",
"illuminate/session": "^11|^12|^13.0",
"illuminate/support": "^11|^12|^13.0",
"php": "^8.2",
"php-debugbar/php-debugbar": "^3.1",
"php-debugbar/php-debugbar": "^3.3.1",
"php-debugbar/symfony-bridge": "^1.1"
},
"replace": {
@@ -10558,7 +10558,7 @@
],
"support": {
"issues": "https://github.com/fruitcake/laravel-debugbar/issues",
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.0.9"
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.0.10"
},
"funding": [
{
@@ -10570,7 +10570,7 @@
"type": "github"
}
],
"time": "2026-02-17T08:14:13+00:00"
"time": "2026-02-26T11:45:48+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -10666,40 +10666,40 @@
},
{
"name": "larastan/larastan",
"version": "v3.9.2",
"version": "v3.9.3",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2"
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2",
"reference": "2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2",
"url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
"reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.7.0",
"illuminate/console": "^11.44.2 || ^12.4.1",
"illuminate/container": "^11.44.2 || ^12.4.1",
"illuminate/contracts": "^11.44.2 || ^12.4.1",
"illuminate/database": "^11.44.2 || ^12.4.1",
"illuminate/http": "^11.44.2 || ^12.4.1",
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"illuminate/console": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/container": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/database": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/http": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
"php": "^8.2",
"phpstan/phpstan": "^2.1.32"
},
"require-dev": {
"doctrine/coding-standard": "^13",
"laravel/framework": "^11.44.2 || ^12.7.2",
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1",
"orchestra/testbench-core": "^9.12.0 || ^10.1",
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15"
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
@@ -10744,7 +10744,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.9.2"
"source": "https://github.com/larastan/larastan/tree/v3.9.3"
},
"funding": [
{
@@ -10752,7 +10752,7 @@
"type": "github"
}
],
"time": "2026-01-30T15:16:32+00:00"
"time": "2026-02-20T12:07:12+00:00"
},
{
"name": "laravel-json-api/testing",
@@ -11140,16 +11140,16 @@
},
{
"name": "php-debugbar/php-debugbar",
"version": "v3.4.0",
"version": "v3.4.1",
"source": {
"type": "git",
"url": "https://github.com/php-debugbar/php-debugbar.git",
"reference": "e50d470344b62a033a76d3d10a803b04c8e3be69"
"reference": "ee9c718797a4c1fdf6c4d980cb3edcc1eeeddcc7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/e50d470344b62a033a76d3d10a803b04c8e3be69",
"reference": "e50d470344b62a033a76d3d10a803b04c8e3be69",
"url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/ee9c718797a4c1fdf6c4d980cb3edcc1eeeddcc7",
"reference": "ee9c718797a4c1fdf6c4d980cb3edcc1eeeddcc7",
"shasum": ""
},
"require": {
@@ -11226,7 +11226,7 @@
],
"support": {
"issues": "https://github.com/php-debugbar/php-debugbar/issues",
"source": "https://github.com/php-debugbar/php-debugbar/tree/v3.4.0"
"source": "https://github.com/php-debugbar/php-debugbar/tree/v3.4.1"
},
"funding": [
{
@@ -11238,7 +11238,7 @@
"type": "github"
}
],
"time": "2026-02-14T14:10:26+00:00"
"time": "2026-02-26T11:40:30+00:00"
},
{
"name": "php-debugbar/symfony-bridge",

View File

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

View File

@@ -87,6 +87,7 @@ class TransactionCurrencySeeder extends Seeder
$currencies[] = ['code' => 'SAR', 'name' => 'Saudi Riyal', 'symbol' => 'SAR', 'decimal_places' => 2];
$currencies[] = ['code' => 'RSD', 'name' => 'Serbian Dinar', 'symbol' => 'RSD', 'decimal_places' => 2];
$currencies[] = ['code' => 'TWD', 'name' => 'New Taiwan Dollar', 'symbol' => 'NT$', 'decimal_places' => 0];
$currencies[] = ['code' => 'THB', 'name' => 'Thai baht', 'symbol' => '฿', 'decimal_places' => 2];
foreach ($currencies as $currency) {
if (null === TransactionCurrency::where('code', $currency['code'])->first()) {

View File

@@ -14,7 +14,7 @@ tab-width = 4
use-tabs = false
trailing-comma = false
method-chain-breaking-style = "same_line"
preserve-breaking-array-like = false
preserve-breaking-array-like = true
align-assignment-like = true
null-type-hint = "null_pipe"
sort-class-methods = true

102
package-lock.json generated
View File

@@ -3246,9 +3246,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3364,42 +3364,42 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.28",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz",
"integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==",
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/shared": "3.5.28",
"@vue/shared": "3.5.29",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.28",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz",
"integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==",
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-core": "3.5.29",
"@vue/shared": "3.5.29"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.28",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz",
"integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==",
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
"integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/compiler-core": "3.5.28",
"@vue/compiler-dom": "3.5.28",
"@vue/compiler-ssr": "3.5.28",
"@vue/shared": "3.5.28",
"@vue/compiler-core": "3.5.29",
"@vue/compiler-dom": "3.5.29",
"@vue/compiler-ssr": "3.5.29",
"@vue/shared": "3.5.29",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
@@ -3407,14 +3407,14 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.28",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz",
"integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==",
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-dom": "3.5.29",
"@vue/shared": "3.5.29"
}
},
"node_modules/@vue/component-compiler-utils": {
@@ -3496,9 +3496,9 @@
"license": "MIT"
},
"node_modules/@vue/shared": {
"version": "3.5.28",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz",
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
"dev": true,
"license": "MIT"
},
@@ -3980,9 +3980,9 @@
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"version": "10.4.27",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
"integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
"dev": true,
"funding": [
{
@@ -4001,7 +4001,7 @@
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"caniuse-lite": "^1.0.30001774",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -4033,9 +4033,9 @@
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4597,9 +4597,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001774",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
"version": "1.0.30001775",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
"integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==",
"dev": true,
"funding": [
{
@@ -5859,9 +5859,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
"integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7923,9 +7923,9 @@
}
},
"node_modules/launch-editor": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz",
"integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz",
"integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8328,9 +8328,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -11836,9 +11836,9 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.105.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz",
"integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==",
"version": "5.105.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz",
"integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11848,7 +11848,7 @@
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn": "^8.16.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
@@ -11866,7 +11866,7 @@
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.16",
"watchpack": "^2.5.1",
"webpack-sources": "^3.3.3"
"webpack-sources": "^3.3.4"
},
"bin": {
"webpack": "bin/webpack.js"

View File

@@ -100,7 +100,7 @@ function updateBudgetedAmount(e) {
input.data('limit', data.id);
// update amount left.
$('.left_span[data-limit="0"][data-id="' + budgetId + '"]').html(data.left_formatted);
if (data.left_per_day > 0) {
if (data.left_per_day > 0 && !data.in_past) {
$('.left_span[data-limit="0"][data-id="' + budgetId + '"]').html(data.left_formatted + '(' + data.left_per_day_formatted + ')');
}
// update budgeted amount
@@ -117,7 +117,7 @@ function updateBudgetedAmount(e) {
input.prop('disabled', false);
input.data('limit', data.id);
$('.left_span[data-limit="' + budgetLimitId + '"]').html(data.left_formatted);
if (data.left_per_day > 0) {
if (data.left_per_day > 0 && !data.in_past) {
$('.left_span[data-limit="' + budgetLimitId + '"]').html(data.left_formatted + '(' + data.left_per_day_formatted + ')');
}
updateTotalBudgetedAmount(data.transaction_currency_id);

View File

@@ -324,89 +324,11 @@
{% endif %}
</td>
<td class="hidden-sm hidden-xs spent" data-id="{{ budget.id }}" style="text-align:right;">
{% for spentInfo in budget.spent %}
{{ formatAmountBySymbol(spentInfo.spent, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
{% if 0 == activeDaysPassed %}
({{ formatAmountBySymbol(spentInfo.spent, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(spentInfo.spent / activeDaysPassed, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
<br/>
{% endfor %}
{% for budgetLimit in budget.budgeted %}
{% if null == budget.spent[budgetLimit.currency_id] %}
{{ formatAmountBySymbol(0, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}<br/>
{% endif %}
{% endfor %}
{% include('budgets.partials.amount-spent') %}
</td>
{# this cell displays the amount left in the budget, per budget limit. #}
<td class="left" data-id="{{ budget.id }}" style="text-align: right;">
{% for spentInfo in budget.spent %}
{% set countLimit = 0 %}
{% for budgetLimit in budget.budgeted %}
{# now looping a single budget limit. #}
{% if spentInfo.currency_id == budgetLimit.currency_id and budgetLimit.in_range %}
{# the code below is used for budget limits INSIDE the current view range. #}
{% set countLimit = countLimit + 1 %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{# the amount left is automatically calculated. #}
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
{% if spentInfo.spent + budgetLimit.amount > 0 %}
{% if 0 == activeDaysLeft %}
({{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol((spentInfo.spent + budgetLimit.amount) / activeDaysLeft, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
{% else %}
({{ formatAmountBySymbol(0, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
</span>
<br/>
{% endif %}
{% if spentInfo.currency_id == budgetLimit.currency_id and not budgetLimit.in_range and 0.0 == budgetLimit.total_days %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
</span>
<span class="text-muted">({{ 'unknown'|_ }})</span>
{% endif %}
{% if spentInfo.currency_id == budgetLimit.currency_id and not budgetLimit.in_range and 0.0 != budgetLimit.total_days %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
</span>
({{ formatAmountBySymbol((spentInfo.spent + budgetLimit.amount) / budgetLimit.total_days, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
{% endfor %}
{% if countLimit == 0 %}
{# display nothing #}
{% endif %}
{% endfor %}
{% for budgetLimit in budget.budgeted %}
{% if null == budget.spent[budgetLimit.currency_id] %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(budgetLimit.amount, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}
{% if budgetLimit.in_range %}
{% if 0 == activeDaysLeft %}
({{ formatAmountBySymbol(budgetLimit.amount, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(budgetLimit.amount / activeDaysLeft, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% endif %}
{% endif %}
{% if not budgetLimit.in_range %}
{# For issue #10441, add per day if the budget limit is out of range. #}
({{ formatAmountBySymbol(budgetLimit.amount / budgetLimit.total_days, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% endif %}
</span>
<br/>
{% endif %}
{% endfor %}
{% include('budgets.partials.amount-left') %}
</td>
</tr>
{% endfor %}

View File

@@ -0,0 +1,96 @@
{# The amount left can only be shown for actual budget limits. #}
{% 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) }}
{# 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) }})
{% else %}
({{ formatAmountBySymbol(budgetLimit.left / budgetLimit.active_days_left, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% 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) }})
{% endif %}
</span><br />
{% endfor %}
{#
{% for spentInfo in budget.spent %}
{% set countLimit = 0 %}
<!-- loop each budget limit collected for this budget in this period. -->
{% for budgetLimit in budget.budgeted %}
<!-- now looping a single budget limit. -->
{% if spentInfo.currency_id == budgetLimit.currency_id and budgetLimit.in_range %}
<!-- the code below is used for budget limits INSIDE the current view range. -->
{% set countLimit = countLimit + 1 %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
<!--the amount left is automatically calculated. -->
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
{% if spentInfo.spent + budgetLimit.amount > 0 %}
{% if 0 == activeDaysLeft %}
({{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol((spentInfo.spent + budgetLimit.amount) / activeDaysLeft, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
{% else %}
({{ formatAmountBySymbol(0, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
</span>
<br/>
{% endif %}
{% if spentInfo.currency_id == budgetLimit.currency_id and not budgetLimit.in_range and 0.0 == budgetLimit.total_days %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
</span>
<span class="text-muted">({{ 'unknown'|_ }})</span>
{% endif %}
{% if spentInfo.currency_id == budgetLimit.currency_id and not budgetLimit.in_range and 0.0 != budgetLimit.total_days %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(spentInfo.spent + budgetLimit.amount, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
</span>
({{ formatAmountBySymbol((spentInfo.spent + budgetLimit.amount) / budgetLimit.total_days, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
{% endfor %}
{% if countLimit == 0 %}
<!-- display nothing -->
{% endif %}
-->
{% endfor %}
{% for budgetLimit in budget.budgeted %}
{% if null == budget.spent[budgetLimit.currency_id] %}
<span class="left_span" data-currency="{{ spentInfo.currency_id }}" data-limit="{{ budgetLimit.id }}"
data-value="{{ spentInfo.spent + budgetLimit.amount }}" class="amount_left">
{{ formatAmountBySymbol(budgetLimit.amount, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}
{% if budgetLimit.in_range %}
{% if 0 == activeDaysLeft %}
({{ formatAmountBySymbol(budgetLimit.amount, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(budgetLimit.amount / activeDaysLeft, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% endif %}
{% endif %}
{% if not budgetLimit.in_range %}
<!-- For issue #10441, add per day if the budget limit is out of range. -->
({{ formatAmountBySymbol(budgetLimit.amount / budgetLimit.total_days, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% endif %}
</span>
<br/>
{% endif %}
{% endfor %}
#}

View File

@@ -0,0 +1,42 @@
{# this is spent in budget limits: #}
{% for budgetLimit in budget.budgeted %}
{{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}
{% if 0 == budgetLimit.active_days_passed %}
({{ formatAmountBySymbol(budgetLimit.spent, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(budgetLimit.spent / budgetLimit.active_days_passed, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }})
{% endif %}
<br />
{% endfor %}
{# 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) }}
{% if 0 == activeDaysPassed %}
({{ formatAmountBySymbol(spent.spent_outside, spent.currency_symbol, spent.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(spent.spent_outside / activeDaysPassed, spent.currency_symbol, spent.currency_decimal_places) }})
{% endif %}
<br />
{% endif %}
{% endfor %}
{#
{% for spentInfo in budget.spent %}
{{ formatAmountBySymbol(spentInfo.spent, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }}
{% if 0 == activeDaysPassed %}
({{ formatAmountBySymbol(spentInfo.spent, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% else %}
({{ formatAmountBySymbol(spentInfo.spent / activeDaysPassed, spentInfo.currency_symbol, spentInfo.currency_decimal_places) }})
{% endif %}
<br/>
{% endfor %}
{% for budgetLimit in budget.budgeted %}
{% if null == budget.spent[budgetLimit.currency_id] %}
{{ formatAmountBySymbol(0, budgetLimit.currency_symbol, budgetLimit.currency_decimal_places) }}<br/>
{% endif %}
{% endfor %}
#}

View File

@@ -344,9 +344,10 @@ Route::group(
// User group API routes.
Route::group(
[
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\UserGroup',
'prefix' => 'v1/user-groups',
'as' => 'api.v1.user-groups.',
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\UserGroup',
'prefix' => 'v1/user-groups',
'as' => 'api.v1.user-groups.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
@@ -720,9 +721,10 @@ Route::group(
// Configuration API routes
Route::group(
[
'namespace' => 'FireflyIII\Api\V1\Controllers\System',
'prefix' => 'v1/configuration',
'as' => 'api.v1.configuration.',
'namespace' => 'FireflyIII\Api\V1\Controllers\System',
'prefix' => 'v1/configuration',
'as' => 'api.v1.configuration.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::get('', ['uses' => 'ConfigurationController@index', 'as' => 'index']);
@@ -736,6 +738,7 @@ Route::group(
'namespace' => 'FireflyIII\Api\V1\Controllers\System',
'prefix' => 'v1/users',
'as' => 'api.v1.users.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::get('', ['uses' => 'UserController@index', 'as' => 'index']);