Compare commits

...

40 Commits

Author SHA1 Message Date
github-actions[bot]
be6801654f Merge pull request #11555 from firefly-iii/release-1768805748
🤖 Automatically merge the PR into the develop branch.
2026-01-19 07:55:56 +01:00
JC5
93fe1f5558 🤖 Auto commit for release 'develop' on 2026-01-19 2026-01-19 07:55:48 +01:00
James Cole
ba5b6ff694 Merge pull request #11550 from firefly-iii/dependabot/composer/develop/larastan/larastan-3.9.0 2026-01-19 07:02:34 +01:00
dependabot[bot]
492aec0652 Bump larastan/larastan from 3.8.1 to 3.9.0
Bumps [larastan/larastan](https://github.com/larastan/larastan) from 3.8.1 to 3.9.0.
- [Release notes](https://github.com/larastan/larastan/releases)
- [Changelog](https://github.com/larastan/larastan/blob/3.x/RELEASE.md)
- [Commits](https://github.com/larastan/larastan/compare/v3.8.1...v3.9.0)

---
updated-dependencies:
- dependency-name: larastan/larastan
  dependency-version: 3.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 04:09:13 +00:00
James Cole
c81bb44552 Fix #11546 2026-01-18 13:32:05 +01:00
James Cole
0431c95b49 Move rule actions for rule events. #11544 2026-01-18 12:13:11 +01:00
James Cole
6408db5414 Remove unused class #11544 2026-01-18 12:08:05 +01:00
James Cole
5de88a70f8 Remove piggy bank event handler and replace name event. #11544 2026-01-18 12:07:03 +01:00
James Cole
968da83c67 Migrate PiggyBankAmountIsChanged handler to listener #11544 2026-01-18 12:00:41 +01:00
James Cole
e77b4ee4a6 Rename to PiggyBankAmountIsChanged for #11544 2026-01-18 11:58:20 +01:00
James Cole
a36c775bd2 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-18 11:57:23 +01:00
James Cole
7773862eb9 Remove unused class. #11544 2026-01-18 11:57:17 +01:00
github-actions[bot]
ae107b1776 Merge pull request #11545 from firefly-iii/release-1768729993
🤖 Automatically merge the PR into the develop branch.
2026-01-18 10:53:20 +01:00
JC5
d1b167447b 🤖 Auto commit for release 'develop' on 2026-01-18 2026-01-18 10:53:13 +01:00
James Cole
a08ba43c23 For multiple subscriptions payment missed warning #11544 2026-01-18 10:17:50 +01:00
James Cole
64325fbce7 Remove unused class #11544 2026-01-18 10:12:39 +01:00
James Cole
aeac59e851 Subscription reminder for extension or ending. https://github.com/firefly-iii/firefly-iii/issues/11544 2026-01-18 10:11:26 +01:00
James Cole
151be7df8a Fix #11541 2026-01-18 06:07:50 +01:00
github-actions[bot]
040bd0f6e3 Merge pull request #11540 from firefly-iii/release-1768662669
🤖 Automatically merge the PR into the develop branch.
2026-01-17 16:11:15 +01:00
JC5
a859ca4e38 🤖 Auto commit for release 'develop' on 2026-01-17 2026-01-17 16:11:09 +01:00
James Cole
95c62d783a Fix https://github.com/orgs/firefly-iii/discussions/11538 2026-01-17 16:02:59 +01:00
github-actions[bot]
e7b67bc85e Merge pull request #11536 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2026-01-17 08:54:06 +01:00
github-actions[bot]
208f13ee75 Merge pull request #11535 from firefly-iii/release-1768636434
🤖 Automatically merge the PR into the develop branch.
2026-01-17 08:54:01 +01:00
JC5
437eecc1c9 🤖 Auto commit for release 'v6.4.16' on 2026-01-17 2026-01-17 08:53:54 +01:00
James Cole
1d34d81389 Remove double line from changelog. 2026-01-17 07:22:40 +01:00
github-actions[bot]
30844e99d4 Merge pull request #11534 from firefly-iii/release-1768630688
🤖 Automatically merge the PR into the develop branch.
2026-01-17 07:18:14 +01:00
JC5
d734449f63 🤖 Auto commit for release 'develop' on 2026-01-17 2026-01-17 07:18:08 +01:00
James Cole
69a9e3a198 Clean up account deletion and balance recalculation. 2026-01-17 07:12:14 +01:00
James Cole
3ad2f8c750 Expand changelog. 2026-01-17 07:03:29 +01:00
James Cole
2166839768 Fix #11475 2026-01-17 07:03:18 +01:00
James Cole
8ecb9d7774 Fix #11508 2026-01-17 06:58:33 +01:00
James Cole
8814fb0806 Fix https://github.com/orgs/firefly-iii/discussions/11509 2026-01-16 09:02:37 +01:00
James Cole
7481c8d4c0 Fix https://github.com/firefly-iii/firefly-iii/issues/11531 2026-01-16 07:34:20 +01:00
James Cole
1e618fbf6d Sort alphabetically https://github.com/orgs/firefly-iii/discussions/11524 2026-01-15 05:59:33 +01:00
James Cole
8b322dc903 Sort alphabetically https://github.com/orgs/firefly-iii/discussions/11524 2026-01-15 05:56:18 +01:00
github-actions[bot]
812b0e6940 Merge pull request #11520 from firefly-iii/release-1768367445
🤖 Automatically merge the PR into the develop branch.
2026-01-14 06:10:53 +01:00
JC5
9070856b9c 🤖 Auto commit for release 'develop' on 2026-01-14 2026-01-14 06:10:45 +01:00
James Cole
84082d38f2 Ignore AppServiceProvider 2026-01-14 06:05:34 +01:00
James Cole
822352827a Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-14 06:00:47 +01:00
James Cole
4c95ab49f4 Cannot be static. 2026-01-14 05:56:08 +01:00
43 changed files with 566 additions and 480 deletions

View File

@@ -14,7 +14,15 @@ SITE_OWNER=mail@example.com
# Change it to a string of exactly 32 chars or use something like `php artisan key:generate` to generate it.
# If you use Docker or similar, you can set this variable from a file by using APP_KEY_FILE
#
# Avoid the "#" character in your APP_KEY, it may break things.
# Try to avoid special characters like #, < and > in your app key. This string does not need full entropy
# When in doubt, follow the link below and pick one.
#
# https://www.random.org/strings/?num=5&len=32&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
#
# If you are a fancy linux nerd like me, use this command:
#
# head /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 32 && echo
#
#
APP_KEY=SomeRandomStringOf32CharsExactly

View File

@@ -82,6 +82,7 @@ class PiggyBankController extends Controller
'currency_decimal_places' => $currency->decimal_places,
'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id,
'object_group_title' => $objectGroup?->title,
'object_group_order' => $objectGroup?->order,
];
}

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Support\System\OAuthKeys;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class RestoresOAuthKeys extends Command
{
@@ -40,7 +41,9 @@ class RestoresOAuthKeys extends Command
*/
public function handle(): int
{
Log::debug('Restore OAuth Keys command.');
$this->restoreOAuthKeys();
Log::debug('Done with OAuth Keys command.');
return 0;
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* Updated.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Account;
use FireflyIII\Models\Account;
use Illuminate\Queue\SerializesModels;
class Updated
{
use SerializesModels;
public function __construct(public Account $account) {}
}

View File

@@ -34,7 +34,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class ChangedAmount
*/
class ChangedAmount extends Event
class PiggyBankAmountIsChanged extends Event
{
use SerializesModels;

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
/*
* ChangedName.php
* Copyright (c) 2025 james@firefly-iii.org
* PiggyBankNameIsChanged.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -21,15 +21,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\PiggyBank;
use FireflyIII\Events\Event;
use FireflyIII\Models\PiggyBank;
use Illuminate\Queue\SerializesModels;
class ChangedName extends Event
/**
* Needs to be an event because system needs old value as well as the new value.
*/
class PiggyBankNameIsChanged extends Event
{
use SerializesModels;

View File

@@ -1,10 +1,8 @@
<?php
/*
* WarnUserAboutBill.php
* Copyright (c) 2025 james@firefly-iii.org
* SubscriptionNeedsExtensionOrRenewal.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -24,18 +22,15 @@
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
namespace FireflyIII\Events\Model\Subscription;
use FireflyIII\Events\Event;
use FireflyIII\Models\Bill;
use Illuminate\Queue\SerializesModels;
/**
* Class WarnUserAboutBill.
*/
class WarnUserAboutBill extends Event
class SubscriptionNeedsExtensionOrRenewal extends Event
{
use SerializesModels;
public function __construct(public Bill $bill, public string $field, public int $diff) {}
public function __construct(public Bill $subscription, public string $field, public int $diff) {}
}

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
/*
* WarnUserAboutOverdueSubscriptions.php
* Copyright (c) 2025 james@firefly-iii.org
* SubscriptionIsOverdueForPayment.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -21,18 +21,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
namespace FireflyIII\Events\Model\Subscription;
use FireflyIII\Events\Event;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
class WarnUserAboutOverdueSubscriptions extends Event
class SubscriptionsAreOverdueForPayment extends Event
{
use SerializesModels;
public function __construct(public User $user, public array $overdue) {}
}

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Factory;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\PiggyBankAmountIsChanged;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\ObjectGroup;
@@ -305,7 +305,7 @@ class PiggyBankFactory
if (0 !== bccomp($oldSavedAmount, $newSavedAmount)) {
Log::debug('Amount changed, will create event for it.');
// create event for difference.
event(new ChangedAmount($piggyBank, bcsub($newSavedAmount, $oldSavedAmount), null, null));
event(new PiggyBankAmountIsChanged($piggyBank, bcsub($newSavedAmount, $oldSavedAmount), null, null));
}
}
if (0 === count($toBeLinked)) {

View File

@@ -28,6 +28,7 @@ use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -61,6 +62,9 @@ class DestroyedGroupEventHandler
private function updateRunningBalance(DestroyedTransactionGroup $event): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
Log::debug(__METHOD__);
$group = $event->transactionGroup;
foreach ($group->transactionJournals as $journal) {

View File

@@ -34,6 +34,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Support\Collection;
@@ -217,6 +218,9 @@ class UpdatedGroupEventHandler
private function updateRunningBalance(UpdatedTransactionGroup $event): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
Log::debug(__METHOD__);
$group = $event->transactionGroup;
foreach ($group->transactionJournals as $journal) {

View File

@@ -25,7 +25,9 @@ namespace FireflyIII\Handlers\Observer;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
@@ -48,13 +50,37 @@ class TransactionJournalObserver
}
});
// delete all links:
TransactionJournalLink::where('source_id', $transactionJournal->id)->delete();
TransactionJournalLink::where('destination_id', $transactionJournal->id)->delete();
// update events
// TODO move to repository
$transactionJournal->piggyBankEvents()->update(['transaction_journal_id' => null]);
// delete all from 'budget_transaction_journal'
DB::table('budget_transaction_journal')->where('transaction_journal_id', $transactionJournal->id)->delete();
// delete all from 'category_transaction_journal'
DB::table('category_transaction_journal')->where('transaction_journal_id', $transactionJournal->id)->delete();
// delete all from 'tag_transaction_journal'
DB::table('tag_transaction_journal')->where('transaction_journal_id', $transactionJournal->id)->delete();
/** @var Attachment $attachment */
foreach ($transactionJournal->attachments()->get() as $attachment) {
$repository->destroy($attachment);
}
$transactionJournal->transactionJournalMeta()->delete();
$transactionJournal->locations()->delete();
$transactionJournal->notes()->delete();
$transactionJournal->sourceJournalLinks()->delete();
$transactionJournal->destJournalLinks()->delete();
$transactionJournal->auditLogEntries()->delete();
// set all transactions AFTER this one to balance_dirty for recalc.
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Bill;
use FireflyIII\Support\Facades\Navigation;
use Illuminate\Support\Facades\Log;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\OrganisesObjectGroups;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
use FireflyIII\Transformers\BillTransformer;
use FireflyIII\User;
@@ -38,6 +37,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
@@ -82,22 +82,18 @@ class IndexController extends Controller
$parameters = new ParameterBag();
// sub one day from temp start so the last paid date is one day before it should be.
$tempStart = clone $start;
// 2023-06-23 do not sub one day from temp start, fix is in BillTransformer::payDates instead
// $tempStart->subDay();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new SubscriptionEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($tempStart);
$enrichment->setStart($start->clone());
$enrichment->setEnd($end);
$collection = $enrichment->enrich($collection);
$parameters->set('start', $tempStart);
$parameters->set('start', $start->clone());
$parameters->set('end', $end);
$parameters->set('convertToPrimary', $this->convertToPrimary);
$parameters->set('primaryCurrency', $this->primaryCurrency);
@@ -152,16 +148,28 @@ class IndexController extends Controller
private function getSums(array $bills): array
{
Log::debug(sprintf('now in getSums(count:%d)', count($bills)));
$sums = [];
$range = Navigation::getViewRange(true);
/** @var array $group */
foreach ($bills as $groupOrder => $group) {
Log::debug(sprintf('Summing up group "%s"', $group['object_group_title']));
if (0 === count($group['bills'])) {
Log::debug('Group has no subscriptions, continue');
continue;
}
Log::debug(sprintf('Group has %d subscription(s)', count($group['bills'])));
/** @var array $bill */
foreach ($group['bills'] as $bill) {
if (false === $bill['active']) {
Log::debug(sprintf('Skip subscription #%d, inactive.', $bill['id']));
continue;
}
Log::debug(sprintf('Now at subscription #%d.', $bill['id']));
$currencyId = $bill['currency_id'];
$sums[$groupOrder][$currencyId] ??= [
@@ -175,26 +183,32 @@ class IndexController extends Controller
'period' => $range,
'per_period' => '0',
];
Log::debug(sprintf('Start with avg:%s, total_left_to_pay:%s, per_period:%s', $sums[$groupOrder][$currencyId]['avg'], $sums[$groupOrder][$currencyId]['total_left_to_pay'], $sums[$groupOrder][$currencyId]['per_period']));
// only fill in avg when bill is active.
if (null !== $bill['next_expected_match']) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)count($bill['pay_dates']));
$sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg);
}
// only fill in total_left_to_pay when bill is not yet paid.
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)$count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
Log::debug(sprintf('next expected match is "%s", avg is now %s', $bill['next_expected_match'], $sums[$groupOrder][$currencyId]['avg']));
// only fill in total_left_to_pay when bill is not yet paid.
// #11474 and when it is expected in the current period
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)$count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
Log::debug(sprintf('Bill has %d dates that need payment, total left to pay is now %s', $count, $sums[$groupOrder][$currencyId]['total_left_to_pay']), $bill['pay_dates']);
}
}
}
$perPeriod = $this->amountPerPeriod($bill, $range);
Log::debug(sprintf('Add amount %s to per_period', $perPeriod));
// fill in per period regardless:
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $this->amountPerPeriod($bill, $range));
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $perPeriod);
}
}

