Compare commits

...

21 Commits

Author SHA1 Message Date
github-actions[bot]
5637573fd0 Merge pull request #10982 from firefly-iii/release-1759116145
🤖 Automatically merge the PR into the develop branch.
2025-09-29 05:22:35 +02:00
JC5
48255ae6ee 🤖 Auto commit for release 'develop' on 2025-09-29 2025-09-29 05:22:25 +02:00
James Cole
ea02986170 Merge pull request #10979 from maksimkurb/patch-1
Add Kazakhstani Tenge (KZT) currency
2025-09-28 15:28:15 +02:00
mergify[bot]
f4fbc15ac6 Merge branch 'develop' into patch-1 2025-09-28 07:17:01 +00:00
Maxim Kurbatov
a02c4b42a4 Add Kazakhstani Tenge (KZT) currency
Signed-off-by: Maxim Kurbatov <maxim@kurb.me>
2025-09-28 11:49:37 +05:00
James Cole
1ff47441ce Also add no budget and no category overview. 2025-09-27 16:07:03 +02:00
James Cole
2cc8568077 Clean up code. 2025-09-27 06:40:56 +02:00
github-actions[bot]
408687ec6b Merge pull request #10975 from firefly-iii/release-1758945886
🤖 Automatically merge the PR into the develop branch.
2025-09-27 06:04:55 +02:00
JC5
308abffb0b 🤖 Auto commit for release 'develop' on 2025-09-27 2025-09-27 06:04:46 +02:00
James Cole
6743b3fe83 Remove stats for other objects as well. 2025-09-27 06:01:14 +02:00
James Cole
ac0113e445 Fix some rector code. 2025-09-27 05:57:58 +02:00
James Cole
0f0a28c3d9 Add cached periods for tags. 2025-09-27 05:49:22 +02:00
James Cole
79f2d70211 Fix #10974 2025-09-27 05:35:20 +02:00
James Cole
8a06c0f7ec Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Support/Http/Controllers/PeriodOverview.php
2025-09-27 05:32:41 +02:00
James Cole
c54da62005 Enable period statistics for category. 2025-09-27 05:31:26 +02:00
github-actions[bot]
e5923202af Merge pull request #10973 from firefly-iii/release-1758914746
🤖 Automatically merge the PR into the develop branch.
2025-09-26 21:25:55 +02:00
JC5
eb6f78406e 🤖 Auto commit for release 'develop' on 2025-09-26 2025-09-26 21:25:46 +02:00
James Cole
d61f87f649 Fix URL 2025-09-26 20:47:44 +02:00
James Cole
33dcce7525 Optimize query for collecting transactions. 2025-09-26 20:46:23 +02:00
James Cole
b1d86c3a37 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Models/PeriodStatistic.php
2025-09-26 19:48:46 +02:00
James Cole
822dee6e70 Allow statistics to be removed from /flush 2025-09-26 19:48:20 +02:00
45 changed files with 725 additions and 616 deletions

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.88.0",
"version": "v3.88.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe"
"reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f23469674ae50d40e398bfff8018911a2a2b0dbe",
"reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8d15584bafb0f0d9d938827840060fd4a3ebc99",
"reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99",
"shasum": ""
},
"require": {
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.2"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2025-09-24T21:31:42+00:00"
"time": "2025-09-27T00:24:15+00:00"
},
{
"name": "psr/container",
@@ -1252,16 +1252,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
@@ -1326,7 +1326,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.3"
"source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@@ -1346,7 +1346,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -2365,16 +2365,16 @@
},
{
"name": "symfony/process",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@@ -2406,7 +2406,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.3.3"
"source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@@ -2426,7 +2426,7 @@
"type": "tidelift"
}
],
"time": "2025-08-18T09:42:54+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2575,16 +2575,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
"reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
"reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@@ -2599,7 +2599,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -2642,7 +2641,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.3"
"source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -2662,7 +2661,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-11T14:36:48+00:00"
}
],
"packages-dev": [],

View File

@@ -28,8 +28,10 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Http\JsonResponse;
/**
@@ -96,6 +98,7 @@ class PiggyBankController extends Controller
/** @var PiggyBank $piggy */
foreach ($piggies as $piggy) {
/** @var TransactionCurrency $currency */
$currency = $piggy->transactionCurrency;
$currentAmount = $this->piggyRepository->getCurrentAmount($piggy);
$objectGroup = $piggy->objectGroups()->first();
@@ -105,8 +108,8 @@ class PiggyBankController extends Controller
'name_with_balance' => sprintf(
'%s (%s / %s)',
$piggy->name,
app('amount')->formatAnything($currency, $currentAmount, false),
app('amount')->formatAnything($currency, $piggy->target_amount, false),
Amount::formatAnything($currency, $currentAmount, false),
Amount::formatAnything($currency, $piggy->target_amount, false),
),
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,

View File

@@ -72,7 +72,7 @@ class DestroyController extends Controller
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $exchangeRate) {
if ($exchangeRate instanceof CurrencyExchangeRate) {
$this->repository->deleteRate($exchangeRate);
}

View File

@@ -95,7 +95,7 @@ class ShowController extends Controller
$transformer->setParameters($this->parameters);
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null === $exchangeRate) {
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}

View File

@@ -74,7 +74,7 @@ class UpdateController extends Controller
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null === $exchangeRate) {
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}
$date = $request->getDate();

View File

@@ -28,7 +28,6 @@ namespace FireflyIII\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
/**
* Class SeparateTimezoneCaster

View File

@@ -283,7 +283,7 @@ class ApplyRules extends Command
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if (null === $inputEnd || null === $inputStart) {
if (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;

View File

@@ -109,6 +109,12 @@ class StoredGroupEventHandler
$dest = $journal->transactions()->where('amount', '>', '0')->first();
$repository->deleteStatisticsForModel($source->account, $journal->date);
$repository->deleteStatisticsForModel($dest->account, $journal->date);
foreach ($journal->categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($journal->tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
}
}

View File

@@ -58,6 +58,9 @@ class UpdatedGroupEventHandler
}
/**
* TODO duplicate
*/
private function removePeriodStatistics(UpdatedTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
@@ -69,6 +72,12 @@ class UpdatedGroupEventHandler
$dest = $journal->transactions()->where('amount', '>', '0')->first();
$repository->deleteStatisticsForModel($source->account, $journal->date);
$repository->deleteStatisticsForModel($dest->account, $journal->date);
foreach ($journal->categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($journal->tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
}
}

View File

@@ -62,5 +62,8 @@ class WebhookEventHandler
Log::debug(sprintf('Skip message #%d', $message->id));
}
}
// clean up sent messages table:
WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(30))->delete();
}
}

