Compare commits

..

10 Commits

Author SHA1 Message Date
github-actions[bot]
35e0791a9f Merge pull request #10411 from firefly-iii/release-1748971941
🤖 Automatically merge the PR into the develop branch.
2025-06-03 19:32:29 +02:00
JC5
637ecc66d2 🤖 Auto commit for release 'develop' on 2025-06-03 2025-06-03 19:32:21 +02:00
James Cole
3a38175b2a Fix renamed variable. 2025-06-03 19:26:07 +02:00
James Cole
d78fd86d7a Catch already linked transactions. 2025-06-02 20:46:09 +02:00
James Cole
395332d6dd Fix #10403 2025-06-02 20:32:58 +02:00
James Cole
c5cbceb81a Fix #10399 2025-06-02 05:39:43 +02:00
github-actions[bot]
ec94f1bcf1 Merge pull request #10401 from firefly-iii/release-1748835078
🤖 Automatically merge the PR into the develop branch.
2025-06-02 05:31:25 +02:00
JC5
ee3d18a8ea 🤖 Auto commit for release 'develop' on 2025-06-02 2025-06-02 05:31:18 +02:00
James Cole
a9cd8b6512 Better fit for run recalculations. 2025-06-01 14:00:35 +02:00
James Cole
5bd87036b0 Config works now. Does not feel solved yet, but still. 2025-06-01 13:32:18 +02:00
28 changed files with 235 additions and 157 deletions

View File

@@ -34,7 +34,6 @@ use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use League\Fractal\Resource\Item;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -73,26 +72,24 @@ class UpdateController extends Controller
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
app('log')->debug('Now in update routine for transaction group');
$data = $request->getAll();
$oldAmount = $this->groupRepository->getTotalAmount($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newAmount = $this->groupRepository->getTotalAmount($transactionGroup);
$manager = $this->getManager();
Log::debug(sprintf('Old amount: %s, new amount: %s', $oldAmount, $newAmount));
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
app('preferences')->mark();
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$amountChanged = 0 !== bccomp($oldAmount, $newAmount);
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $amountChanged));
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations));
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
// filter on transaction group.
@@ -101,20 +98,20 @@ class UpdateController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new NotFoundHttpException();
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -65,29 +65,29 @@ class UpdateController extends Controller
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
app('log')->debug('Now in update routine for transaction group [v2]!');
$data = $request->getAll();
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$amountChanged = true;
$data = $request->getAll();
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = true;
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $amountChanged));
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations));
app('preferences')->mark();
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUser($admin)->setTransactionGroup($transactionGroup);
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new FireflyException('200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
$transformer = new TransactionGroupTransformer();
$transformer = new TransactionGroupTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject('transactions', $selectedGroup, $transformer))->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -58,6 +58,7 @@ class CorrectsGroupAccounts extends Command
$handler = new UpdatedGroupEventHandler();
foreach ($groups as $groupId) {
$group = TransactionGroup::find($groupId);
// TODO in theory the "unifyAccounts" method could lead to the need for run recalculations.
$event = new UpdatedTransactionGroup($group, true, true, false);
$handler->unifyAccounts($event);
}

View File

@@ -0,0 +1,16 @@
<?php
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
{
use SerializesModels;
public function __construct(public PiggyBank $piggyBank, public string $oldName, public string $newName) {}
}

View File

@@ -37,5 +37,5 @@ class UpdatedTransactionGroup extends Event
/**
* Create a new event instance.
*/
public function __construct(public TransactionGroup $transactionGroup, public bool $applyRules, public bool $fireWebhooks, public bool $amountChanged) {}
public function __construct(public TransactionGroup $transactionGroup, public bool $applyRules, public bool $fireWebhooks, public bool $runRecalculations) {}
}

View File