View File

@@ -113,7 +113,9 @@ class DebugController extends Controller
// also do some recalculations.
Artisan::call('correction:recalculates-liabilities');
AccountBalanceCalculator::recalculateAll(false);
if (true === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
AccountBalanceCalculator::recalculateAll(false);
}
try {
Artisan::call('twig:clean');

View File

@@ -25,8 +25,8 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Events\Model\Subscription\SubscriptionNeedsExtensionOrRenewal;
use FireflyIII\Events\Model\Subscription\SubscriptionsAreOverdueForPayment;
use FireflyIII\Models\Bill;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
@@ -143,7 +143,7 @@ class WarnAboutBills implements ShouldQueue
{
$diff = $this->getDiff($bill, $field);
Log::debug('Will now send warning!');
event(new WarnUserAboutBill($bill, $field, $diff));
event(new SubscriptionNeedsExtensionOrRenewal($bill, $field, $diff));
}
public function setDate(Carbon $date): void
@@ -197,8 +197,8 @@ class WarnAboutBills implements ShouldQueue
private function sendOverdueAlerts(User $user, array $overdue): void
{
if (count($overdue) > 0) {
Log::debug(sprintf('Will now send warning about overdue bill for user #%d.', $user->id));
event(new WarnUserAboutOverdueSubscriptions($user, $overdue));
Log::debug(sprintf('Will now send warning about overdue subscription(s) for user #%d.', $user->id));
event(new SubscriptionsAreOverdueForPayment($user, $overdue));
}
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/*
* PiggyBankEventHandler.php
* Copyright (c) 2023 james@firefly-iii.org
* CreatesPiggyBankEventForChangedAmount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,43 +21,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Listeners\Model\PiggyBank;
namespace FireflyIII\Handlers\Events\Model;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\ChangedName;
use FireflyIII\Models\Account;
use FireflyIII\Events\Model\PiggyBank\PiggyBankAmountIsChanged;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleAction;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
/**
* Class PiggyBankEventHandler
*/
class PiggyBankEventHandler
class CreatesPiggyBankEventForChangedAmount implements ShouldQueue
{
public function changedPiggyBankName(ChangedName $event): void
{
// loop all accounts, collect all user's rules.
/** @var Account $account */
foreach ($event->piggyBank->accounts as $account) {
/** @var Rule $rule */
foreach ($account->user->rules as $rule) {
/** @var RuleAction $ruleAction */
foreach ($rule->ruleActions()->where('action_type', 'update_piggy')->get() as $ruleAction) {
if ($event->oldName === $ruleAction->action_value) {
$ruleAction->action_value = $event->newName;
$ruleAction->save();
}
}
}
}
}
public function changePiggyAmount(ChangedAmount $event): void
public function handle(PiggyBankAmountIsChanged $event): void
{
// find journal if group is present.
$journal = $event->transactionJournal;

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* UpdatesRulesForChangedPiggyBankName.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Listeners\Model\PiggyBank;
use FireflyIII\Events\Model\PiggyBank\PiggyBankNameIsChanged;
use FireflyIII\Models\Account;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleAction;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdatesRulesForChangedPiggyBankName implements ShouldQueue
{
public function handle(PiggyBankNameIsChanged $event): void
{
// loop all accounts, collect all user's rules.
/** @var Account $account */
foreach ($event->piggyBank->accounts as $account) {
/** @var Rule $rule */
foreach ($account->user->rules as $rule) {
/** @var RuleAction $ruleAction */
foreach ($rule->ruleActions()->where('action_type', 'update_piggy')->get() as $ruleAction) {
if ($event->oldName === $ruleAction->action_value) {
$ruleAction->action_value = $event->newName;
$ruleAction->save();
}
}
}
}
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/*
* RuleHandler.php
* Copyright (c) 2023 james@firefly-iii.org
* NotifiesUserAboutFailedRuleAction.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,24 +21,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events\Model;
namespace FireflyIII\Listeners\Model\Rule;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject;
use FireflyIII\Notifications\User\RuleActionFailed;
use FireflyIII\Support\Facades\Preferences;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
/**
* Class RuleHandler
*/
class RuleHandler
class NotifiesUserAboutFailedRuleAction implements ShouldQueue
{
public function ruleActionFailedOnArray(RuleActionFailedOnArray $event): void
public function handle(RuleActionFailedOnArray|RuleActionFailedOnObject $event): void
{
$ruleAction = $event->ruleAction;
$rule = $ruleAction->rule;
@@ -52,9 +49,12 @@ class RuleHandler
$error = $event->error;
$user = $ruleAction->rule->user;
$mainMessage = trans('rules.main_message', ['rule' => $rule->title, 'action' => $ruleAction->action_type, 'group' => $journal['transaction_group_id'], 'error' => $error]);
$groupTitle = $journal['description'] ?? '';
$groupLink = route('transactions.show', [$journal['transaction_group_id']]);
$groupId = is_array($journal) ? $journal['transaction_group_id'] : $journal->transaction_group_id;
$groupTitle = is_array($journal) ? ($journal['description'] ?? '') : ($journal->description ?? '');
$mainMessage = trans('rules.main_message', ['rule' => $rule->title, 'action' => $ruleAction->action_type, 'group' => $groupId, 'error' => $error]);
$groupLink = route('transactions.show', [$groupId]);
$ruleTitle = $rule->title;
$ruleLink = route('rules.edit', [$rule->id]);
$params = [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink];
@@ -65,33 +65,4 @@ class RuleHandler
Log::error(sprintf('[a] Error sending notification that the rule action failed: %s', $e->getMessage()));
}
}
public function ruleActionFailedOnObject(RuleActionFailedOnObject $event): void
{
$ruleAction = $event->ruleAction;
$rule = $ruleAction->rule;
/** @var bool $preference */
$preference = Preferences::getForUser($rule->user, 'notification_rule_action_failures', true)->data;
if (false === $preference) {
return;
}
Log::debug('Now in ruleActionFailedOnObject');
$journal = $event->journal;
$error = $event->error;
$user = $ruleAction->rule->user;
$mainMessage = trans('rules.main_message', ['rule' => $rule->title, 'action' => $ruleAction->action_type, 'group' => $journal->transaction_group_id, 'error' => $error]);
$groupTitle = $journal->description ?? '';
$groupLink = route('transactions.show', [$journal->transaction_group_id]);
$ruleTitle = $rule->title;
$ruleLink = route('rules.edit', [$rule->id]);
$params = [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink];
try {
Notification::send($user, new RuleActionFailed($params));
} catch (ClientException $e) {
Log::error(sprintf('[b] Error sending notification that the rule action failed: %s', $e->getMessage()));
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* NotifiesAboutExtensionOrRenewal.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Listeners\Model\Subscription;
use Exception;
use FireflyIII\Events\Model\Subscription\SubscriptionNeedsExtensionOrRenewal;
use FireflyIII\Notifications\User\BillReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesAboutExtensionOrRenewal implements ShouldQueue
{
public function handle(SubscriptionNeedsExtensionOrRenewal $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$subscription = $event->subscription;
/** @var bool $preference */
$preference = Preferences::getForUser($subscription->user, 'notification_bill_reminder', true)->data;
if (true === $preference) {
Log::debug('Subscription reminder is true!');
try {
Notification::send($subscription->user, new BillReminder($subscription, $event->field, $event->diff));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
return;
}
Log::debug('User has disabled subscription reminders.');
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/*
* BillEventHandler.php
* Copyright (c) 2022 james@firefly-iii.org
* NotifiesAboutOverdueSubscription.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,35 +21,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
namespace FireflyIII\Listeners\Model\Subscription;
use Exception;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Events\Model\Subscription\SubscriptionsAreOverdueForPayment;
use FireflyIII\Models\Bill;
use FireflyIII\Notifications\User\BillReminder;
use FireflyIII\Notifications\User\SubscriptionsOverdueReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use function Safe\json_encode;
/**
* Class BillEventHandler
*/
class BillEventHandler
class NotifiesAboutOverdueSubscriptions implements ShouldQueue
{
public function warnAboutOverdueSubscriptions(WarnUserAboutOverdueSubscriptions $event): void
public function handle(SubscriptionsAreOverdueForPayment $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
// make sure user does not get the warning twice.
$overdue = $event->overdue;
$user = $event->user;
$toBeWarned = [];
Log::debug(sprintf('%d bills to warn about.', count($overdue)));
Log::debug(sprintf('%d subscriptions to warn about.', count($overdue)));
foreach ($overdue as $item) {
/** @var Bill $bill */
$bill = $item['bill'];
@@ -62,12 +55,12 @@ class BillEventHandler
$toBeWarned[] = $item;
}
unset($bill);
Log::debug(sprintf('Now %d bills to warn about.', count($toBeWarned)));
Log::debug(sprintf('Now %d subscription(s) to warn about.', count($toBeWarned)));
/** @var bool $sendNotification */
$sendNotification = Preferences::getForUser($user, 'notification_bill_reminder', true)->data;
if (false === $sendNotification) {
Log::debug('User has disabled bill reminders.');
Log::debug('User has disabled subscription reminders.');
return;
}
@@ -105,39 +98,4 @@ class BillEventHandler
}
public function warnAboutBill(WarnUserAboutBill $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$bill = $event->bill;
/** @var bool $preference */
$preference = Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data;
if (true === $preference) {
Log::debug('Bill reminder is true!');
try {
Notification::send($bill->user, new BillReminder($bill, $event->field, $event->diff));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
return;
}
Log::debug('User has disabled bill reminders.');
}
}

View File

@@ -45,11 +45,11 @@ class AppServiceProvider extends ServiceProvider
{
Schema::defaultStringLength(191);
// Passport::$clientUuids = false;
Response::macro('api', static function (array $value) {
Response::macro('api', function (array $value) {
$headers = [
'Cache-Control' => 'no-store',
];
$uuid = (string) request()->header('X-Trace-Id');
$uuid = (string)request()->header('X-Trace-Id');
if ('' !== trim($uuid) && (1 === preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', trim($uuid)))) {
$headers['X-Trace-Id'] = $uuid;
}
@@ -61,7 +61,7 @@ class AppServiceProvider extends ServiceProvider
});
// blade extension
Blade::directive('activeXRoutePartial', static function (string $route): string {
Blade::directive('activeXRoutePartial', function (string $route): string {
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'menu-open';
@@ -69,7 +69,7 @@ class AppServiceProvider extends ServiceProvider
return '';
});
Blade::if('partialroute', static function (string $route, string $firstParam = ''): bool {
Blade::if('partialroute', function (string $route, string $firstParam = ''): bool {
$name = Route::getCurrentRoute()->getName() ?? '';
if ('' === $firstParam && str_contains($name, $route)) {
return true;

View File

@@ -27,12 +27,6 @@ use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\DetectedNewIPAddress;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\ChangedName;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
@@ -73,145 +67,145 @@ class EventServiceProvider extends ServiceProvider
protected $listen
= [
// is a User related event.
RegisteredUser::class => [
RegisteredUser::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendAdminRegistrationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
],
UserAttemptedLogin::class => [
UserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification',
],
// is a User related event.
Login::class => [
Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
ActuallyLoggedIn::class => [
ActuallyLoggedIn::class => [
'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress',
],
DetectedNewIPAddress::class => [
DetectedNewIPAddress::class => [
'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress',
],
RequestedVersionCheckStatus::class => [
RequestedVersionCheckStatus::class => [
'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates',
],
RequestedReportOnJournals::class => [
RequestedReportOnJournals::class => [
'FireflyIII\Handlers\Events\AutomationHandler@reportJournals',
],
// is a User related event.
RequestedNewPassword::class => [
RequestedNewPassword::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
],
UserTestNotificationChannel::class => [
UserTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendTestNotification',
],
// is a User related event.
UserChangedEmail::class => [
UserChangedEmail::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
],
// admin related
OwnerTestNotificationChannel::class => [
OwnerTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification',
],
NewVersionAvailable::class => [
NewVersionAvailable::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',
],
InvitationCreated::class => [
InvitationCreated::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
UnknownUserAttemptedLogin::class => [
UnknownUserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => [
StoredTransactionGroup::class => [
'FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers',
],
TriggeredStoredTransactionGroup::class => [
TriggeredStoredTransactionGroup::class => [
'FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerRulesManually',
],
// is a Transaction Journal related event.
UpdatedTransactionGroup::class => [
UpdatedTransactionGroup::class => [
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers',
],
DestroyedTransactionGroup::class => [
DestroyedTransactionGroup::class => [
'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers',
],
// API related events:
AccessTokenCreated::class => [
AccessTokenCreated::class => [
'FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated',
],
// Webhook related event:
RequestedSendWebhookMessages::class => [
RequestedSendWebhookMessages::class => [
'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages',
],
// account related events:
StoredAccount::class => [
StoredAccount::class => [
'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit',
],
UpdatedAccount::class => [
UpdatedAccount::class => [
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit',
],
// bill related events:
WarnUserAboutBill::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
],
WarnUserAboutOverdueSubscriptions::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions',
],
// subscription related events:
// SubscriptionNeedsExtensionOrRenewal::class => [
// 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
// ],
// WarnUserAboutOverdueSubscriptions::class => [
// 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions',
// ],
// audit log events:
TriggeredAuditLog::class => [
TriggeredAuditLog::class => [
'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent',
],
// piggy bank related events:
ChangedAmount::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
],
ChangedName::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
],
// PiggyBankAmountIsChanged::class => [
// 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
// ],
// ChangedName::class => [
// 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
// ],
// rule actions
RuleActionFailedOnArray::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
],
RuleActionFailedOnObject::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
],
// RuleActionFailedOnArray::class => [
// 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
// ],
// RuleActionFailedOnObject::class => [
// 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
// ],
// security related
EnabledMFA::class => [
EnabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
],
DisabledMFA::class => [
DisabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
],
MFANewBackupCodes::class => [
MFANewBackupCodes::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
],
MFAUsedBackupCode::class => [
MFAUsedBackupCode::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail',
],
MFABackupFewLeft::class => [
MFABackupFewLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail',
],
MFABackupNoLeft::class => [
MFABackupNoLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
],
MFAManyFailedAttempts::class => [
MFAManyFailedAttempts::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
],
// preferences
UserGroupChangedPrimaryCurrency::class => [
UserGroupChangedPrimaryCurrency::class => [
'FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts',
],
];

View File

@@ -514,7 +514,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
{
$query = sprintf('%%%s%%', $query);
return $this->user->bills()->whereLike('name', $query)->take($limit)->get();
return $this->user->bills()->orderBy('name', 'ASC')->whereLike('name', $query)->take($limit)->get();
}
public function setObjectGroup(Bill $bill, string $objectGroupTitle): Bill

View File

@@ -331,7 +331,7 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf
public function searchCategory(string $query, int $limit): Collection
{
$search = $this->user->categories();
$search = $this->user->categories()->orderBy('name', 'ASC');
if ('' !== $query) {
$search->whereLike('name', sprintf('%%%s%%', $query));
}

View File

@@ -361,7 +361,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface, UserGroupInterf
public function searchCurrency(string $search, int $limit): Collection
{
$query = TransactionCurrency::where('enabled', true);
$query = TransactionCurrency::where('enabled', true)->orderBy('code', 'ASC');
if ('' !== $search) {
$query->whereLike('name', sprintf('%%%s%%', $search));
}

View File

@@ -182,6 +182,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac
{
$query = $this->user->transactionJournals()
->orderBy('date', 'DESC')
->orderBy('description', 'ASC')
;
if ('' !== $search) {
$query->whereLike('description', sprintf('%%%s%%', $search));

View File

@@ -25,8 +25,8 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\PiggyBank;
use Exception;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\ChangedName;
use FireflyIII\Events\Model\PiggyBank\PiggyBankAmountIsChanged;
use FireflyIII\Events\Model\PiggyBank\PiggyBankNameIsChanged;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\PiggyBankFactory;
use FireflyIII\Models\Account;
@@ -68,7 +68,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcsub((string) $currentAmount, $amount);
$pivot->current_amount = bcsub((string)$currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -82,7 +82,7 @@ trait ModifiesPiggyBanks
$pivot->save();
Log::debug('ChangedAmount: removeAmount [a]: Trigger change for negative amount.');
event(new ChangedAmount($piggyBank, bcmul($amount, '-1'), $journal, null));
event(new PiggyBankAmountIsChanged($piggyBank, bcmul($amount, '-1'), $journal, null));
return true;
}
@@ -91,7 +91,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcadd((string) $currentAmount, $amount);
$pivot->current_amount = bcadd((string)$currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -105,7 +105,7 @@ trait ModifiesPiggyBanks
$pivot->save();
Log::debug('ChangedAmount: addAmount [b]: Trigger change for positive amount.');
event(new ChangedAmount($piggyBank, $amount, $journal, null));
event(new PiggyBankAmountIsChanged($piggyBank, $amount, $journal, null));
return true;
}
@@ -123,13 +123,13 @@ trait ModifiesPiggyBanks
if (0 !== bccomp($piggyBank->target_amount, '0')) {
$leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar);
$maxAmount = 1 === bccomp((string) $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, (string) $maxAmount);
$compare = bccomp($amount, (string)$maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
@@ -141,7 +141,7 @@ trait ModifiesPiggyBanks
{
$savedSoFar = $this->getCurrentAmount($piggyBank, $account);
return bccomp($amount, (string) $savedSoFar) <= 0;
return bccomp($amount, (string)$savedSoFar) <= 0;
}
/**
@@ -178,11 +178,11 @@ trait ModifiesPiggyBanks
if (-1 === bccomp($difference, '0')) {
Log::debug('ChangedAmount: addAmount [c]: Trigger change for negative amount.');
event(new ChangedAmount($piggyBank, $difference, null, null));
event(new PiggyBankAmountIsChanged($piggyBank, $difference, null, null));
}
if (1 === bccomp($difference, '0')) {
Log::debug('ChangedAmount: addAmount [d]: Trigger change for positive amount.');
event(new ChangedAmount($piggyBank, $difference, null, null));
event(new PiggyBankAmountIsChanged($piggyBank, $difference, null, null));
}
return $piggyBank;
@@ -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((string) $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, (string) $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));
@@ -283,7 +283,7 @@ trait ModifiesPiggyBanks
private function updateProperties(PiggyBank $piggyBank, array $data): PiggyBank
{
if (array_key_exists('name', $data) && '' !== $data['name']) {
event(new ChangedName($piggyBank, $piggyBank->name, $data['name']));
event(new PiggyBankNameIsChanged($piggyBank, $piggyBank->name, $data['name']));
$piggyBank->name = $data['name'];
}
if (array_key_exists('transaction_currency_id', $data) && is_int($data['transaction_currency_id'])) {

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\PiggyBank;
use Carbon\Carbon;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\PiggyBankAmountIsChanged;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\PiggyBankFactory;
use FireflyIII\Models\Account;
@@ -431,7 +431,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface, UserGroupInte
'objectGroups',
]
)
->orderBy('piggy_banks.order', 'ASC')->distinct()
->orderBy('piggy_banks.order', 'ASC')->orderBy('piggy_banks.name', 'ASC')->distinct()
;
if ('' !== $query) {
$search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query));
@@ -448,7 +448,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface, UserGroupInte
$piggyBank->piggyBankEvents()->delete();
foreach ($piggyBank->accounts as $account) {
if (0 !== bccomp('0', (string) $account->pivot->current_amount)) {
event(new ChangedAmount($piggyBank, $account->pivot->current_amount, null, null));
event(new PiggyBankAmountIsChanged($piggyBank, $account->pivot->current_amount, null, null));
}
}
}

View File

@@ -255,7 +255,7 @@ class UserRepository implements UserRepositoryInterface
if (!$user instanceof User) {
throw new FireflyException('User is not a User object.');
}
$now = today(config('app.timezone'));
$now = now(config('app.timezone'));
$now->addDays(2);
$invitee = new InvitedUser();
$invitee->user()->associate($user);

View File

@@ -25,12 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Services\Internal\Destroy;
use Illuminate\Support\Facades\Log;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Models\TransactionJournalMeta;
use Illuminate\Support\Facades\DB;
/**
* Class JournalDestroyService
@@ -41,51 +36,6 @@ class JournalDestroyService
{
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Transaction $transaction */
foreach ($journal->transactions()->get() as $transaction) {
Log::debug(sprintf('Will now delete transaction #%d', $transaction->id));
$transaction->delete();
}
// also delete journal_meta entries.
/** @var TransactionJournalMeta $meta */
foreach ($journal->transactionJournalMeta()->get() as $meta) {
Log::debug(sprintf('Will now delete meta-entry #%d', $meta->id));
$meta->delete();
}
// also delete attachments.
/** @var Attachment $attachment */
foreach ($journal->attachments()->get() as $attachment) {
$attachment->delete();
}
// delete all from 'budget_transaction_journal'
DB::table('budget_transaction_journal')
->where('transaction_journal_id', $journal->id)->delete()
;
// delete all from 'category_transaction_journal'
DB::table('category_transaction_journal')
->where('transaction_journal_id', $journal->id)->delete()
;
// delete all from 'tag_transaction_journal'
DB::table('tag_transaction_journal')
->where('transaction_journal_id', $journal->id)->delete()
;
// delete all links:
TransactionJournalLink::where('source_id', $journal->id)->delete();
TransactionJournalLink::where('destination_id', $journal->id)->delete();
// delete all notes
$journal->notes()->delete();
// update events
// TODO move to repository
$journal->piggyBankEvents()->update(['transaction_journal_id' => null]);
$journal->delete();
// delete group, if group is empty:

View File

@@ -31,6 +31,8 @@ use FireflyIII\Models\AccountBalance;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -64,6 +66,9 @@ class AccountBalanceCalculator
public static function recalculateForJournal(TransactionJournal $transactionJournal): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
Log::debug(__METHOD__);
$object = new self();
@@ -93,10 +98,9 @@ class AccountBalanceCalculator
return '0';
}
Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->whereNull('transactions.deleted_at')
->where('transaction_journals.transaction_currency_id', $currencyId)
->where('transactions.transaction_currency_id', $currencyId)
->whereNull('transaction_journals.deleted_at')
// this order is the same as GroupCollector
->orderBy('transaction_journals.date', 'DESC')
@@ -106,21 +110,32 @@ class AccountBalanceCalculator
->orderBy('transactions.amount', 'DESC')
->where('transactions.account_id', $accountId)
;
$notBefore->startOfDay();
$query->where('transaction_journals.date', '<', $notBefore);
$first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
if (null === $first) {
Log::debug(sprintf('Found no transactions for currency #%d and account #%d, return 0.', $currencyId, $accountId));
return '0';
}
$balance = (string)($first->balance_after ?? '0');
Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d on moment %s', Steam::bcround($balance, 2), $first->id ?? 0, $notBefore->format('Y-m-d H:i:s')));
return $balance;
}
private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void
{
Log::debug('start of optimizedCalculation');
if ($notBefore instanceof Carbon) {
$notBefore->startOfDay();
}
Log::debug(sprintf('start of optimizedCalculation with date "%s"', $notBefore?->format('Y-m-d H:i:s')));
if ($accounts->count() > 0) {
Log::debug(sprintf('Limited to %d account(s)', $accounts->count()));
Log::debug(sprintf('Limited to %d account(s): %s', $accounts->count(), implode(', ', $accounts->pluck('id')->toArray())));
}
// collect all transactions and the change they make.
$balances = [];
@@ -139,18 +154,18 @@ class AccountBalanceCalculator
$query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray());
}
if ($notBefore instanceof Carbon) {
$notBefore->startOfDay();
$query->where('transaction_journals.date', '>=', $notBefore);
}
$set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']);
Log::debug(sprintf('Counted %d transaction(s)', $set->count()));
Log::debug(sprintf('Found %d transaction(s)', $set->count()));
// the balance value is an array.
// first entry is the balance, second is the date.
/** @var Transaction $entry */
foreach ($set as $entry) {
Log::debug(sprintf('Processing transaction #%d with currency #%d and amount %s', $entry->id, $entry->transaction_currency_id, Steam::bcround($entry->amount)));
// start with empty array:
$balances[$entry->account_id] ??= [];
$balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null];
@@ -158,6 +173,9 @@ class AccountBalanceCalculator
// before and after are easy:
$before = $balances[$entry->account_id][$entry->transaction_currency_id][0];
$after = bcadd($before, (string)$entry->amount);
Log::debug(sprintf('Before:%s, after:%s', Steam::bcround($before, 2), Steam::bcround($after, 2)));
if (true === $entry->balance_dirty || $accounts->count() > 0) {
// update the transaction:
$entry->balance_before = $before;

View File

@@ -24,12 +24,12 @@ declare(strict_types=1);
namespace FireflyIII\Support\System;
use Illuminate\Support\Facades\Log;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Laravel\Passport\Console\KeysCommand;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
@@ -48,16 +48,27 @@ class OAuthKeys
public static function generateKeys(): void
{
Log::debug('Will now run generateKeys()');
Artisan::registerCommand(new KeysCommand());
Artisan::call('firefly-iii:laravel-passport-keys');
Log::debug('Done with generateKeys()');
}
public static function hasKeyFiles(): bool
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
Log::debug('hasKeyFiles()');
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
$privateExists = file_exists($private);
$publicExists = file_exists($public);
return file_exists($private) && file_exists($public);
Log::debug(sprintf('Private key file at "%s" exists? %s', $private, var_export($privateExists, true)));
Log::debug(sprintf('Public key file at "%s" exists ? %s', $public, var_export($publicExists, true)));
$result = file_exists($private) && file_exists($public);
Log::debug(sprintf('Method will return %s', var_export($result, true)));
return $result;
}
public static function keysInDatabase(): bool
@@ -65,17 +76,36 @@ class OAuthKeys
$privateKey = '';
$publicKey = '';
// better check if keys are in the database:
if (FireflyConfig::has(self::PRIVATE_KEY) && FireflyConfig::has(self::PUBLIC_KEY)) {
$hasPrivate = FireflyConfig::has(self::PRIVATE_KEY);
$hasPublic = FireflyConfig::has(self::PUBLIC_KEY);
Log::debug(sprintf('keysInDatabase: hasPrivate:%s, hasPublic:%s', var_export($hasPrivate, true), var_export($hasPublic, true)));
if ($hasPrivate && $hasPublic) {
try {
$privateKey = (string)FireflyConfig::get(self::PRIVATE_KEY)?->data;
$publicKey = (string)FireflyConfig::get(self::PUBLIC_KEY)?->data;
$privateKey = trim((string)FireflyConfig::get(self::PRIVATE_KEY)?->data);
$publicKey = trim((string)FireflyConfig::get(self::PUBLIC_KEY)?->data);
} catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) {
Log::error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage()));
Log::error($e->getTraceAsString());
}
}
if ('' === $privateKey) {
Log::warning('Private key in DB is unexpectedly an empty string.');
}
if ('' === $publicKey) {
Log::warning('Public key in DB is unexpectedly an empty string.');
}
if ('' !== $privateKey) {
Log::debug(sprintf('SHA2 hash of private key in DB: %s', hash('sha256', $privateKey)));
}
if ('' !== $publicKey) {
Log::debug(sprintf('SHA2 hash of public key in DB : %s', hash('sha256', $publicKey)));
}
$return = '' !== $privateKey && '' !== $publicKey;
Log::debug(sprintf('keysInDatabase will return %s', var_export($return, true)));
return '' !== $privateKey && '' !== $publicKey;
return $return;
}
/**
@@ -86,12 +116,20 @@ class OAuthKeys
*/
public static function restoreKeysFromDB(): bool
{
Log::debug('restoreKeysFromDB()');
$privateKey = (string)FireflyConfig::get(self::PRIVATE_KEY)?->data;
$publicKey = (string)FireflyConfig::get(self::PUBLIC_KEY)?->data;
if ('' === $privateKey) {
Log::warning('Private key is not in the database.');
}
if ('' === $publicKey) {
Log::warning('Public key is not in the database.');
}
try {
$privateContent = Crypt::decrypt($privateKey);
$publicContent = Crypt::decrypt($publicKey);
$privateContent = trim(Crypt::decrypt($privateKey));
$publicContent = trim(Crypt::decrypt($publicKey));
} catch (DecryptException $e) {
Log::error('Could not decrypt pub/private keypair.');
Log::error($e->getMessage());
@@ -99,6 +137,7 @@ class OAuthKeys
// delete config vars from DB:
FireflyConfig::delete(self::PRIVATE_KEY);
FireflyConfig::delete(self::PUBLIC_KEY);
Log::debug('Done with generateKeysFromDB(), return FALSE');
return false;
}
@@ -107,15 +146,24 @@ class OAuthKeys
file_put_contents($private, $privateContent);
file_put_contents($public, $publicContent);
Log::debug(sprintf('Will store private key with hash "%s" in file "%s"', hash('sha256', $privateContent), $private));
Log::debug(sprintf('Will store public key with hash "%s" in file "%s"', hash('sha256', $publicContent), $public));
Log::debug('Done with generateKeysFromDB()');
return true;
}
public static function storeKeysInDB(): void
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
FireflyConfig::set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
FireflyConfig::set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
$privateContent = file_get_contents($private);
$publicContent = file_get_contents($public);
FireflyConfig::set(self::PRIVATE_KEY, Crypt::encrypt($privateContent));
FireflyConfig::set(self::PUBLIC_KEY, Crypt::encrypt($publicContent));
Log::debug(sprintf('Will store the content of file "%s" as "%s" in the database (hash: %s)', $private, self::PRIVATE_KEY, hash('sha256', $privateContent)));
Log::debug(sprintf('Will store the content of file "%s" as "%s" in the database (hash: %s)', $public, self::PUBLIC_KEY, hash('sha256', $publicContent)));
}
public static function verifyKeysRoutine(): void

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Validation;
use ErrorException;
use FireflyIII\Support\Facades\Preferences;
use Config;
use FireflyIII\Enums\AccountTypeEnum;
@@ -210,7 +211,12 @@ class FireflyValidator extends Validator
$value = strtoupper($value);
// replace characters outside of ASCI range.
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
try {
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
} catch (ErrorException $e) {
Log::error(sprintf('Could not convert IBAN "%s" to safe characters. Future steps may fail.', $value));
Log::error($e->getMessage());
}
$search = [' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
$replace = ['', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35'];

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## v6.4.16 - 2026-01-18
> [!WARNING]
> This will be one of the last Firefly III data importer releases that supports PHP 8.4.
### Fixed
- [Discussion 11431](https://github.com/orgs/firefly-iii/discussions/11431) (Settings don't get saved) started by @PVTejas
- [Issue 11473](https://github.com/firefly-iii/firefly-iii/issues/11473) (Searching transaction with two tags_contains returns results matching only one of those) reported by @F-DXI
- [Issue 11474](https://github.com/firefly-iii/firefly-iii/issues/11474) (Potential error in sub total computation for group in subscription) reported by @ma-clog
- [Issue 11479](https://github.com/firefly-iii/firefly-iii/issues/11479) (Editing a user profile as admin without setting a new password causes a 500 Internal server error) reported by @watertrainer
- [Issue 11501](https://github.com/firefly-iii/firefly-iii/issues/11501) (Schema of /api/v1/available-budgets different from spec) reported by @RadCod3
- [Issue 11502](https://github.com/firefly-iii/firefly-iii/issues/11502) (Visual bug - Transaction notes' markdown doesn't properly render code blocks in dark mode) reported by @AyluinReymaer
- [Discussion 11508](https://github.com/orgs/firefly-iii/discussions/11508) (Grouped Piggy banks show as ungrouped when creating a transaction) started by @AyluinReymaer
- [Discussion 11509](https://github.com/orgs/firefly-iii/discussions/11509) (IBAN - iconv(): Wrong encoding) started by @s0fax
- [Discussion 11524](https://github.com/orgs/firefly-iii/discussions/11524) (Can items in dropdowns (specifically categories) be sorted alphabetically?) started by @mvpaderin
- [Issue 11531](https://github.com/firefly-iii/firefly-iii/issues/11531) (Performance: updateRunningBalance executes even when use_running_balance is disabled, causing timeouts on Mass Edits) reported by @maxime-killinger
### Changed
- Rules that delete a transaction will no longer throws a 500, but a 410.
## v6.4.15 - 2026-01-07
### Added

74
composer.lock generated
View File

@@ -2176,16 +2176,16 @@
},
{
"name": "laravel/prompts",
"version": "v0.3.9",
"version": "v0.3.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
"reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4"
"reference": "360ba095ef9f51017473505191fbd4ab73e1cab3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/5c41bf0555b7cfefaad4e66d3046675829581ac4",
"reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4",
"url": "https://api.github.com/repos/laravel/prompts/zipball/360ba095ef9f51017473505191fbd4ab73e1cab3",
"reference": "360ba095ef9f51017473505191fbd4ab73e1cab3",
"shasum": ""
},
"require": {
@@ -2229,9 +2229,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.3.9"
"source": "https://github.com/laravel/prompts/tree/v0.3.10"
},
"time": "2026-01-07T21:00:29+00:00"
"time": "2026-01-13T20:29:29+00:00"
},
{
"name": "laravel/sanctum",
@@ -3304,20 +3304,20 @@
},
{
"name": "league/uri",
"version": "7.7.0",
"version": "7.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
"reference": "4436c6ec8d458e4244448b069cc572d088230b76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76",
"reference": "4436c6ec8d458e4244448b069cc572d088230b76",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.7",
"league/uri-interfaces": "^7.8",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -3331,11 +3331,11 @@
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"ext-uri": "to use the PHP native URI class",
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
"league/uri-components": "Needed to easily manipulate URI objects components",
"league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP",
"jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
"league/uri-components": "to provide additional tools to manipulate URI objects components",
"league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
"php-64bit": "to improve IPV4 host parsing",
"rowbot/url": "to handle WHATWG URL",
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
@@ -3390,7 +3390,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.7.0"
"source": "https://github.com/thephpleague/uri/tree/7.8.0"
},
"funding": [
{
@@ -3398,20 +3398,20 @@
"type": "github"
}
],
"time": "2025-12-07T16:02:06+00:00"
"time": "2026-01-14T17:24:56+00:00"
},
{
"name": "league/uri-interfaces",
"version": "7.7.0",
"version": "7.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
"reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
"reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
"shasum": ""
},
"require": {
@@ -3424,7 +3424,7 @@
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"php-64bit": "to improve IPV4 host parsing",
"rowbot/url": "to handle WHATWG URL",
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"type": "library",
@@ -3474,7 +3474,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0"
},
"funding": [
{
@@ -3482,7 +3482,7 @@
"type": "github"
}
],
"time": "2025-12-07T16:03:21+00:00"
"time": "2026-01-15T06:54:53+00:00"
},
{
"name": "mailersend/laravel-driver",
@@ -10706,16 +10706,16 @@
},
{
"name": "larastan/larastan",
"version": "v3.8.1",
"version": "v3.9.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9"
"reference": "82c18890d0d5b012bc39a3432531e5b6cd1b4b3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9",
"reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9",
"url": "https://api.github.com/repos/larastan/larastan/zipball/82c18890d0d5b012bc39a3432531e5b6cd1b4b3a",
"reference": "82c18890d0d5b012bc39a3432531e5b6cd1b4b3a",
"shasum": ""
},
"require": {
@@ -10784,7 +10784,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.8.1"
"source": "https://github.com/larastan/larastan/tree/v3.9.0"
},
"funding": [
{
@@ -10792,7 +10792,7 @@
"type": "github"
}
],
"time": "2025-12-11T16:37:35+00:00"
"time": "2026-01-17T23:00:37+00:00"
},
{
"name": "laravel-json-api/testing",
@@ -11784,16 +11784,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.4",
"version": "12.5.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a"
"reference": "ab8e4374264bc65523d1458d14bf80261577e01f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a",
"reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ab8e4374264bc65523d1458d14bf80261577e01f",
"reference": "ab8e4374264bc65523d1458d14bf80261577e01f",
"shasum": ""
},
"require": {
@@ -11807,7 +11807,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.5.1",
"phpunit/php-code-coverage": "^12.5.2",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@@ -11861,7 +11861,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.5.4"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.6"
},
"funding": [
{
@@ -11885,7 +11885,7 @@
"type": "tidelift"
}
],
"time": "2025-12-15T06:05:34+00:00"
"time": "2026-01-16T16:28:10+00:00"
},
{
"name": "rector/rector",

View File

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

View File

@@ -20,6 +20,7 @@ null-type-hint = "null_pipe"
[linter]
integrations = ["symfony", "laravel", "phpunit"]
excludes = ["app/Providers/AppServiceProvider.php"] # Additionally excluded from linter only
[linter.rules]
ambiguous-function-call = { enabled = false }

90
package-lock.json generated
View File

@@ -3232,9 +3232,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz",
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
"version": "25.0.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
"integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3350,42 +3350,42 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
"integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.26",
"@vue/shared": "3.5.27",
"entities": "^7.0.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
"integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.26",
"@vue/shared": "3.5.26"
"@vue/compiler-core": "3.5.27",
"@vue/shared": "3.5.27"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
"integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.26",
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-ssr": "3.5.26",
"@vue/shared": "3.5.26",
"@vue/compiler-core": "3.5.27",
"@vue/compiler-dom": "3.5.27",
"@vue/compiler-ssr": "3.5.27",
"@vue/shared": "3.5.27",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
@@ -3393,14 +3393,14 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
"integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/shared": "3.5.26"
"@vue/compiler-dom": "3.5.27",
"@vue/shared": "3.5.27"
}
},
"node_modules/@vue/component-compiler-utils": {
@@ -3482,9 +3482,9 @@
"license": "MIT"
},
"node_modules/@vue/shared": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz",
"integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
"dev": true,
"license": "MIT"
},
@@ -4131,9 +4131,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.14",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
"integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4271,9 +4271,9 @@
"license": "MIT"
},
"node_modules/bootstrap5-autocomplete": {
"version": "1.1.41",
"resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.41.tgz",
"integrity": "sha512-GnjG9/oNOzDPdNumGeWyUhLct6q2y+HrYsznEbmNVhS51IEa6kls9txnogFZLxAXhH93F1RRt4BQMyTBMu5DDg==",
"version": "1.1.42",
"resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.42.tgz",
"integrity": "sha512-bmglwpqdKALLUVuxktgX+AmKCNy3aR7TqDLCxvOM/NXaeLxSY/q7uLMvxFaTyvoNRTXi5uW1Vzuxo2kNHCIChA==",
"license": "MIT"
},
"node_modules/bootstrap5-tags": {
@@ -4564,9 +4564,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001764",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
"version": "1.0.30001765",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
"dev": true,
"funding": [
{
@@ -10974,9 +10974,9 @@
}
},
"node_modules/terser": {
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -11766,9 +11766,9 @@
}
},
"node_modules/watchpack": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
"integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
"integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12290,9 +12290,9 @@
}
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -121,8 +121,10 @@ export default {
let srcType = this.source.type ? this.source.type.toLowerCase() : 'invalid';
let tType = this.transactionType ? this.transactionType.toLowerCase() : 'invalid';
let liabilities = ['loan', 'debt', 'mortgage'];
let asset = ['asset account'];
let sourceIsLiability = liabilities.indexOf(srcType) !== -1;
let destIsLiability = liabilities.indexOf(destType) !== -1;
let destIsAsset =asset.indexOf(destType) !== -1;
// console.log(srcType + ' (source) is a liability: ' + sourceIsLiability);
@@ -131,18 +133,15 @@ export default {
if (tType === 'transfer' || destIsLiability || sourceIsLiability) {
console.log('Source or dest is a liability.')
console.log('Source is liability OR dest is liability, OR transfer. Lock list on currency of destination.');
console.log(this.destination.type);
// console.log('Length of currencies is ' + this.currencies.length);
// console.log(this.currencies);
this.liability = true;
// lock dropdown list on currencyID of destination UNLESS dest is not liab
for (const key in this.currencies) {
if (this.currencies.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
if (
parseInt(this.currencies[key].id) === parseInt(this.destination.currency_id) || !destIsLiability
) {
console.log('Enable currency!!');
console.log(this.currencies[key]);
// console.log(this.destination);
if (parseInt(this.currencies[key].id) === parseInt(this.destination.currency_id) || (!destIsLiability && 'transfer' !== tType)) {
console.log('Enable currency: ' + this.currencies[key].attributes.code + '.');
this.enabledCurrencies.push(this.currencies[key]);
}
}

View File

@@ -80,11 +80,11 @@ export default {
// add to temp list
let currentPiggy = res.data[key];
if (currentPiggy.objectGroup) {
let groupOrder = currentPiggy.objectGroup.order;
let groupOrder = currentPiggy.object_group_order;
if (!tempList[groupOrder]) {
tempList[groupOrder] = {
group: {
title: currentPiggy.objectGroup.title
title: currentPiggy.object_group_title
},
piggies: [],
};
@@ -94,7 +94,7 @@ export default {
id: currentPiggy.id
});
}
if (!currentPiggy.objectGroup) {
if (null === currentPiggy.object_group_id) {
// add to empty one:
tempList[0].piggies.push({name_with_balance: currentPiggy.name_with_balance, id: currentPiggy.id});
}

View File

@@ -1,11 +1,11 @@
{
"firefly": {
"administrations_page_title": "Financial administrations",
"administrations_index_menu": "Financial administrations",
"administrations_page_title": "Administrasi keuangan",
"administrations_index_menu": "Administrasi keuangan",
"expires_at": "Expires at",
"temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.",
"temp_administrations_introduction": "Firefly III segera akan memiliki kemampuan untuk mengelola beberapa administrasi keuangan. Saat ini, Anda hanya memiliki satu. Anda dapat mengatur judul administrasi ini serta mata uang utamanya. Pengaturan ini menggantikan pengaturan sebelumnya di mana Anda menetapkan \u201cmata uang default\u201d. Sekarang, pengaturan ini terkait langsung dengan masing-masing administrasi keuangan dan dapat berbeda untuk setiap administrasi.",
"administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.",
"administrations_page_edit_sub_title_js": "Edit financial administration \"{title}\"",
"administrations_page_edit_sub_title_js": "Edit administrasi keuangan \u201c{title}\u201d",
"table": "Meja",
"welcome_back": "Apa yang sedang dimainkan?",
"flash_error": "Kesalahan!",
@@ -20,7 +20,7 @@
"split": "Pisah",
"single_split": "Pisah",
"not_enough_currencies": "Not enough currencies",
"not_enough_currencies_enabled": "If you have just one currency enabled, there is no need to add exchange rates.",
"not_enough_currencies_enabled": "Jika Anda hanya mengaktifkan satu mata uang, tidak perlu menambahkan nilai tukar.",
"transaction_stored_link": "<a href=\"transactions\/show\/{ID}\">Transaction #{ID} (\"{title}\")<\/a> has been stored.",
"webhook_stored_link": "<a href=\"webhooks\/show\/{ID}\">Webhook #{ID} (\"{title}\")<\/a> has been stored.",
"webhook_updated_link": "<a href=\"webhooks\/show\/{ID}\">Webhook #{ID}<\/a> (\"{title}\") has been updated.",
@@ -31,7 +31,7 @@
"apply_rules_checkbox": "Apply rules",
"fire_webhooks_checkbox": "Fire webhooks",
"no_budget_pointer": "Anda tampaknya belum memiliki anggaran. Anda harus membuat beberapa di halaman-<a href=\"budgets\">anggaran<\/a>. Anggaran dapat membantu anda melacak pengeluaran.",
"no_bill_pointer": "You seem to have no subscription yet. You should create some on the <a href=\"subscriptions\">subscription<\/a>-page. Subscriptions can help you keep track of expenses.",
"no_bill_pointer": "Sepertinya Anda belum memiliki langganan. Buat beberapa di halaman <a href=\"subscriptions\">Langganan<\/a>. Langganan dapat membantu Anda melacak pengeluaran.",
"source_account": "Akun sumber",
"hidden_fields_preferences": "You can enable more transaction options in your <a href=\"preferences\">preferences<\/a>.",
"destination_account": "Akun tujuan",
@@ -50,7 +50,7 @@
"category": "Kategori",
"attachments": "Lampiran",
"notes": "Notes",
"external_url": "URL luar",
"external_url": "URL eksternal",
"update_transaction": "Update transaction",
"after_update_create_another": "After updating, return here to continue editing.",
"store_as_new": "Store as a new transaction instead of updating.",
@@ -59,7 +59,7 @@
"no_piggy_bank": "(tidak ada celengan)",
"description": "Deskripsi",
"split_transaction_title_help": "If you create a split transaction, there must be a global description for all splits of the transaction.",
"destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.",
"destination_account_reconciliation": "Anda tidak dapat mengubah rekening tujuan pada transaksi rekonsiliasi.",
"source_account_reconciliation": "Anda tidak dapat mengedit akun sumber dari transaksi rekonsiliasi.",
"budget": "Anggaran",
"bill": "Subscription",
@@ -146,14 +146,14 @@
"response": "Tanggapan",
"visit_webhook_url": "Visit webhook URL",
"reset_webhook_secret": "Reset webhook secret",
"header_exchange_rates": "Exchange rates",
"exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in <a href=\"https:\/\/docs.firefly-iii.org\/explanation\/financial-concepts\/exchange-rates\/\">the documentation<\/a>.",
"exchange_rates_from_to": "Between {from} and {to} (and the other way around)",
"exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.",
"header_exchange_rates_rates": "Exchange rates",
"header_exchange_rates_table": "Table with exchange rates",
"header_exchange_rates": "Nilai tukar",
"exchange_rates_intro": "Firefly III mendukung pengunduhan dan penggunaan nilai tukar. Baca selengkapnya di <a href=\"https:\/\/docs.firefly-iii.org\/explanation\/financial-concepts\/exchange-rates\/\">dokumentasi<\/a>.",
"exchange_rates_from_to": "Antara {from} dan {to} (dan sebaliknya)",
"exchange_rates_intro_rates": "Firefly III menggunakan nilai tukar berikut. Nilai tukar terbalik akan dihitung secara otomatis jika tidak tersedia. Jika tidak ada nilai tukar untuk tanggal transaksi, Firefly III akan mencari nilai tukar sebelumnya. Jika tetap tidak ditemukan, nilai tukar \u201c1\u201d akan digunakan.",
"header_exchange_rates_rates": "Nilai tukar",
"header_exchange_rates_table": "Tabel dengan nilai tukar",
"help_rate_form": "On this day, how many {to} will you get for one {from}?",
"add_new_rate": "Add a new exchange rate",
"add_new_rate": "Tambahkan nilai tukar baru",
"save_new_rate": "Save new rate"
},
"form": {

View File

@@ -180,7 +180,7 @@
"list": {
"title": "Titlu",
"active": "Este activ?",
"primary_currency": "Primary currency",
"primary_currency": "Moned\u0103 principal\u0103",
"trigger": "Declan\u0219ator",
"response": "R\u0103spuns",
"delivery": "Livrare",

View File

@@ -314,7 +314,12 @@
{% if account.id == transaction.source_account_id %}
<span title="Transfer, source">{{ formatAmountBySymbol(transaction.source_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}</span>
{% else %}
<span title="Transfer, dest">{{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}</span>
{% if null == transaction.foreign_currency_id %}
<span title="Transfer, dest, normal currency">{{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}</span>
{% endif %}
{% if null != transaction.foreign_currency_id %}
<span title="Transfer, dest, foreign currency">{{ formatAmountBySymbol(transaction.destination_balance_after, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}</span>
{% endif %}
{% endif %}
{% else %}
&nbsp;