View File

@@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -66,8 +67,8 @@ class UpdateController extends Controller
{
$subTitle = (string) trans('firefly.update_check_title');
$subTitleIcon = 'fa-star';
$permission = app('fireflyconfig')->get('permission_update_check', -1);
$channel = app('fireflyconfig')->get('update_channel', 'stable');
$permission = FireflyConfig::get('permission_update_check', -1);
$channel = FireflyConfig::get('update_channel', 'stable');
$selected = $permission->data;
$channelSelected = $channel->data;
$options = [
@@ -96,9 +97,9 @@ class UpdateController extends Controller
$channel = $request->get('update_channel');
$channel = in_array($channel, ['stable', 'beta', 'alpha'], true) ? $channel : 'stable';
app('fireflyconfig')->set('permission_update_check', $checkForUpdates);
app('fireflyconfig')->set('last_update_check', Carbon::now()->getTimestamp());
app('fireflyconfig')->set('update_channel', $channel);
FireflyConfig::set('permission_update_check', $checkForUpdates);
FireflyConfig::set('last_update_check', Carbon::now()->getTimestamp());
FireflyConfig::set('update_channel', $channel);
session()->flash('success', (string) trans('firefly.configuration_updated'));
return redirect(route('settings.update-check'));

View File

@@ -92,7 +92,7 @@ class ShowController extends Controller
// get first journal ever to set off the budget period overview.
$first = $this->journalRepos->firstNull();
$firstDate = $first instanceof TransactionJournal ? $first->date : $start;
$periods = $this->getNoBudgetPeriodOverview($firstDate, $end);
$periods = $this->getNoModelPeriodOverview('budget', $firstDate, $end);
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;

View File

@@ -35,6 +35,7 @@ use FireflyIII\Support\Http\Controllers\PeriodOverview;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
@@ -74,7 +75,7 @@ class NoCategoryController extends Controller
*/
public function show(Request $request, ?Carbon $start = null, ?Carbon $end = null)
{
app('log')->debug('Start of noCategory()');
Log::debug('Start of noCategory()');
$start ??= session('start');
$end ??= session('end');
@@ -82,14 +83,12 @@ class NoCategoryController extends Controller
/** @var Carbon $end */
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
$subTitle = trans(
'firefly.without_category_between',
['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]
);
$periods = $this->getNoCategoryPeriodOverview($start);
$subTitle = trans('firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]);
$first = $this->journalRepos->firstNull()->date ?? clone $start;
$periods = $this->getNoModelPeriodOverview('category', $first, $end);
app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@@ -117,13 +116,13 @@ class NoCategoryController extends Controller
$periods = new Collection();
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
app('log')->debug('Start of noCategory()');
Log::debug('Start of noCategory()');
$subTitle = (string) trans('firefly.all_journals_without_category');
$first = $this->journalRepos->firstNull();
$start = $first instanceof TransactionJournal ? $first->date : new Carbon();
$end = today(config('app.timezone'));
app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);

View File

@@ -30,6 +30,7 @@ use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
@@ -108,6 +109,8 @@ class DebugController extends Controller
Artisan::call('route:clear');
Artisan::call('view:clear');
PeriodStatistic::where('id', '>', 0)->delete();
// also do some recalculations.
Artisan::call('correction:recalculates-liabilities');
AccountBalanceCalculator::recalculateAll(false);

View File

@@ -31,6 +31,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Services\Internal\Update\GroupCloneService;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
@@ -76,7 +77,7 @@ class CreateController extends Controller
// event!
event(new StoredTransactionGroup($newGroup, true, true));
app('preferences')->mark();
Preferences::mark();
$title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;
$link = route('transactions.show', [$newGroup->id]);
@@ -103,7 +104,7 @@ class CreateController extends Controller
* */
public function create(?string $objectType)
{
app('preferences')->mark();
Preferences::mark();
$sourceId = (int) request()->get('source');
$destinationId = (int) request()->get('destination');
@@ -114,7 +115,9 @@ class CreateController extends Controller
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType)));
$subTitleIcon = 'fa-plus';
$optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data;
/** @var null|array $optionalFields */
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$previousUrl = $this->rememberPreviousUrl('transactions.create.url');

View File

@@ -109,4 +109,9 @@ class Category extends Model
'user_group_id' => 'integer',
];
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -8,25 +8,28 @@ use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class PeriodStatistic extends Model
{
use ReturnsIntegerUserIdTrait;
use SoftDeletes;
protected function casts(): array
{
return [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'start' => SeparateTimezoneCaster::class,
'end' => SeparateTimezoneCaster::class,
];
}
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class);
}
protected function count(): Attribute
{
return Attribute::make(

View File

@@ -104,4 +104,9 @@ class Tag extends Model
'user_group_id' => 'integer',
];
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -76,6 +76,14 @@ class UserGroup extends Model
return $this->hasMany(Account::class);
}
/**
* Link to accounts.
*/
public function periodStatistics(): HasMany
{
return $this->hasMany(PeriodStatistic::class);
}
/**
* Link to attachments.
*/

View File