@@ -24,6 +24,10 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events\Model;
use FireflyIII\Events\Model\PiggyBank\ChangedName;
use FireflyIII\Models\Account;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleAction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Models\PiggyBankEvent;
@@ -33,6 +37,24 @@ use FireflyIII\Models\PiggyBankEvent;
*/
class PiggyBankEventHandler
{
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
{
// find journal if group is present.

View File

@@ -49,7 +49,7 @@ class UpdatedGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
if ($event->amountChanged) {
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}

View File

@@ -75,13 +75,22 @@ class AmountController extends Controller
$totalSaved = $this->piggyRepos->getCurrentAmount($piggyBank);
foreach ($piggyBank->accounts as $account) {
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, $date);
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account);
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$leftToSave = bcsub($piggyBank->target_amount, $totalSaved);
$maxAmount = 0 === bccomp($piggyBank->target_amount, '0') ? $leftOnAccount : min($leftOnAccount, $leftToSave);
Log::debug(sprintf(
'Account "%s", left on account "%s", saved so far "%s", left to save "%s", max amount "%s".',
$account->name,
$leftOnAccount,
$totalSaved,
$leftToSave,
$maxAmount,
));
$accounts[] = [
'account' => $account,
'left_on_account' => $leftOnAccount,
'saved_so_far' => $savedSoFar,
'total_saved' => $totalSaved,
'left_to_save' => $leftToSave,
'max_amount' => $maxAmount,
];
@@ -100,18 +109,18 @@ class AmountController extends Controller
public function addMobile(PiggyBank $piggyBank)
{
/** @var Carbon $date */
$date = session('end', today(config('app.timezone')));
$accounts = [];
$total = '0';
$date = session('end', today(config('app.timezone')));
$accounts = [];
$total = '0';
$totalSaved = $this->piggyRepos->getCurrentAmount($piggyBank);
foreach ($piggyBank->accounts as $account) {
$leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, $date);
$savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account);
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$leftToSave = bcsub($piggyBank->target_amount, $totalSaved);
$maxAmount = 0 === bccomp($piggyBank->target_amount, '0') ? $leftOnAccount : min($leftOnAccount, $leftToSave);
$accounts[] = [
'account' => $account,
'left_on_account' => $leftOnAccount,
'saved_so_far' => $savedSoFar,
'total_saved' => $totalSaved,
'left_to_save' => $leftToSave,
'max_amount' => $maxAmount,
];

View File

@@ -292,7 +292,7 @@ class ConvertController extends Controller
$group->refresh();
session()->flash('success', (string) trans('firefly.converted_to_'.$destinationType->type));
event(new UpdatedTransactionGroup($group, true, true, false));
event(new UpdatedTransactionGroup($group, true, true, true));
return redirect(route('transactions.show', [$group->id]));
}

View File

