Compare commits

..

35 Commits

Author SHA1 Message Date
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
github-actions[bot]
7c1b550c2c Merge pull request #11518 from firefly-iii/release-1768366825
🤖 Automatically merge the PR into the develop branch.
2026-01-14 06:00:33 +01:00
JC5
94a25d3137 🤖 Auto commit for release 'develop' on 2026-01-14 2026-01-14 06:00:25 +01:00
James Cole
4c95ab49f4 Cannot be static. 2026-01-14 05:56:08 +01:00
github-actions[bot]
be87a24b8f Merge pull request #11517 from firefly-iii/release-1768334732
🤖 Automatically merge the PR into the develop branch.
2026-01-13 21:05:40 +01:00
JC5
f6032d5502 🤖 Auto commit for release 'develop' on 2026-01-13 2026-01-13 21:05:32 +01:00
James Cole
7dcfb68fca Fix parse issue. 2026-01-13 20:59:54 +01:00
James Cole
419c796eac Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-13 20:54:44 +01:00
James Cole
1d5733adba Experimental command 2026-01-13 20:54:31 +01:00
35 changed files with 510 additions and 289 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

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/*
* ExplainAvailableBudget.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\Console\Commands\Explain;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Console\Commands\VerifiesAccessToken;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Console\Command;
class ExplainAvailableBudget extends Command
{
use VerifiesAccessToken;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'explain:available-budget
{--date=now : A date formatted YYYY-MM-DD or the word "now"}
{--user=1 : The user ID.}
{--token= : The user\'s access token.}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Explains why the available budget amount is what it is.';
/**
* Execute the console command.
*/
public function handle(): int
{
$date = $this->getDate((string) $this->option('date'));
$range = Preferences::getForUser($this->getUser(), 'viewRange', '1M')->data ?? '1M';
$title = Navigation::periodShow($date, $range);
$this->line('This command explains why the "available" budget bar at the top of your /budget bar means.');
$this->line(sprintf(
'You submitted date %s and your settings show a %s period, so this explanation concerns the period "%s".',
$date->format('Y-m-d'),
$range,
$title
));
return Command::SUCCESS;
}
private function getDate(string $param): Carbon
{
if ('now' === $param) {
return today();
}
try {
$date = Carbon::parse($param);
} catch (InvalidFormatException) {
$this->warn('Invalid date given. Fall back to today\'s date.');
return today();
}
return $date;
}
}

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,8 +1,9 @@
<?php
declare(strict_types=1);
/*
* Updated.php
* Copyright (c) 2024 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).
*
@@ -17,19 +18,18 @@
* 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/.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Subscription;
namespace FireflyIII\Events\Model\Account;
use FireflyIII\Models\Account;
use FireflyIII\Events\Event;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
class Updated
class SubscriptionsAreOverdueForPayment extends Event
{
use SerializesModels;
public function __construct(public Account $account) {}
public function __construct(public User $user, public array $overdue) {}
}

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\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

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

@@ -103,7 +103,7 @@ class AvailableBudget extends Model
protected function endDate(): Attribute
{
return Attribute::make(
get: Carbon::parse(...),
get: static fn (string $value): Carbon => Carbon::parse($value),
set: static fn (Carbon $value): string => $value->format('Y-m-d'),
);
}
@@ -111,7 +111,7 @@ class AvailableBudget extends Model
protected function startDate(): Attribute
{
return Attribute::make(
get: Carbon::parse(...),
get: static fn (string $value): Carbon => Carbon::parse($value),
set: static fn (Carbon $value): string => $value->format('Y-m-d'),
);
}

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,8 +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;
@@ -73,145 +71,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 => [
ChangedAmount::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
],
ChangedName::class => [
ChangedName::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
],
// rule actions
RuleActionFailedOnArray::class => [
RuleActionFailedOnArray::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
],
RuleActionFailedOnObject::class => [
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

@@ -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));

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

62
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",
@@ -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-13',
'build_time' => 1768333193,
'version' => 'develop/2026-01-18',
'build_time' => 1768729886,
'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 }

42
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": {
@@ -3834,9 +3834,9 @@
}
},
"node_modules/alpinejs": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.3.tgz",
"integrity": "sha512-fSI6F5213FdpMC4IWaup92KhuH3jBX0VVqajRJ6cOTCy1cL6888KyXdGO+seAAkn+g6fnrxBqQEx6gRpQ5EZoQ==",
"version": "3.15.4",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.4.tgz",
"integrity": "sha512-lDpOdoVo0bhFjgl310k1qw3kbpUYwM/v0WByvAchsO93bl3o1rrgr0P/ssx3CimwEtNfXbmwKbtHbqTRCTTH9g==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
@@ -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": {
@@ -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

@@ -30,8 +30,8 @@
"submission_options": "Op\u0163iuni de depunere",
"apply_rules_checkbox": "Aplic\u0103 regulile",
"fire_webhooks_checkbox": "Webhook-uri de incendiu",
"no_budget_pointer": "Se pare c\u0103 nu ave\u021bi \u00eenc\u0103 bugete. Ar trebui s\u0103 crea\u021bi c\u00e2teva pe pagina <a href=\"\/budgets\">bugete<\/a>. Bugetele v\u0103 pot ajuta s\u0103 \u021bine\u021bi eviden\u021ba cheltuielilor.",
"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_budget_pointer": "Se pare c\u0103 nu ai \u00eenc\u0103 bugete. Creeaz\u0103-le pe pagina <a href=\"budgets\">bugete<\/a>. Bugetele te ajut\u0103 s\u0103 \u021bii eviden\u021ba cheltuielilor.",
"no_bill_pointer": "Se pare c\u0103 nu ai \u00eenc\u0103 abonamente. Creeaz\u0103-le pe pagina <a href=\"subscriptions\">abonamente<\/a>. Abonamentele te ajut\u0103 s\u0103 \u021bii eviden\u021ba cheltuielilor.",
"source_account": "Contul surs\u0103",
"hidden_fields_preferences": "Pute\u021bi activa mai multe op\u021biuni de tranzac\u021bie \u00een <a href=\"preferences\">preferin\u021bele<\/a> dvs.",
"destination_account": "Contul de destina\u021bie",
@@ -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",
@@ -189,6 +189,6 @@
},
"config": {
"html_language": "ro",
"date_time_fns": "MMMM do yyy @ HH:mm:ss"
"date_time_fns": "MMMM do yyyy @ HH:mm:ss"
}
}

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;