@@ -163,7 +163,6 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class);
$this->app->bind(ALERepositoryInterface::class, ALERepository::class);
$this->app->bind(PeriodStatisticRepositoryInterface::class, PeriodStatisticRepository::class);
$this->app->bind(
static function (Application $app): ObjectGroupRepositoryInterface {
@@ -177,6 +176,18 @@ class FireflyServiceProvider extends ServiceProvider
}
);
$this->app->bind(
static function (Application $app): PeriodStatisticRepositoryInterface {
/** @var PeriodStatisticRepository $repository */
$repository = app(PeriodStatisticRepository::class);
if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
$repository->setUser(auth()->user());
}
return $repository;
}
);
$this->app->bind(
static function (Application $app): WebhookRepositoryInterface {
/** @var WebhookRepository $repository */

View File

@@ -358,4 +358,43 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf
return $service->update($category, $data);
}
public function periodCollection(Category $category, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $category->transactionJournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
->where('transaction_journals.date', '>=', $start)
->where('transaction_journals.date', '<=', $end)
->where('transactions.amount', '>', 0)
->get([
// currencies
'transaction_currencies.id as currency_id',
'transaction_currencies.code as currency_code',
'transaction_currencies.name as currency_name',
'transaction_currencies.symbol as currency_symbol',
'transaction_currencies.decimal_places as currency_decimal_places',
// foreign
'foreign_currencies.id as foreign_currency_id',
'foreign_currencies.code as foreign_currency_code',
'foreign_currencies.name as foreign_currency_name',
'foreign_currencies.symbol as foreign_currency_symbol',
'foreign_currencies.decimal_places as foreign_currency_decimal_places',
// fields
'transaction_journals.date',
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()
;
}
}

View File

@@ -48,6 +48,8 @@ interface CategoryRepositoryInterface
public function categoryStartsWith(string $query, int $limit): Collection;
public function periodCollection(Category $category, Carbon $start, Carbon $end): array;
public function destroy(Category $category): bool;
/**

View File

@@ -510,9 +510,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$summarizer->setConvertToPrimary($convertToPrimary);
// filter $journals by range AND currency if it is present.
$expenses = array_filter($expenses, static function (array $expense) use ($category): bool {
return $expense['category_id'] === $category->id;
});
$expenses = array_filter($expenses, static fn (array $expense): bool => $expense['category_id'] === $category->id);
return $summarizer->groupByCurrencyId($expenses, $method, false);
}

View File

@@ -25,12 +25,17 @@ namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Override;
class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface
class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, UserGroupInterface
{
use UserGroupTrait;
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection
{
return $model->primaryPeriodStatistics()
@@ -56,6 +61,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface
$stat = new PeriodStatistic();
$stat->primaryStatable()->associate($model);
$stat->transaction_currency_id = $currencyId;
$stat->user_group_id = $this->getUserGroup()->id;
$stat->start = $start;
$stat->start_tz = $start->format('e');
$stat->end = $end;
@@ -68,7 +74,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface
Log::debug(sprintf(
'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.',
$stat->id,
get_class($model),
$model::class,
$model->id,
$stat->transaction_currency_id,
$stat->start->toW3cString(),
@@ -89,4 +95,42 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface
{
$model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete();
}
#[Override]
public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection
{
return $this->userGroup->periodStatistics()
->where('type', 'LIKE', sprintf('%s%%', $prefix))
->where('start', '>=', $start)->where('end', '<=', $end)->get()
;
}
#[Override]
public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
{
$stat = new PeriodStatistic();
$stat->transaction_currency_id = $currencyId;
$stat->user_group_id = $this->getUserGroup()->id;
$stat->start = $start;
$stat->start_tz = $start->format('e');
$stat->end = $end;
$stat->end_tz = $end->format('e');
$stat->amount = $amount;
$stat->count = $count;
$stat->type = sprintf('%s_%s', $prefix, $type);
$stat->save();
Log::debug(sprintf(
'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.',
$stat->id,
$stat->transaction_currency_id,
$stat->type,
$stat->start->toW3cString(),
$stat->end->toW3cString(),
$count,
$amount
));
return $stat;
}
}

View File

@@ -36,7 +36,11 @@ interface PeriodStatisticRepositoryInterface
public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection;
public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection;
public function deleteStatisticsForModel(Model $model, Carbon $date): void;
}

View File

@@ -67,7 +67,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcsub($currentAmount, $amount);
$pivot->current_amount = bcsub((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -90,7 +90,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcadd($currentAmount, $amount);
$pivot->current_amount = bcadd((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -122,13 +122,13 @@ trait ModifiesPiggyBanks
if (0 !== bccomp($piggyBank->target_amount, '0')) {
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
$leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar);
$maxAmount = 1 === bccomp((string) $leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
Log::debug(sprintf('Left to save: %s', $leftToSave));
Log::debug(sprintf('Maximum amount: %s', $maxAmount));
}
$compare = bccomp($amount, $maxAmount);
$compare = bccomp($amount, (string) $maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
@@ -140,7 +140,7 @@ trait ModifiesPiggyBanks
{
$savedSoFar = $this->getCurrentAmount($piggyBank, $account);
return bccomp($amount, $savedSoFar) <= 0;
return bccomp($amount, (string) $savedSoFar) <= 0;
}
/**
@@ -234,9 +234,9 @@ trait ModifiesPiggyBanks
// if the piggy bank is now smaller than the sum of the money saved,
// remove money from all accounts until the piggy bank is the right amount.
$currentAmount = $this->getCurrentAmount($piggyBank);
if (1 === bccomp($currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
if (1 === bccomp((string) $currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
Log::debug(sprintf('Current amount is %s, target amount is %s', $currentAmount, $piggyBank->target_amount));
$difference = bcsub((string)$piggyBank->target_amount, $currentAmount);
$difference = bcsub((string)$piggyBank->target_amount, (string) $currentAmount);
// an amount will be removed, create "negative" event:
// Log::debug(sprintf('ChangedAmount: is triggered with difference "%s"', $difference));

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Tag;
use Override;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Factory\TagFactory;
@@ -379,4 +380,44 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface
/** @var null|Location */
return $tag->locations()->first();
}
#[Override]
public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $tag->transactionJournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
->where('transaction_journals.date', '>=', $start)
->where('transaction_journals.date', '<=', $end)
->where('transactions.amount', '>', 0)
->get([
// currencies
'transaction_currencies.id as currency_id',
'transaction_currencies.code as currency_code',
'transaction_currencies.name as currency_name',
'transaction_currencies.symbol as currency_symbol',
'transaction_currencies.decimal_places as currency_decimal_places',
// foreign
'foreign_currencies.id as foreign_currency_id',
'foreign_currencies.code as foreign_currency_code',
'foreign_currencies.name as foreign_currency_name',
'foreign_currencies.symbol as foreign_currency_symbol',
'foreign_currencies.decimal_places as foreign_currency_decimal_places',
// fields
'transaction_journals.date',
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()
;
}
}

View File

@@ -51,6 +51,8 @@ interface TagRepositoryInterface
*/
public function destroy(Tag $tag): bool;
public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array;
/**
* Destroy all tags.
*/

View File

@@ -276,66 +276,66 @@ class CreditRecalculateService
if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS.
// Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE.
// Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
@@ -344,7 +344,7 @@ class CreditRecalculateService
if (in_array($type, [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value], true)) {
$usedAmount = app('steam')->negative($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}

View File

@@ -38,7 +38,7 @@ class Timer
public static function getInstance(): self
{
if (null === self::$instance) {
if (!self::$instance instanceof self) {
self::$instance = new self();
}

View File

@@ -33,13 +33,18 @@ use FireflyIII\Models\Category;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Trait PeriodOverview.
@@ -71,9 +76,12 @@ use Illuminate\Support\Facades\Log;
trait PeriodOverview
{
protected AccountRepositoryInterface $accountRepository;
protected CategoryRepositoryInterface $categoryRepository;
protected TagRepositoryInterface $tagRepository;
protected JournalRepositoryInterface $journalRepos;
protected PeriodStatisticRepositoryInterface $periodStatisticRepo;
private Collection $statistics;
private Collection $statistics; // temp data holder
private array $transactions; // temp data holder
/**
* This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range)
@@ -86,6 +94,7 @@ trait PeriodOverview
{
Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u')));
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->accountRepository->setUser($account->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
@@ -95,16 +104,10 @@ trait PeriodOverview
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end);
// TODO needs to be re-arranged:
// get all period stats for entire range.
// loop blocks, an loop the types, and select the missing ones.
// create new ones, or use collected.
$entries = [];
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']);
$entries[] = $this->getSingleModelPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
Log::debug('End of getAccountPeriodOverview()');
@@ -137,68 +140,24 @@ trait PeriodOverview
*/
protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$this->categoryRepository = app(CategoryRepositoryInterface::class);
$this->categoryRepository->setUser($category->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
// properties for entries with their amounts.
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($range);
$cache->addProperty('category-show-period-entries');
$cache->addProperty($category->id);
if ($cache->has()) {
return $cache->get();
}
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end);
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
'title' => $title,
'route' => route(
'categories.show',
[$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
$entries[] = $this->getSingleModelPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
$cache->store($entries);
return $entries;
}
@@ -210,130 +169,151 @@ trait PeriodOverview
*
* @throws FireflyException
*/
protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
protected function getNoModelPeriodOverview(string $model, Carbon $start, Carbon $end): array
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('no-budget-period-entries');
if ($cache->has()) {
return $cache->get();
}
Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d')));
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// get all expenses without a budget.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$journals = $collector->getExtractedJournals();
$dates = Navigation::blockPeriods($start, $end, $range);
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$entries = [];
$this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end);
Log::debug(sprintf('Collected %d stats', $this->statistics->count()));
foreach ($dates as $currentDate) {
$set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($set),
'spent' => $this->groupByCurrency($set),
'earned' => [],
'transferred_away' => [],
'transferred_in' => [],
];
$entries[] = $this->getSingleNoModelPeriodOverview($model, $currentDate['start'], $currentDate['end'], $currentDate['period']);
}
$cache->store($entries);
return $entries;
}
/**
* TODO fix the date.
*
* Show period overview for no category view.
*
* @throws FireflyException
*/
protected function getNoCategoryPeriodOverview(Carbon $theDate): array
private function getSingleNoModelPeriodOverview(string $model, Carbon $start, Carbon $end, string $period): array
{
Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = Navigation::getViewRange(true);
$first = $this->journalRepos->firstNull();
$start = null === $first ? new Carbon() : $first->date;
$end = clone $theDate;
$end = Navigation::endOfPeriod($end, $range);
Log::debug(sprintf('getSingleNoModelPeriodOverview(%s, %s, %s, %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'), $period));
$statistics = $this->filterPrefixedStatistics($start, $end, sprintf('no_%s', $model));
$title = Navigation::periodShow($end, $period);
Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
if (0 === $statistics->count()) {
Log::debug(sprintf('Found no statistics in period %s - %s, regenerating them.', $start->format('Y-m-d'), $end->format('Y-m-d')));
// properties for cache
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
switch ($model) {
default:
throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model));
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
case 'budget':
// get all expenses without a budget.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spent = $collector->getExtractedJournals();
$earned = [];
$transferred = [];
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
break;
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
case 'category':
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earned = $collector->getExtractedJournals();
/** @var array $currentDate */
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spent = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferred = $collector->getExtractedJournals();
break;
}
$groupedSpent = $this->groupByCurrency($spent);
$groupedEarned = $this->groupByCurrency($earned);
$groupedTransferred = $this->groupByCurrency($transferred);
$entry
= [
'title' => $title,
'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => count($spent),
'spent' => $groupedSpent,
'earned' => $groupedEarned,
'transferred' => $groupedTransferred,
];
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent);
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned);
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred);
return $entry;
}
Log::debug('End of loops');
Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d')));
return $entries;
$entry
= [
'title' => $title,
'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => 0,
'spent' => [],
'earned' => [],
'transferred' => [],
];
$grouped = [];
/** @var PeriodStatistic $statistic */
foreach ($statistics as $statistic) {
$type = str_replace(sprintf('no_%s_', $model), '', $statistic->type);
$id = (int)$statistic->transaction_currency_id;
$currency = Amount::getTransactionCurrencyById($id);
$grouped[$type]['count'] ??= 0;
$grouped[$type][$id] = [
'amount' => (string)$statistic->amount,
'count' => (int)$statistic->count,
'currency_id' => $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$grouped[$type]['count'] += (int)$statistic->count;
}
$types = ['spent', 'earned', 'transferred'];
foreach ($types as $type) {
if (array_key_exists($type, $grouped)) {
$entry['total_transactions'] += $grouped[$type]['count'];
unset($grouped[$type]['count']);
$entry[$type] = $grouped[$type];
}
}
return $entry;
}
protected function getSingleAccountPeriod(Account $account, string $period, Carbon $start, Carbon $end): array
protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getSingleAccountPeriod(#%d, %s %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
$types = ['spent', 'earned', 'transferred_in', 'transferred_away'];
$return = [
Log::debug(sprintf('Now in getSingleModelPeriod(%s #%d, %s %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
$types = ['spent', 'earned', 'transferred_in', 'transferred_away'];
$return = [
'title' => Navigation::periodShow($start, $period),
'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]),
'route' => route(sprintf('%s.show', strtolower(Str::plural(class_basename($model)))), [$model->id, $start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => 0,
];
$this->transactions = [];
foreach ($types as $type) {
$set = $this->getSingleAccountPeriodByType($account, $start, $end, $type);
$set = $this->getSingleModelPeriodByType($model, $start, $end, $type);
$return['total_transactions'] += $set['count'];
unset($set['count']);
$return[$type] = $set;
@@ -342,66 +322,96 @@ trait PeriodOverview
return $return;
}
protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection
private function filterStatistics(Carbon $start, Carbon $end, string $type): Collection
{
if (0 === $this->statistics->count()) {
Log::warning('Have no statistic to filter!');
return new Collection();
}
return $this->statistics->filter(
function (PeriodStatistic $statistic) use ($start, $end, $type) {
if (
!$statistic->end->equalTo($end)
&& $statistic->end->format('Y-m-d H:i:s') === $end->format('Y-m-d H:i:s')
) {
echo sprintf('End: "%s" vs "%s": %s', $statistic->end->toW3cString(), $end->toW3cString(), var_export($statistic->end->eq($end), true));
var_dump($statistic->end);
var_dump($end);
exit;
}
return $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type;
}
);
}
protected function getSingleAccountPeriodByType(Account $account, Carbon $start, Carbon $end, string $type): array
private function filterPrefixedStatistics(Carbon $start, Carbon $end, string $prefix): Collection
{
Log::debug(sprintf('Now in getSingleAccountPeriodByType(#%d, %s %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type));
if (0 === $this->statistics->count()) {
Log::warning('Have no statistic to filter!');
return new Collection();
}
return $this->statistics->filter(
function (PeriodStatistic $statistic) use ($start, $end, $prefix) {
return $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix);
}
);
}
private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array
{
Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type));
$statistics = $this->filterStatistics($start, $end, $type);
// nothing found, regenerate them.
if (0 === $statistics->count()) {
Log::debug(sprintf('Found nothing in this period for type "%s"', $type));
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
if (0 === count($this->transactions)) {
switch ($model::class) {
default:
throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model::class));
case Category::class:
$this->transactions = $this->categoryRepository->periodCollection($model, $start, $end);
break;
case Account::class:
$this->transactions = $this->accountRepository->periodCollection($model, $start, $end);
break;
case Tag::class:
$this->transactions = $this->tagRepository->periodCollection($model, $start, $end);
break;
}
}
switch ($type) {
default:
throw new FireflyException(sprintf('Cannot deal with account period type %s', $type));
throw new FireflyException(sprintf('Cannot deal with category period type %s', $type));
case 'spent':
$result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $start, $end);
$result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end);
break;
case 'earned':
$result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $start, $end);
$result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end);
break;
case 'transferred_in':
$result = $this->filterTransfers('in', $transactions, $start, $end);
$result = $this->filterTransfers('in', $start, $end);
break;
case 'transferred_away':
$result = $this->filterTransfers('away', $transactions, $start, $end);
$result = $this->filterTransfers('away', $start, $end);
break;
}
// each result must be grouped by currency, then saved as period statistic.
Log::debug(sprintf('Going to group %d found journal(s)', count($result)));
$grouped = $this->groupByCurrency($result);
$grouped = $this->groupByCurrency($result);
$this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped);
$this->saveGroupedAsStatistics($model, $start, $end, $type, $grouped);
return $grouped;
}
@@ -435,70 +445,23 @@ trait PeriodOverview
*/
protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$this->tagRepository = app(TagRepositoryInterface::class);
$this->tagRepository->setUser($tag->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
// properties for cache
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('tag-period-entries');
$cache->addProperty($tag->id);
if ($cache->has()) {
return $cache->get();
}
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end);
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
// filer all of them:
$earnedSet = $this->filterJournalsByTag($earnedSet, $tag);
$spentSet = $this->filterJournalsByTag($spentSet, $tag);
$transferSet = $this->filterJournalsByTag($transferSet, $tag);
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
'title' => $title,
'route' => route(
'tags.show',
[$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
$entries[] = $this->getSingleModelPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
return $entries;
@@ -565,16 +528,29 @@ trait PeriodOverview
return $entries;
}
protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void
private function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void
{
unset($array['count']);
Log::debug(sprintf('saveGroupedAsStatistics(#%d, %s, %s, "%s", array(%d))', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
foreach ($array as $entry) {
$this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
$this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
}
if (0 === count($array)) {
Log::debug('Save empty statistic.');
$this->periodStatisticRepo->saveStatistic($account, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
$this->periodStatisticRepo->saveStatistic($model, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
}
}
private function saveGroupedForPrefix(string $prefix, Carbon $start, Carbon $end, string $type, array $array): void
{
unset($array['count']);
Log::debug(sprintf('saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', $prefix, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
foreach ($array as $entry) {
$this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
}
if (0 === count($array)) {
Log::debug('Save empty statistic.');
$this->periodStatisticRepo->savePrefixedStatistic($prefix, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
}
}
@@ -595,28 +571,7 @@ trait PeriodOverview
return $result;
}
private function filterJournalsByTag(array $set, Tag $tag): array
{
$return = [];
foreach ($set as $entry) {
$found = false;
/** @var array $localTag */
foreach ($entry['tags'] as $localTag) {
if ($localTag['id'] === $tag->id) {
$found = true;
}
}
if (false === $found) {
continue;
}
$return[] = $entry;
}
return $return;
}
private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array
private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array
{
$result = [];
@@ -624,53 +579,23 @@ trait PeriodOverview
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
// if type is withdrawal, negative amount:
if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string)$item['amount'], '0')) {
$item['amount'] = Steam::negative($item['amount']);
}
$result[] = $item;
unset($transactions[$index]);
}
}
return $result;
}
/**
* Return only transactions where $account is the source.
*/
private function filterTransferredAway(Account $account, array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['source_account_id']) {
$return[] = $journal;
}
}
return $return;
}
/**
* Return only transactions where $account is the source.
*/
private function filterTransferredIn(Account $account, array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['destination_account_id']) {
$return[] = $journal;
}
}
return $return;
}
private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array
private function filterTransfers(string $direction, Carbon $start, Carbon $end): array
{
$result = [];
@@ -678,7 +603,7 @@ trait PeriodOverview
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
@@ -688,8 +613,6 @@ trait PeriodOverview
}
if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
}
}
@@ -703,17 +626,12 @@ trait PeriodOverview
$return = [
'count' => 0,
];
if (0 === count($journals)) {
return $return;
}
/** @var array $journal */
foreach ($journals as $journal) {
if (!array_key_exists('currency_id', $journal)) {
Log::debug('very strange!');
var_dump($journals);
exit;
}
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
@@ -749,7 +667,7 @@ trait PeriodOverview
];
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount);
$return[$currencyId]['amount'] = bcadd((string)$return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
++$return['count'];
}