@@ -191,15 +191,15 @@ class MassController extends Controller
*/
private function updateJournal(int $journalId, MassEditJournalRequest $request): void
{
$journal = $this->repository->find($journalId);
$journal = $this->repository->find($journalId);
if (!$journal instanceof TransactionJournal) {
throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId));
}
$service = app(JournalUpdateService::class);
$service = app(JournalUpdateService::class);
// for each field, call the update service.
$service->setTransactionJournal($journal);
$data = [
$data = [
'date' => $this->getDateFromRequest($request, $journal->id, 'date'),
'description' => $this->getStringFromRequest($request, $journal->id, 'description'),
'source_id' => $this->getIntFromRequest($request, $journal->id, 'source_id'),
@@ -217,8 +217,8 @@ class MassController extends Controller
$service->setData($data);
$service->update();
// trigger rules
$amountChanged = $service->isAmountChanged();
event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, $amountChanged));
$runRecalculations = $service->isCompareHashChanged();
event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, $runRecalculations));
}
private function getDateFromRequest(MassEditJournalRequest $request, int $journalId, string $key): ?Carbon

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Carbon\Carbon;
use FireflyIII\Support\Request\ChecksLogin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
@@ -41,15 +40,9 @@ class SelectTransactionsRequest extends FormRequest
*/
public function rules(): array
{
// fixed
/** @var Carbon $sessionFirst */
$sessionFirst = clone session('first');
$first = $sessionFirst->subDay()->format('Y-m-d');
$today = today(config('app.timezone'))->addDay()->format('Y-m-d');
return [
'start' => 'required|date|after:'.$first,
'end' => 'required|date|before:'.$today,
'start' => 'required|date|after:1900-01-01|before:2099-12-31|before:end|required_with:end',
'end' => 'required|date|after:1900-01-01|before:2099-12-31|after:start|required_with:start',
'accounts' => 'required',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];

View File

@@ -31,6 +31,7 @@ use FireflyIII\Events\Model\BudgetLimit\Created;
use FireflyIII\Events\Model\BudgetLimit\Deleted;
use FireflyIII\Events\Model\BudgetLimit\Updated;
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\NewVersionAvailable;
@@ -210,6 +211,9 @@ class EventServiceProvider extends ServiceProvider
ChangedAmount::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
],
ChangedName::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
],
// budget related events: CRUD budget limit
Created::class => [

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\PiggyBank;
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
use FireflyIII\Events\Model\PiggyBank\ChangedName;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\PiggyBankFactory;
use FireflyIII\Models\Account;
@@ -66,7 +67,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.
@@ -89,7 +90,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.
@@ -121,13 +122,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)));
@@ -139,7 +140,7 @@ trait ModifiesPiggyBanks
{
$savedSoFar = $this->getCurrentAmount($piggyBank, $account);
return bccomp($amount, (string) $savedSoFar) <= 0;
return bccomp($amount, (string)$savedSoFar) <= 0;
}
/**
@@ -170,7 +171,7 @@ trait ModifiesPiggyBanks
if (1 === bccomp($amount, $max) && 0 !== bccomp($piggyBank->target_amount, '0')) {
$amount = $max;
}
$difference = bcsub($amount, (string) $repetition->current_amount);
$difference = bcsub($amount, (string)$repetition->current_amount);
$repetition->current_amount = $amount;
$repetition->save();
@@ -211,12 +212,12 @@ trait ModifiesPiggyBanks
{
$piggyBank = $this->updateProperties($piggyBank, $data);
if (array_key_exists('notes', $data)) {
$this->updateNote($piggyBank, (string) $data['notes']);
$this->updateNote($piggyBank, (string)$data['notes']);
}
// update the order of the piggy bank:
$oldOrder = $piggyBank->order;
$newOrder = (int) ($data['order'] ?? $oldOrder);
$newOrder = (int)($data['order'] ?? $oldOrder);
if ($oldOrder !== $newOrder) {
$this->setOrder($piggyBank, $newOrder);
}
@@ -233,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));
@@ -248,7 +249,7 @@ trait ModifiesPiggyBanks
// update using name:
if (array_key_exists('object_group_title', $data)) {
$objectGroupTitle = (string) $data['object_group_title'];
$objectGroupTitle = (string)$data['object_group_title'];
if ('' !== $objectGroupTitle) {
$objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle);
if (null !== $objectGroup) {
@@ -264,7 +265,7 @@ trait ModifiesPiggyBanks
// try also with ID:
if (array_key_exists('object_group_id', $data)) {
$objectGroupId = (int) ($data['object_group_id'] ?? 0);
$objectGroupId = (int)($data['object_group_id'] ?? 0);
if (0 !== $objectGroupId) {
$objectGroup = $this->findObjectGroupById($objectGroupId);
if (null !== $objectGroup) {
@@ -282,6 +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']));
$piggyBank->name = $data['name'];
}
if (array_key_exists('transaction_currency_id', $data) && is_int($data['transaction_currency_id'])) {
@@ -366,14 +368,14 @@ trait ModifiesPiggyBanks
foreach ($piggyBank->accounts as $account) {
$current = $account->pivot->current_amount;
// if this account contains more than the amount, remove the amount and return.
if (1 === bccomp((string) $current, $amount)) {
if (1 === bccomp((string)$current, $amount)) {
$this->removeAmount($piggyBank, $account, $amount);
return;
}
// if this account contains less than the amount, remove the current amount, update the amount and continue.
$this->removeAmount($piggyBank, $account, $current);
$amount = bcsub($amount, (string) $current);
$amount = bcsub($amount, (string)$current);
}
}
}

View File

@@ -217,7 +217,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
'link' => $entry->outward,
'group' => $entry->destination->transaction_group_id,
'description' => $entry->destination->description,
'editable' => 1 === (int) $entry->editable,
'editable' => 1 === (int)$entry->editable,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
];
@@ -230,7 +230,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
'link' => $entry->inward,
'group' => $entry->source->transaction_group_id,
'description' => $entry->source->description,
'editable' => 1 === (int) $entry->editable,
'editable' => 1 === (int)$entry->editable,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
];
@@ -264,7 +264,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
if (null === $transaction->foreign_amount || '' === $transaction->foreign_amount) {
return '';
}
if (0 === bccomp('0', (string) $transaction->foreign_amount)) {
if (0 === bccomp('0', (string)$transaction->foreign_amount)) {
return '';
}
$currency = $transaction->foreignCurrency;
@@ -305,7 +305,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
$return = [];
foreach ($query as $row) {
$return[$row->name] = new Carbon(json_decode((string) $row->data, true, 512, JSON_THROW_ON_ERROR));
$return[$row->name] = new Carbon(json_decode((string)$row->data, true, 512, JSON_THROW_ON_ERROR));
}
return new NullArrayObject($return);
@@ -325,7 +325,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
$return = [];
foreach ($query as $row) {
$return[$row->name] = json_decode((string) $row->data);
$return[$row->name] = json_decode((string)$row->data);
}
return new NullArrayObject($return);
@@ -433,20 +433,22 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
return $service->update($transactionGroup, $data);
}
public function getTotalAmount(TransactionGroup $group): string
public function getCompareHash(TransactionGroup $group): string
{
$sum = '0';
$sum = '0';
$names = '';
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
if (-1 === bccomp('0', (string) $transaction->amount)) {
$sum = bcadd($sum, $transaction->amount);
if (-1 === bccomp('0', (string)$transaction->amount)) {
$sum = bcadd($sum, $transaction->amount);
$names = sprintf('%s%s', $names, $transaction->account->name);
}
}
}
return $sum;
return hash('sha256', sprintf('%s-%s', $names, $sum));
}
}

View File

@@ -49,7 +49,10 @@ interface TransactionGroupRepositoryInterface
{
public function countAttachments(int $journalId): int;
public function getTotalAmount(TransactionGroup $group): string;
/**
* Small method that returns a hash that can be used to compare two transaction groups.
*/
public function getCompareHash(TransactionGroup $group): string;
public function destroy(TransactionGroup $group): void;