View File

@@ -135,7 +135,7 @@ class CategoryEnrichment implements EnrichmentInterface
private function collectTransactions(): void
{
if (null !== $this->start && null !== $this->end) {
if ($this->start instanceof Carbon && $this->end instanceof Carbon) {
/** @var OperationsRepositoryInterface $opsRepository */
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);

View File

@@ -142,9 +142,9 @@ class PiggyBankEnrichment implements EnrichmentInterface
'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places),
'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places),
];
$meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']);
$meta['current_amount'] = bcadd($meta['current_amount'], (string) $row['current_amount']);
// only add pc_current_amount when the pc_current_amount is set
$meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']);
$meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd((string) $meta['pc_current_amount'], (string) $row['pc_current_amount']);
}
$meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places);
// only round this number when pc_current_amount is set.
@@ -152,8 +152,8 @@ class PiggyBankEnrichment implements EnrichmentInterface
// calculate left to save, only when there is a target amount.
if (null !== $targetAmount) {
$meta['left_to_save'] = bcsub($meta['target_amount'], $meta['current_amount']);
$meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub($meta['pc_target_amount'], $meta['pc_current_amount']);
$meta['left_to_save'] = bcsub((string) $meta['target_amount'], (string) $meta['current_amount']);
$meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub((string) $meta['pc_target_amount'], (string) $meta['pc_current_amount']);
}
// get suggested per month.
@@ -199,7 +199,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount);
$this->amounts[$id][$accountId]['current_amount'] = bcadd((string) $this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string)$item->native_current_amount);
}