View File

@@ -43,6 +43,7 @@ use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\JournalServiceTrait;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\NullArrayObject;
@@ -60,6 +61,7 @@ class JournalUpdateService
private BillRepositoryInterface $billRepository;
private CurrencyRepositoryInterface $currencyRepository;
private TransactionGroupRepositoryInterface $transactionGroupRepository;
private array $data;
private ?Account $destinationAccount;
private ?Transaction $destinationTransaction;
@@ -69,26 +71,27 @@ class JournalUpdateService
private ?Transaction $sourceTransaction;
private ?TransactionGroup $transactionGroup;
private ?TransactionJournal $transactionJournal;
private bool $amountChanged = false;
private string $startCompareHash = '';
/**
* JournalUpdateService constructor.
*/
public function __construct()
{
$this->destinationAccount = null;
$this->destinationTransaction = null;
$this->sourceAccount = null;
$this->sourceTransaction = null;
$this->transactionGroup = null;
$this->transactionJournal = null;
$this->billRepository = app(BillRepositoryInterface::class);
$this->categoryRepository = app(CategoryRepositoryInterface::class);
$this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->tagFactory = app(TagFactory::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->metaString = [
$this->destinationAccount = null;
$this->destinationTransaction = null;
$this->sourceAccount = null;
$this->sourceTransaction = null;
$this->transactionGroup = null;
$this->transactionJournal = null;
$this->billRepository = app(BillRepositoryInterface::class);
$this->categoryRepository = app(CategoryRepositoryInterface::class);
$this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->tagFactory = app(TagFactory::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->transactionGroupRepository = app(TransactionGroupRepositoryInterface::class);
$this->metaString = [
'sepa_cc',
'sepa_ct_op',
'sepa_ct_id',
@@ -103,7 +106,7 @@ class JournalUpdateService
'external_id',
'external_url',
];
$this->metaDate = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date',
$this->metaDate = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date',
'invoice_date', ];
}
@@ -120,10 +123,12 @@ class JournalUpdateService
$this->budgetRepository->setUser($transactionGroup->user);
$this->tagFactory->setUser($transactionGroup->user);
$this->accountRepository->setUser($transactionGroup->user);
$this->transactionGroupRepository->setUser($transactionGroup->user);
$this->destinationAccount = null;
$this->destinationTransaction = null;
$this->sourceAccount = null;
$this->sourceTransaction = null;
$this->startCompareHash = $this->transactionGroupRepository->getCompareHash($transactionGroup);
}
public function setTransactionJournal(TransactionJournal $transactionJournal): void
@@ -675,7 +680,6 @@ class JournalUpdateService
return;
}
$origSourceTransaction = $this->getSourceTransaction();
$this->amountChanged = 0 !== bccomp($origSourceTransaction->amount, app('steam')->negative($amount));
$origSourceTransaction->amount = app('steam')->negative($amount);
$origSourceTransaction->balance_dirty = true;
$origSourceTransaction->save();
@@ -818,8 +822,13 @@ class JournalUpdateService
return false;
}
public function isAmountChanged(): bool
public function isCompareHashChanged(): bool
{
return $this->amountChanged;
Log::debug(sprintf('Now in %s', __METHOD__));
$compareHash = $this->transactionGroupRepository->getCompareHash($this->transactionGroup);
Log::debug(sprintf('Compare hash is "%s".', $compareHash));
Log::debug(sprintf('Start compare hash is "%s".', $this->startCompareHash));
return $compareHash !== $this->startCompareHash;
}
}