View File

@@ -197,7 +197,7 @@ trait RecalculatesAvailableBudgetsTrait
// if not exists:
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$daily = $this->getDailyAmount($budgetLimit);
$amount = bcmul($daily, (string)$currentPeriod->length(), 12);
$amount = bcmul((string) $daily, (string)$currentPeriod->length(), 12);
// no need to calculate if period is equal.
if ($currentPeriod->equals($limitPeriod)) {

View File

@@ -334,16 +334,16 @@ class BudgetReportGenerator
// make sum information:
$this->report['sums'][$currencyId]
??= [
'budgeted' => '0',
'spent' => '0',
'left' => '0',
'overspent' => '0',
'currency_id' => $currencyId,
'currency_code' => $limitCurrency->code,
'currency_name' => $limitCurrency->name,
'currency_symbol' => $limitCurrency->symbol,
'currency_decimal_places' => $limitCurrency->decimal_places,
];
'budgeted' => '0',
'spent' => '0',
'left' => '0',
'overspent' => '0',
'currency_id' => $currencyId,
'currency_code' => $limitCurrency->code,
'currency_name' => $limitCurrency->name,
'currency_symbol' => $limitCurrency->symbol,
'currency_decimal_places' => $limitCurrency->decimal_places,
];
$this->report['sums'][$currencyId]['budgeted'] = bcadd((string)$this->report['sums'][$currencyId]['budgeted'], $limit->amount);
$this->report['sums'][$currencyId]['spent'] = bcadd((string)$this->report['sums'][$currencyId]['spent'], $spent);
$this->report['sums'][$currencyId]['left'] = bcadd((string)$this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent));

View File

@@ -193,7 +193,7 @@ class TransactionSummarizer
];
// add the data from the $field to the array.
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line
Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum']));
// also do foreign amount, but only when convertToPrimary is false (otherwise we have it already)
@@ -211,7 +211,7 @@ class TransactionSummarizer
'currency_code' => $journal['foreign_currency_code'],
'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
];
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line
}
}

View File

@@ -38,7 +38,7 @@ class PreferencesSingleton
public static function getInstance(): self
{
if (null === self::$instance) {
if (!self::$instance instanceof self) {
self::$instance = new self();
}

View File

@@ -756,7 +756,7 @@ class Steam
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
}
$total = bcadd($current, $total);
$total = bcadd((string) $current, $total);
}
return $total;

View File

@@ -35,7 +35,7 @@ use FireflyIII\Support\Facades\Steam;
*/
class PiggyBankEventTransformer extends AbstractTransformer
{
private TransactionCurrency $primaryCurrency;
private readonly TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
/**

183
composer.lock generated
View File

@@ -6488,16 +6488,16 @@
},
{
"name": "symfony/cache",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "6621a2bee5373e3e972b2ae5dbedd5ac899d8cb6"
"reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/6621a2bee5373e3e972b2ae5dbedd5ac899d8cb6",
"reference": "6621a2bee5373e3e972b2ae5dbedd5ac899d8cb6",
"url": "https://api.github.com/repos/symfony/cache/zipball/bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f",
"reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f",
"shasum": ""
},
"require": {
@@ -6566,7 +6566,7 @@
"psr6"
],
"support": {
"source": "https://github.com/symfony/cache/tree/v7.3.2"
"source": "https://github.com/symfony/cache/tree/v7.3.4"
},
"funding": [
{
@@ -6586,7 +6586,7 @@
"type": "tidelift"
}
],
"time": "2025-07-30T17:13:41+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -6740,16 +6740,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
@@ -6814,7 +6814,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.3"
"source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@@ -6834,7 +6834,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/css-selector",
@@ -6970,16 +6970,16 @@
},
{
"name": "symfony/error-handler",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3"
"reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3",
"reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
"reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
"shasum": ""
},
"require": {
@@ -7027,7 +7027,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v7.3.2"
"source": "https://github.com/symfony/error-handler/tree/v7.3.4"
},
"funding": [
{
@@ -7047,7 +7047,7 @@
"type": "tidelift"
}
],
"time": "2025-07-07T08:17:57+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -7347,16 +7347,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019"
"reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019",
"reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019",
"url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62",
"reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62",
"shasum": ""
},
"require": {
@@ -7423,7 +7423,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.3"
"source": "https://github.com/symfony/http-client/tree/v7.3.4"
},
"funding": [
{
@@ -7443,7 +7443,7 @@
"type": "tidelift"
}
],
"time": "2025-08-27T07:45:05+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -7525,16 +7525,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00"
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00",
"reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6",
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6",
"shasum": ""
},
"require": {
@@ -7584,7 +7584,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.3.3"
"source": "https://github.com/symfony/http-foundation/tree/v7.3.4"
},
"funding": [
{
@@ -7604,20 +7604,20 @@
"type": "tidelift"
}
],
"time": "2025-08-20T08:04:18+00:00"
"time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b"
"reference": "b796dffea7821f035047235e076b60ca2446e3cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b",
"reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf",
"reference": "b796dffea7821f035047235e076b60ca2446e3cf",
"shasum": ""
},
"require": {
@@ -7702,7 +7702,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.3.3"
"source": "https://github.com/symfony/http-kernel/tree/v7.3.4"
},
"funding": [
{
@@ -7722,20 +7722,20 @@
"type": "tidelift"
}
],
"time": "2025-08-29T08:23:45+00:00"
"time": "2025-09-27T12:32:17+00:00"
},
{
"name": "symfony/mailer",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575"
"reference": "ab97ef2f7acf0216955f5845484235113047a31d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575",
"reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575",
"url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d",
"reference": "ab97ef2f7acf0216955f5845484235113047a31d",
"shasum": ""
},
"require": {
@@ -7786,7 +7786,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v7.3.3"
"source": "https://github.com/symfony/mailer/tree/v7.3.4"
},
"funding": [
{
@@ -7806,7 +7806,7 @@
"type": "tidelift"
}
],
"time": "2025-08-13T11:49:31+00:00"
"time": "2025-09-17T05:51:54+00:00"
},
{
"name": "symfony/mailgun-mailer",
@@ -7879,16 +7879,16 @@
},
{
"name": "symfony/mime",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1"
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1",
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1",
"url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
"shasum": ""
},
"require": {
@@ -7943,7 +7943,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.3.2"
"source": "https://github.com/symfony/mime/tree/v7.3.4"
},
"funding": [
{
@@ -7963,7 +7963,7 @@
"type": "tidelift"
}
],
"time": "2025-07-15T13:41:35+00:00"
"time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8867,16 +8867,16 @@
},
{
"name": "symfony/process",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@@ -8908,7 +8908,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.3.3"
"source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@@ -8928,7 +8928,7 @@
"type": "tidelift"
}
],
"time": "2025-08-18T09:42:54+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -9015,16 +9015,16 @@
},
{
"name": "symfony/routing",
"version": "v7.3.2",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4"
"reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4",
"reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4",
"url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c",
"reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c",
"shasum": ""
},
"require": {
@@ -9076,7 +9076,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v7.3.2"
"source": "https://github.com/symfony/routing/tree/v7.3.4"
},
"funding": [
{
@@ -9096,7 +9096,7 @@
"type": "tidelift"
}
],
"time": "2025-07-15T11:36:08+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/service-contracts",
@@ -9183,16 +9183,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
"reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
"reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@@ -9207,7 +9207,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -9250,7 +9249,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.3"
"source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -9270,20 +9269,20 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-11T14:36:48+00:00"
},
{
"name": "symfony/translation",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "e0837b4cbcef63c754d89a4806575cada743a38d"
"reference": "ec25870502d0c7072d086e8ffba1420c85965174"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d",
"reference": "e0837b4cbcef63c754d89a4806575cada743a38d",
"url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174",
"reference": "ec25870502d0c7072d086e8ffba1420c85965174",
"shasum": ""
},
"require": {
@@ -9350,7 +9349,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.3.3"
"source": "https://github.com/symfony/translation/tree/v7.3.4"
},
"funding": [
{
@@ -9370,7 +9369,7 @@
"type": "tidelift"
}
],
"time": "2025-08-01T21:02:37+00:00"
"time": "2025-09-07T11:39:36+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -9526,16 +9525,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f"
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"shasum": ""
},
"require": {
@@ -9589,7 +9588,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.3"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
},
"funding": [
{
@@ -9609,20 +9608,20 @@
"type": "tidelift"
}
],
"time": "2025-08-13T11:49:31+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/var-exporter",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "d4dfcd2a822cbedd7612eb6fbd260e46f87b7137"
"reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/d4dfcd2a822cbedd7612eb6fbd260e46f87b7137",
"reference": "d4dfcd2a822cbedd7612eb6fbd260e46f87b7137",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
"reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
"shasum": ""
},
"require": {
@@ -9670,7 +9669,7 @@
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v7.3.3"
"source": "https://github.com/symfony/var-exporter/tree/v7.3.4"
},
"funding": [
{
@@ -9690,7 +9689,7 @@
"type": "tidelift"
}
],
"time": "2025-08-18T13:10:53+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -11893,16 +11892,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.3.14",
"version": "12.3.15",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "13e9b2bea9327b094176147250d2c10319a10f5b"
"reference": "b035ee2cd8ecad4091885b61017ebb1d80eb0e57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/13e9b2bea9327b094176147250d2c10319a10f5b",
"reference": "13e9b2bea9327b094176147250d2c10319a10f5b",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b035ee2cd8ecad4091885b61017ebb1d80eb0e57",
"reference": "b035ee2cd8ecad4091885b61017ebb1d80eb0e57",
"shasum": ""
},
"require": {
@@ -11916,7 +11915,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.3.8",
"phpunit/php-code-coverage": "^12.4.0",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@@ -11970,7 +11969,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.14"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.15"
},
"funding": [
{
@@ -11994,7 +11993,7 @@
"type": "tidelift"
}
],
"time": "2025-09-24T06:34:27+00:00"
"time": "2025-09-28T12:10:54+00:00"
},
{
"name": "rector/rector",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-09-26',
'build_time' => 1758908498,
'version' => 'develop/2025-09-29',
'build_time' => 1759116036,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 27,

View File

@@ -14,7 +14,10 @@ return new class extends Migration
Schema::create('period_statistics', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->softDeletes();
// reference to user group id.
$table->bigInteger('user_group_id', false, true);
$table->integer('primary_statable_id', false, true)->nullable();
$table->string('primary_statable_type', 255)->nullable();
@@ -34,6 +37,7 @@ return new class extends Migration
$table->string('type',255);
$table->integer('count', false, true)->default(0);
$table->decimal('amount', 32, 12);
$table->foreign('user_group_id')->references('id')->on('user_groups')->onDelete('cascade');
});
}

View File

@@ -80,6 +80,7 @@ class TransactionCurrencySeeder extends Seeder
$currencies[] = ['code' => 'CHF', 'name' => 'Swiss franc', 'symbol' => 'CHF', 'decimal_places' => 2];
$currencies[] = ['code' => 'NOK', 'name' => 'Norwegian krone', 'symbol' => 'kr.', 'decimal_places' => 2];
$currencies[] = ['code' => 'CZK', 'name' => 'Czech koruna', 'symbol' => 'Kč', 'decimal_places' => 2];
$currencies[] = ['code' => 'KZT', 'name' => 'Kazakhstani tenge', 'symbol' => '₸', 'decimal_places' => 2];
foreach ($currencies as $currency) {
if (null === TransactionCurrency::where('code', $currency['code'])->first()) {

200
package-lock.json generated
View File

@@ -2589,9 +2589,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz",
"integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
"integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==",
"cpu": [
"arm"
],
@@ -2603,9 +2603,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz",
"integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz",
"integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==",
"cpu": [
"arm64"
],
@@ -2617,9 +2617,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz",
"integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz",
"integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==",
"cpu": [
"arm64"
],
@@ -2631,9 +2631,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz",
"integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz",
"integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==",
"cpu": [
"x64"
],
@@ -2645,9 +2645,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz",
"integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz",
"integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==",
"cpu": [
"arm64"
],
@@ -2659,9 +2659,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz",
"integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz",
"integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==",
"cpu": [
"x64"
],
@@ -2673,9 +2673,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz",
"integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz",
"integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==",
"cpu": [
"arm"
],
@@ -2687,9 +2687,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz",
"integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz",
"integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==",
"cpu": [
"arm"
],
@@ -2701,9 +2701,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz",
"integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz",
"integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==",
"cpu": [
"arm64"
],
@@ -2715,9 +2715,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz",
"integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz",
"integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==",
"cpu": [
"arm64"
],
@@ -2729,9 +2729,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz",
"integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz",
"integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==",
"cpu": [
"loong64"
],
@@ -2743,9 +2743,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz",
"integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz",
"integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==",
"cpu": [
"ppc64"
],
@@ -2757,9 +2757,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz",
"integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz",
"integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==",
"cpu": [
"riscv64"
],
@@ -2771,9 +2771,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz",
"integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz",
"integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==",
"cpu": [
"riscv64"
],
@@ -2785,9 +2785,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz",
"integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz",
"integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==",
"cpu": [
"s390x"
],
@@ -2799,9 +2799,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz",
"integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
"cpu": [
"x64"
],
@@ -2813,9 +2813,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz",
"integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
"cpu": [
"x64"
],
@@ -2827,9 +2827,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz",
"integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz",
"integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==",
"cpu": [
"arm64"
],
@@ -2841,9 +2841,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz",
"integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz",
"integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==",
"cpu": [
"arm64"
],
@@ -2855,9 +2855,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz",
"integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz",
"integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==",
"cpu": [
"ia32"
],
@@ -2869,9 +2869,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz",
"integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
"integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==",
"cpu": [
"x64"
],
@@ -2883,9 +2883,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz",
"integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz",
"integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==",
"cpu": [
"x64"
],
@@ -4075,9 +4075,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz",
"integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5736,9 +5736,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.224",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz",
"integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==",
"version": "1.5.227",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
"integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==",
"dev": true,
"license": "ISC"
},
@@ -5820,9 +5820,9 @@
}
},
"node_modules/envinfo": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.15.0.tgz",
"integrity": "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g==",
"dev": true,
"license": "MIT",
"bin": {
@@ -10130,9 +10130,9 @@
}
},
"node_modules/rollup": {
"version": "4.52.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz",
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10146,28 +10146,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.2",
"@rollup/rollup-android-arm64": "4.52.2",
"@rollup/rollup-darwin-arm64": "4.52.2",
"@rollup/rollup-darwin-x64": "4.52.2",
"@rollup/rollup-freebsd-arm64": "4.52.2",
"@rollup/rollup-freebsd-x64": "4.52.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.2",
"@rollup/rollup-linux-arm-musleabihf": "4.52.2",
"@rollup/rollup-linux-arm64-gnu": "4.52.2",
"@rollup/rollup-linux-arm64-musl": "4.52.2",
"@rollup/rollup-linux-loong64-gnu": "4.52.2",
"@rollup/rollup-linux-ppc64-gnu": "4.52.2",
"@rollup/rollup-linux-riscv64-gnu": "4.52.2",
"@rollup/rollup-linux-riscv64-musl": "4.52.2",
"@rollup/rollup-linux-s390x-gnu": "4.52.2",
"@rollup/rollup-linux-x64-gnu": "4.52.2",
"@rollup/rollup-linux-x64-musl": "4.52.2",
"@rollup/rollup-openharmony-arm64": "4.52.2",
"@rollup/rollup-win32-arm64-msvc": "4.52.2",
"@rollup/rollup-win32-ia32-msvc": "4.52.2",
"@rollup/rollup-win32-x64-gnu": "4.52.2",
"@rollup/rollup-win32-x64-msvc": "4.52.2",
"@rollup/rollup-android-arm-eabi": "4.52.3",
"@rollup/rollup-android-arm64": "4.52.3",
"@rollup/rollup-darwin-arm64": "4.52.3",
"@rollup/rollup-darwin-x64": "4.52.3",
"@rollup/rollup-freebsd-arm64": "4.52.3",
"@rollup/rollup-freebsd-x64": "4.52.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.3",
"@rollup/rollup-linux-arm-musleabihf": "4.52.3",
"@rollup/rollup-linux-arm64-gnu": "4.52.3",
"@rollup/rollup-linux-arm64-musl": "4.52.3",
"@rollup/rollup-linux-loong64-gnu": "4.52.3",
"@rollup/rollup-linux-ppc64-gnu": "4.52.3",
"@rollup/rollup-linux-riscv64-gnu": "4.52.3",
"@rollup/rollup-linux-riscv64-musl": "4.52.3",
"@rollup/rollup-linux-s390x-gnu": "4.52.3",
"@rollup/rollup-linux-x64-gnu": "4.52.3",
"@rollup/rollup-linux-x64-musl": "4.52.3",
"@rollup/rollup-openharmony-arm64": "4.52.3",
"@rollup/rollup-win32-arm64-msvc": "4.52.3",
"@rollup/rollup-win32-ia32-msvc": "4.52.3",
"@rollup/rollup-win32-x64-gnu": "4.52.3",
"@rollup/rollup-win32-x64-msvc": "4.52.3",
"fsevents": "~2.3.2"
}
},

View File

@@ -164,7 +164,7 @@
"title": "Titel",
"date": "Datum",
"book_date": "Buchungsdatum",
"process_date": "Bearbeitungsdatum",
"process_date": "Wertstellungsdatum",
"due_date": "F\u00e4lligkeitstermin",
"foreign_amount": "Ausl\u00e4ndischer Betrag",
"payment_date": "Zahlungsdatum",