View File

@@ -79,12 +79,12 @@ class ExchangeRateConverter
public function getCurrencyRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
{
if (false === $this->enabled()) {
Log::debug('ExchangeRateConverter: disabled, return "1".');
// Log::debug('ExchangeRateConverter: disabled, return "1".');
return '1';
}
if ($from->id === $to->id) {
Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
// Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
return '1';
}

View File

@@ -35,6 +35,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Debug\Timer;
use Illuminate\Support\Facades\Log;
/**
* Trait PeriodOverview.
@@ -77,6 +78,7 @@ trait PeriodOverview
*/
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
Log::debug('Now in getAccountPeriodOverview()');
Timer::start('account-period-total');
$this->accountRepository = app(AccountRepositoryInterface::class);
$range = app('navigation')->getViewRange(true);
@@ -100,6 +102,7 @@ trait PeriodOverview
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
// loop dates
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
@@ -119,6 +122,7 @@ trait PeriodOverview
}
$cache->store($entries);
Timer::stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
}

View File

@@ -67,6 +67,16 @@ class UpdatePiggyBank implements ActionInterface
Log::debug(sprintf('Found piggy bank #%d ("%s")', $piggyBank->id, $piggyBank->name));
// piggy bank already has an event for this transaction journal?
if ($this->alreadyEventPresent($piggyBank, $journal)) {
Log::info(sprintf('Piggy bank #%d ("%s") already has an event for transaction journal #%d, so no action will be taken.', $piggyBank->id, $piggyBank->name, $journalObj->id));
event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.cannot_find_piggy', ['name' => $actionValue])));
return false;
}
/** @var Transaction $destination */
$destination = $journalObj->transactions()->where('amount', '>', 0)->first();
@@ -231,4 +241,9 @@ class UpdatePiggyBank implements ActionInterface
$repository->addAmount($piggyBank, $account, $amount, $journal);
}
private function alreadyEventPresent(PiggyBank $piggyBank, array $journal): bool
{
return $piggyBank->piggyBankEvents()->where('transaction_journal_id', $journal['transaction_journal_id'])->exists();
}
}

36
composer.lock generated
View File

@@ -129,16 +129,16 @@
},
{
"name": "brick/math",
"version": "0.12.3",
"version": "0.13.1",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
"url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04",
"shasum": ""
},
"require": {
@@ -177,7 +177,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.12.3"
"source": "https://github.com/brick/math/tree/0.13.1"
},
"funding": [
{
@@ -185,7 +185,7 @@
"type": "github"
}
],
"time": "2025-02-28T13:11:00+00:00"
"time": "2025-03-29T13:50:30+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -1879,20 +1879,20 @@
},
{
"name": "laravel/framework",
"version": "v12.16.0",
"version": "v12.17.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "293bb1c70224faebfd3d4328e201c37115da055f"
"reference": "8729d084510480fdeec9b6ad198180147d4a7f06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/293bb1c70224faebfd3d4328e201c37115da055f",
"reference": "293bb1c70224faebfd3d4328e201c37115da055f",
"url": "https://api.github.com/repos/laravel/framework/zipball/8729d084510480fdeec9b6ad198180147d4a7f06",
"reference": "8729d084510480fdeec9b6ad198180147d4a7f06",
"shasum": ""
},
"require": {
"brick/math": "^0.11|^0.12",
"brick/math": "^0.11|^0.12|^0.13",
"composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4",
@@ -2090,7 +2090,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-05-27T15:49:44+00:00"
"time": "2025-06-03T14:04:18+00:00"
},
{
"name": "laravel/passport",
@@ -3880,16 +3880,16 @@
},
{
"name": "nette/utils",
"version": "v4.0.6",
"version": "v4.0.7",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "ce708655043c7050eb050df361c5e313cf708309"
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309",
"reference": "ce708655043c7050eb050df361c5e313cf708309",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"shasum": ""
},
"require": {
@@ -3960,9 +3960,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.6"
"source": "https://github.com/nette/utils/tree/v4.0.7"
},
"time": "2025-03-30T21:06:30+00:00"
"time": "2025-06-03T04:55:08+00:00"
},
{
"name": "nunomaduro/collision",

View File

@@ -78,7 +78,7 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-06-01',
'version' => 'develop/2025-06-03',
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 25,

56
package-lock.json generated
View File

@@ -43,9 +43,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz",
"integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==",
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -94,13 +94,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
"integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.3",
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
@@ -406,9 +406,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz",
"integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==",
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -683,9 +683,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz",
"integrity": "sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ==",
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz",
"integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1255,9 +1255,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.4.tgz",
"integrity": "sha512-Glp/0n8xuj+E1588otw5rjJkTXfzW7FjH3IIUrfqiZOPQCd2vbg8e+DQE8jK9g4V5/zrxFW+D9WM9gboRPELpQ==",
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz",
"integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5632,9 +5632,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.161",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz",
"integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==",
"version": "1.5.162",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.162.tgz",
"integrity": "sha512-hQA+Zb5QQwoSaXJWEAGEw1zhk//O7qDzib05Z4qTqZfNju/FAkrm5ZInp0JbTp4Z18A6bilopdZWEYrFSsfllA==",
"dev": true,
"license": "ISC"
},
@@ -7713,9 +7713,9 @@
}
},
"node_modules/laravel-vite-plugin": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
"integrity": "sha512-R0pJ+IcTVeqEMoKz/B2Ij57QVq3sFTABiFmb06gAwFdivbOgsUtuhX6N2MGLEArajrS3U5JbberzwOe7uXHMHQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
"integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10414,9 +10414,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -12081,9 +12081,9 @@
}
},
"node_modules/webpack/node_modules/webpack-sources": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.0.tgz",
"integrity": "sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.2.tgz",
"integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -490,7 +490,6 @@ let transactions = function () {
// addedSplit, is called from the HTML
// for source account
const renderAccount = function (item, b, c) {
console.log('render account');
return item.name_with_balance + '<br><small class="text-muted">' + i18next.t('firefly.account_type_' + item.type) + '</small>';
};
console.log('here we are in');
@@ -499,7 +498,7 @@ let transactions = function () {
serverUrl: urls.account,
onRenderItem: renderAccount,
valueField: 'id',
labelField: 'title',
labelField: 'name_with_balance',
onChange: changeSourceAccount,
onSelectItem: selectSourceAccount,
hiddenValue: this.entries[count].source_account.alpine_name
@@ -509,7 +508,7 @@ let transactions = function () {
serverUrl: urls.account,
account_types: this.filters.destination,
valueField: 'id',
labelField: 'title',
labelField: 'name_with_balance',
onRenderItem: renderAccount,
onChange: changeDestinationAccount,
onSelectItem: selectDestinationAccount

View File

@@ -45,7 +45,7 @@ export function addAutocomplete(options) {
liveServer: true,
};
if (typeof options.account_types !== 'undefined' && options.account_types.length > 0) {
params.serverParams['filter[account_types]'] = options.account_types;
params.serverParams['types'] = options.account_types;
}
if (typeof options.onRenderItem !== 'undefined' && null !== options.onRenderItem) {
console.log('overrule onRenderItem.');

View File

@@ -55,7 +55,7 @@ export function changeDestinationAccount(item, ac) {
export function selectDestinationAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account = {
id: item.id, name: item.title, alpine_name: item.title, type: item.meta.type, currency_code: item.meta.currency_code,
id: item.id, name: item.name, alpine_name: item.name, type: item.type, currency_code: item.currency_code,
};
document.querySelector('#form')._x_dataStack[0].changedDestinationAccount();
}
@@ -78,7 +78,7 @@ export function changeSourceAccount(item, ac) {
export function selectSourceAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = {
id: item.id, name: item.title, alpine_name: item.title, type: item.meta.type, currency_code: item.meta.currency_code,
id: item.id, name: item.name, alpine_name: item.name, type: item.type, currency_code: item.currency_code,
};
document.querySelector('#form')._x_dataStack[0].changedSourceAccount();
}

View File

@@ -36,8 +36,8 @@ export default defineConfig(({command, mode, isSsrBuild, isPreview}) => {
let https = null;
if (command === 'serve') {
https = {
key: fs.readFileSync(`/Users/sander/Sites/vm/tls-certificates/wildcard.sd.internal.key`),
cert: fs.readFileSync(`/Users/sander/Sites/vm/tls-certificates/wildcard.sd.internal.crt`),
key: fs.readFileSync(`/vagrant/tls-certificates/wildcard.sd.internal.key`),
cert: fs.readFileSync(`/vagrant/tls-certificates/wildcard.sd.internal.crt`),
};
}
@@ -83,7 +83,7 @@ export default defineConfig(({command, mode, isSsrBuild, isPreview}) => {
server: {
cors: true,
origin: 'https://localhost:5173',
origin: 'https://192.168.96.154:5173',
watch: {
usePolling: true,
},

View File

@@ -68,6 +68,7 @@ return [
'cannot_find_piggy' => 'Firefly III can\'t find a piggy bank named ":name"',
'no_link_piggy' => 'This transaction\'s accounts are not linked to the piggy bank, so no action will be taken',
'both_link_piggy' => 'This transaction\'s accounts are both linked to the piggy bank, so no action will be taken',
'already_linked' => 'This transaction is already linked to piggy bank ":name"',
'cannot_unlink_tag' => 'Tag ":tag" isn\'t linked to this transaction',
'cannot_find_budget' => 'Firefly III can\'t find budget ":name"',
'cannot_find_category' => 'Firefly III can\'t find category ":name"',

View File

@@ -163,6 +163,7 @@
{% endif %}
</td>
<td style="{{ style|raw }};text-align:right">
{# deposit #}
{% if transaction.transaction_type_type == 'Deposit' %}
{# amount of deposit #}