🤖 Auto commit for release 'develop' on 2026-02-06

This commit is contained in:
JC5
2026-02-06 18:26:14 +01:00
parent df8a406c58
commit e37ef69491
18 changed files with 255 additions and 222 deletions

View File

@@ -43,7 +43,7 @@ use Illuminate\View\View;
*/
class ExecutionController extends Controller
{
private readonly AccountRepositoryInterface $repository;
private readonly AccountRepositoryInterface $repository;
private readonly RuleGroupRepositoryInterface $ruleGroupRepository;
/**
@@ -55,7 +55,7 @@ class ExecutionController extends Controller
$this->repository = app(AccountRepositoryInterface::class);
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$this->middleware(function ($request, $next) {
app('view')->share('title', (string)trans('firefly.rules'));
app('view')->share('title', (string) trans('firefly.rules'));
app('view')->share('mainTitleIcon', 'fa-random');
$this->repository->setUser(auth()->user());
$this->ruleGroupRepository->setUser(auth()->user());
@@ -75,8 +75,8 @@ class ExecutionController extends Controller
// start code
/** @var User $user */
$user = auth()->user();
$accounts = implode(',', $request->get('accounts'));
$user = auth()->user();
$accounts = implode(',', $request->get('accounts'));
// create new rule engine:
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
@@ -84,22 +84,22 @@ class ExecutionController extends Controller
// add date operators.
if (null !== $request->get('start')) {
$startDate = new Carbon($request->get('start'));
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
}
if (null !== $request->get('end')) {
$endDate = new Carbon($request->get('end'));
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
}
// add extra operators:
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);
// set rules:
$rules = $this->ruleGroupRepository->getActiveRules($ruleGroup);
$rules = $this->ruleGroupRepository->getActiveRules($ruleGroup);
$newRuleEngine->setRules($rules);
$newRuleEngine->fire();
$resultCount = $newRuleEngine->getResults();
$resultCount = $newRuleEngine->getResults();
// Tell the user that the job is queued
session()->flash('success', trans_choice('firefly.applied_rule_group_selection', $resultCount, ['title' => $ruleGroup->title]));
@@ -112,10 +112,10 @@ class ExecutionController extends Controller
*
* @return Factory|View
*/
public function selectTransactions(RuleGroup $ruleGroup): Factory | \Illuminate\Contracts\View\View
public function selectTransactions(RuleGroup $ruleGroup): Factory|\Illuminate\Contracts\View\View
{
$subTitle = (string)trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]);
$subTitle = (string) trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]);
return view('rules.rule-group.select-transactions', ['ruleGroup' => $ruleGroup, 'subTitle' => $subTitle]);
return view('rules.rule-group.select-transactions', ['ruleGroup' => $ruleGroup, 'subTitle' => $subTitle]);
}
}

View File

@@ -192,7 +192,6 @@ class ShowController extends Controller
{
$amounts = [];
foreach ($group['transactions'] as $transaction) {
// add normal amount:
$symbol = $transaction['currency_symbol'];
$amounts[$symbol] ??= ['amount' => '0', 'symbol' => $symbol, 'decimal_places' => $transaction['currency_decimal_places']];
@@ -214,7 +213,11 @@ class ShowController extends Controller
$amounts[$foreignSymbol]['amount'] = bcadd($amounts[$foreignSymbol]['amount'], (string) $transaction['foreign_amount']);
}
// add primary currency amount, but only if it is not the foreign amount or the current one.
if (null !== $transaction['pc_amount'] && $transaction['currency_id'] !== $this->primaryCurrency->id && $transaction['foreign_currency_code'] !== $this->primaryCurrency->code) {
if (
null !== $transaction['pc_amount']
&& $transaction['currency_id'] !== $this->primaryCurrency->id
&& $transaction['foreign_currency_code'] !== $this->primaryCurrency->code
) {
// same for foreign currency:
$primarySymbol = $this->primaryCurrency->symbol;
$amounts[$primarySymbol] ??= [

View File

@@ -60,9 +60,9 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
*/
public function budgeted(Carbon $start, Carbon $end, TransactionCurrency $currency, ?Collection $budgets = null): string
{
$query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
$query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
// same complex where query as below.
->where(static function (Builder $q5) use ($start, $end): void {
->where(static function (Builder $q5) use ($start, $end): void {
$q5->where(static function (Builder $q1) use ($start, $end): void {
$q1->where(static function (Builder $q2) use ($start, $end): void {
$q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d'));
@@ -77,10 +77,11 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
$q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d'));
});
})
->where('budget_limits.transaction_currency_id', $currency->id)
->whereNull('budgets.deleted_at')
->where('budgets.active', true)
->where('budgets.user_id', $this->user->id);
->where('budget_limits.transaction_currency_id', $currency->id)
->whereNull('budgets.deleted_at')
->where('budgets.active', true)
->where('budgets.user_id', $this->user->id)
;
if ($budgets instanceof Collection && $budgets->count() > 0) {
$query->whereIn('budget_limits.budget_id', $budgets->pluck('id')->toArray());
}
@@ -91,12 +92,13 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
/** @var BudgetLimit $budgetLimit */
foreach ($set as $budgetLimit) {
if ($budgetLimit->start_date->isSameDay($start) && $budgetLimit->end_date->isSameDay($end)) {
$result = bcadd((string)$budgetLimit->amount, $result);
$result = bcadd((string) $budgetLimit->amount, $result);
continue;
}
$period = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$amountPerDay = $this->getDailyAmount($budgetLimit);
$result = bcadd($result, bcmul((string)$period->length(), $amountPerDay));
$result = bcadd($result, bcmul((string) $period->length(), $amountPerDay));
}
return $result;
@@ -137,7 +139,8 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
->where('transaction_currency_id', $currency->id)
->where('start_date', $start->format('Y-m-d'))
->where('end_date', $end->format('Y-m-d'))
->first();
->first()
;
}
public function getAllBudgetLimits(?Carbon $start = null, ?Carbon $end = null): Collection
@@ -145,17 +148,19 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
// both are NULL:
if (!$start instanceof Carbon && !$end instanceof Carbon) {
return BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->whereNull('budgets.deleted_at')
->get(['budget_limits.*']);
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->whereNull('budgets.deleted_at')
->get(['budget_limits.*'])
;
}
// one of the two is NULL.
if (!$start instanceof Carbon xor !$end instanceof Carbon) {
$query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->whereNull('budgets.deleted_at')
->where('budgets.user_id', $this->user->id);
->with(['budget'])
->whereNull('budgets.deleted_at')
->where('budgets.user_id', $this->user->id)
;
if ($end instanceof Carbon) {
// end date must be before $end.
$query->where('end_date', '<=', $end->format('Y-m-d 00:00:00'));
@@ -170,31 +175,32 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
// neither are NULL:
return BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->whereNull('budgets.deleted_at')
->where(static function (Builder $q5) use ($start, $end): void {
$q5->where(static function (Builder $q1) use ($start, $end): void {
$q1->where(static function (Builder $q2) use ($start, $end): void {
$q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d'));
$q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d'));
})->orWhere(static function (Builder $q3) use ($start, $end): void {
$q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d'));
$q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d'));
});
})->orWhere(static function (Builder $q4) use ($start, $end): void {
// or start is before start AND end is after end.
$q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d'));
$q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d'));
});
})
->get(['budget_limits.*']);
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->whereNull('budgets.deleted_at')
->where(static function (Builder $q5) use ($start, $end): void {
$q5->where(static function (Builder $q1) use ($start, $end): void {
$q1->where(static function (Builder $q2) use ($start, $end): void {
$q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d'));
$q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d'));
})->orWhere(static function (Builder $q3) use ($start, $end): void {
$q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d'));
$q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d'));
});
})->orWhere(static function (Builder $q4) use ($start, $end): void {
// or start is before start AND end is after end.
$q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d'));
$q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d'));
});
})
->get(['budget_limits.*'])
;
}
public function getAllBudgetLimitsByCurrency(TransactionCurrency $currency, ?Carbon $start = null, ?Carbon $end = null): Collection
{
return $this->getAllBudgetLimits($start, $end)->filter(
static fn(BudgetLimit $budgetLimit): bool => $budgetLimit->transaction_currency_id === $currency->id
static fn (BudgetLimit $budgetLimit): bool => $budgetLimit->transaction_currency_id === $currency->id
);
}
@@ -233,7 +239,8 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
->orWhere(static function (Builder $q3) use ($start, $end): void {
$q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
$q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 23:59:59'));
});
})
;
})->orWhere(static function (Builder $q4) use ($start, $end): void {
// or start is before start AND end is after end.
$q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 23:59:59'));
@@ -241,21 +248,22 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
});
})
->orderBy('budget_limits.start_date', 'DESC')
->get(['budget_limits.*']);
->get(['budget_limits.*'])
;
}
public function getDailyAmount(BudgetLimit $budgetLimit): string
{
$limitPeriod = Period::make($budgetLimit->start_date, $budgetLimit->end_date, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$days = $limitPeriod->length();
$amount = bcdiv($budgetLimit->amount, (string)$days, 12);
$amount = bcdiv($budgetLimit->amount, (string) $days, 12);
Log::debug(sprintf(
'Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s',
$budgetLimit->id,
$budgetLimit->amount,
$days,
$amount
));
'Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s',
$budgetLimit->id,
$budgetLimit->amount,
$days,
$amount
));
return $amount;
}
@@ -263,7 +271,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
#[Override]
public function getNoteText(BudgetLimit $budgetLimit): string
{
return (string)$budgetLimit->notes()->first()?->text;
return (string) $budgetLimit->notes()->first()?->text;
}
#[Override]
@@ -290,39 +298,40 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
{
// if no currency has been provided, use the user's default currency:
/** @var TransactionCurrencyFactory $factory */
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$currency->enabled = true;
$currency->enabled = true;
$currency->save();
// find the budget:
/** @var null|Budget $budget */
$budget = $this->user->budgets()->find((int)$data['budget_id']);
$budget = $this->user->budgets()->find((int) $data['budget_id']);
if (null === $budget) {
throw new FireflyException('200004: Budget does not exist.');
}
// find limit with same date range and currency.
$limit = $budget
$limit = $budget
->budgetlimits()
->where('budget_limits.start_date', $data['start_date']->format('Y-m-d'))
->where('budget_limits.end_date', $data['end_date']->format('Y-m-d'))
->where('budget_limits.transaction_currency_id', $currency->id)
->first(['budget_limits.*']);
->first(['budget_limits.*'])
;
if (null !== $limit) {
throw new FireflyException('200027: Budget limit already exists.');
}
Log::debug('No existing budget limit, create a new one');
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
$singleton = PreferencesSingleton::getInstance();
$singleton->setPreference('fire_webhooks_bl_store', $data['fire_webhooks'] ?? true);
// or create one and return it.
$limit = new BudgetLimit();
$limit = new BudgetLimit();
$limit->budget()->associate($budget);
$limit->start_date = $data['start_date']->format('Y-m-d');
$limit->end_date = $data['end_date']->format('Y-m-d');
@@ -332,13 +341,13 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
$limit->transaction_currency_id = $currency->id;
$limit->save();
$noteText = (string)($data['notes'] ?? '');
$noteText = (string) ($data['notes'] ?? '');
if ('' !== $noteText) {
$this->setNoteText($limit, $noteText);
}
Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount']));
$createWebhookMessages = $data['fire_webhooks'] ?? true;
$createWebhookMessages = $data['fire_webhooks'] ?? true;
event(new CreatedBudgetLimit($limit, $createWebhookMessages));
event(new WebhookMessagesRequestSending());
@@ -350,8 +359,8 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
*/
public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit
{
$budgetLimit->amount = array_key_exists('amount', $data) ? $data['amount'] : $budgetLimit->amount;
$budgetLimit->budget_id = array_key_exists('budget_id', $data) ? $data['budget_id'] : $budgetLimit->budget_id;
$budgetLimit->amount = array_key_exists('amount', $data) ? $data['amount'] : $budgetLimit->amount;
$budgetLimit->budget_id = array_key_exists('budget_id', $data) ? $data['budget_id'] : $budgetLimit->budget_id;
if (array_key_exists('start', $data)) {
$budgetLimit->start_date = $data['start']->startOfDay();
@@ -363,7 +372,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
}
// if no currency has been provided, use the user's default currency:
$currency = null;
$currency = null;
// update if relevant:
if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) {
@@ -375,12 +384,12 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
if (null === $currency) {
$currency = $budgetLimit->transactionCurrency ?? Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$currency->enabled = true;
$currency->enabled = true;
$currency->save();
// this is a lame trick to communicate with the observer.
// FIXME so don't do that lol.
$singleton = PreferencesSingleton::getInstance();
$singleton = PreferencesSingleton::getInstance();
$singleton->setPreference('fire_webhooks_bl_update', $data['fire_webhooks'] ?? true);
$budgetLimit->transaction_currency_id = $currency->id;
@@ -388,9 +397,9 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
// update notes if they exist.
if (array_key_exists('notes', $data)) {
$this->setNoteText($budgetLimit, (string)$data['notes']);
$this->setNoteText($budgetLimit, (string) $data['notes']);
}
$generateMessages = $data['fire_webhooks'] ?? true;
$generateMessages = $data['fire_webhooks'] ?? true;
event(new UpdatedBudgetLimit($budgetLimit, $generateMessages));
event(new WebhookMessagesRequestSending());

View File

@@ -62,8 +62,8 @@ trait AccountServiceTrait
if (null === $iban) {
return null;
}
$data = ['iban' => $iban];
$rules = ['iban' => 'required|iban'];
$data = ['iban' => $iban];
$rules = ['iban' => 'required|iban'];
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
Log::info(sprintf('Detected invalid IBAN ("%s"). Return NULL instead.', $iban));
@@ -89,7 +89,7 @@ trait AccountServiceTrait
array_key_exists('opening_balance', $data)
&& '' === $data['opening_balance']
|| array_key_exists('opening_balance_date', $data)
&& '' === $data['opening_balance_date'];
&& '' === $data['opening_balance_date'];
}
/**
@@ -101,14 +101,14 @@ trait AccountServiceTrait
*/
public function updateMetaData(Account $account, array $data): void
{
$fields = $this->validFields;
$fields = $this->validFields;
if (AccountTypeEnum::ASSET->value === $account->accountType->type) {
$fields = $this->validAssetFields;
}
// remove currency_id if necessary.
$type = $account->accountType->type;
$list = config('firefly.valid_currency_account_types');
$type = $account->accountType->type;
$list = config('firefly.valid_currency_account_types');
if (!in_array($type, $list, true)) {
$pos = array_search('currency_id', $fields, true);
if (false !== $pos) {
@@ -150,14 +150,14 @@ trait AccountServiceTrait
$data[$field] = $data[$field]->toAtomString();
}
$factory->crud($account, $field, (string)$data[$field]);
$factory->crud($account, $field, (string) $data[$field]);
}
}
}
public function updateNote(Account $account, string $note): bool
{
$dbNote = $account->notes()->first();
$dbNote = $account->notes()->first();
if ('' === $note) {
$dbNote?->delete();
@@ -178,7 +178,7 @@ trait AccountServiceTrait
*/
public function validOBData(array $data): bool
{
$data['opening_balance'] = (string)($data['opening_balance'] ?? '');
$data['opening_balance'] = (string) ($data['opening_balance'] ?? '');
if ('' !== $data['opening_balance'] && 0 === bccomp($data['opening_balance'], '0')) {
$data['opening_balance'] = '';
}
@@ -210,11 +210,11 @@ trait AccountServiceTrait
throw new FireflyException('Amount for new liability credit was unexpectedly 0.');
}
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$language = (string) $language;
// set source and/or destination based on whether the amount is positive or negative.
// first, assume the amount is positive and go from there:
@@ -228,14 +228,14 @@ trait AccountServiceTrait
$sourceId = $account->id;
$sourceName = null;
$destId = null;
$destName = trans('firefly.liability_credit_description', ['account' => $account->name], $language);
$destName = trans('firefly.liability_credit_description', ['account' => $account->name], $language);
}
// amount must be positive for the transaction to work.
$amount = Steam::positive($openingBalance);
$amount = Steam::positive($openingBalance);
// get or grab currency:
$currency = $this->accountRepository->getAccountCurrency($account);
$currency = $this->accountRepository->getAccountCurrency($account);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
@@ -245,34 +245,34 @@ trait AccountServiceTrait
'user' => $account->user,
'user_group' => $account->user->userGroup,
'transactions' => [[
'type' => 'Liability credit',
'date' => $openingBalanceDate,
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.liability_credit_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
'type' => 'Liability credit',
'date' => $openingBalanceDate,
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.liability_credit_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
];
Log::debug('Going for submission in createCreditTransaction', $submission);
/** @var TransactionGroupFactory $factory */
$factory = app(TransactionGroupFactory::class);
$factory = app(TransactionGroupFactory::class);
$factory->setUser($account->user);
try {
@@ -295,11 +295,11 @@ trait AccountServiceTrait
protected function createOBGroup(Account $account, array $data): TransactionGroup
{
Log::debug('Now going to create an OB group.');
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$language = (string) $language;
$sourceId = null;
$sourceName = null;
$destId = null;
@@ -307,29 +307,29 @@ trait AccountServiceTrait
$amount = array_key_exists('opening_balance', $data) ? $data['opening_balance'] : '0';
// amount is positive.
if (1 === bccomp((string)$amount, '0')) {
if (1 === bccomp((string) $amount, '0')) {
Log::debug(sprintf('Amount is %s, which is positive. Source is a new IB account, destination is #%d', $amount, $account->id));
$sourceName = trans('firefly.initial_balance_description', ['account' => $account->name], $language);
$destId = $account->id;
}
// amount is not positive
if (-1 === bccomp((string)$amount, '0')) {
if (-1 === bccomp((string) $amount, '0')) {
Log::debug(sprintf('Amount is %s, which is negative. Destination is a new IB account, source is #%d', $amount, $account->id));
$destName = trans('firefly.initial_balance_account', ['account' => $account->name], $language);
$sourceId = $account->id;
}
// amount is 0
if (0 === bccomp((string)$amount, '0')) {
if (0 === bccomp((string) $amount, '0')) {
Log::debug('Amount is zero, so will not make an OB group.');
throw new FireflyException('Amount for new opening balance was unexpectedly 0.');
}
// make amount positive, regardless:
$amount = Steam::positive($amount);
$amount = Steam::positive($amount);
// get or grab currency:
$currency = $this->accountRepository->getAccountCurrency($account);
$currency = $this->accountRepository->getAccountCurrency($account);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
@@ -340,34 +340,34 @@ trait AccountServiceTrait
'user' => $account->user,
'user_group' => $account->user->userGroup,
'transactions' => [[
'type' => 'Opening balance',
'date' => $data['opening_balance_date'],
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.initial_balance_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
'type' => 'Opening balance',
'date' => $data['opening_balance_date'],
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.initial_balance_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
];
Log::debug('Going for submission in createOBGroup', $submission);
/** @var TransactionGroupFactory $factory */
$factory = app(TransactionGroupFactory::class);
$factory = app(TransactionGroupFactory::class);
$factory->setUser($account->user);
try {
@@ -388,15 +388,15 @@ trait AccountServiceTrait
protected function createOBGroupV2(Account $account, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup
{
Log::debug('Now going to create an OB group.');
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
$language = Preferences::getForUser($account->user, 'language', 'en_US')->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$sourceId = null;
$sourceName = null;
$destId = null;
$destName = null;
$language = (string) $language;
$sourceId = null;
$sourceName = null;
$destId = null;
$destName = null;
// amount is positive.
if (1 === bccomp($openingBalance, '0')) {
@@ -418,48 +418,48 @@ trait AccountServiceTrait
}
// make amount positive, regardless:
$amount = Steam::positive($openingBalance);
$amount = Steam::positive($openingBalance);
// get or grab currency:
$currency = $this->accountRepository->getAccountCurrency($account);
$currency = $this->accountRepository->getAccountCurrency($account);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
// submit to factory:
$submission = [
$submission = [
'group_title' => null,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'transactions' => [[
'type' => 'Opening balance',
'date' => $openingBalanceDate,
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.initial_balance_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
'type' => 'Opening balance',
'date' => $openingBalanceDate,
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destId,
'destination_name' => $destName,
'user' => $account->user,
'user_group' => $account->user->userGroup,
'currency_id' => $currency->id,
'order' => 0,
'amount' => $amount,
'foreign_amount' => null,
'description' => trans('firefly.initial_balance_description', ['account' => $account->name]),
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'reconciled' => false,
'notes' => null,
'tags' => [],
]],
];
Log::debug('Going for submission in createOBGroupV2', $submission);
/** @var TransactionGroupFactory $factory */
$factory = app(TransactionGroupFactory::class);
$factory = app(TransactionGroupFactory::class);
$factory->setUser($account->user);
try {
@@ -530,10 +530,10 @@ trait AccountServiceTrait
{
// find currency, or use default currency instead.
/** @var TransactionCurrencyFactory $factory */
$factory = app(TransactionCurrencyFactory::class);
$factory = app(TransactionCurrencyFactory::class);
/** @var null|TransactionCurrency $currency */
$currency = $factory->find($currencyId, $currencyCode);
$currency = $factory->find($currencyId, $currencyCode);
if (null === $currency) {
// use default currency:
@@ -569,7 +569,7 @@ trait AccountServiceTrait
}
// if direction is "debit" (I owe this debt), amount is negative.
// which means the liability will have a negative balance which the user must fill.
$openingBalance = Steam::negative($openingBalance);
$openingBalance = Steam::negative($openingBalance);
// if direction is "credit" (I am owed this debt), amount is positive.
// which means the liability will have a positive balance which is drained when its paid back into any asset.
@@ -578,21 +578,21 @@ trait AccountServiceTrait
}
// create if not exists:
$clGroup = $this->getCreditTransaction($account);
$clGroup = $this->getCreditTransaction($account);
if (null === $clGroup) {
return $this->createCreditTransaction($account, $openingBalance, $openingBalanceDate);
}
// if exists, update:
$currency = $this->accountRepository->getAccountCurrency($account);
$currency = $this->accountRepository->getAccountCurrency($account);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
// simply grab the first journal and change it:
$journal = $this->getObJournal($clGroup);
$clTransaction = $this->getOBTransaction($journal, $account);
$accountTransaction = $this->getNotOBTransaction($journal, $account);
$journal->date = $openingBalanceDate;
$journal = $this->getObJournal($clGroup);
$clTransaction = $this->getOBTransaction($journal, $account);
$accountTransaction = $this->getNotOBTransaction($journal, $account);
$journal->date = $openingBalanceDate;
$journal->transactionCurrency()->associate($currency);
// account always gains money:
@@ -600,8 +600,8 @@ trait AccountServiceTrait
$accountTransaction->transaction_currency_id = $currency->id;
// CL account always loses money:
$clTransaction->amount = Steam::negative($openingBalance);
$clTransaction->transaction_currency_id = $currency->id;
$clTransaction->amount = Steam::negative($openingBalance);
$clTransaction->transaction_currency_id = $currency->id;
// save both
$accountTransaction->save();
$clTransaction->save();
@@ -621,23 +621,23 @@ trait AccountServiceTrait
{
Log::debug(sprintf('Now in %s', __METHOD__));
// create if not exists:
$obGroup = $this->getOBGroup($account);
$obGroup = $this->getOBGroup($account);
if (null === $obGroup) {
return $this->createOBGroupV2($account, $openingBalance, $openingBalanceDate);
}
Log::debug('Update OB group');
// if exists, update:
$currency = $this->accountRepository->getAccountCurrency($account);
$currency = $this->accountRepository->getAccountCurrency($account);
if (null === $currency) {
$currency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
// simply grab the first journal and change it:
$journal = $this->getObJournal($obGroup);
$obTransaction = $this->getOBTransaction($journal, $account);
$accountTransaction = $this->getNotOBTransaction($journal, $account);
$journal->date = $openingBalanceDate;
$journal = $this->getObJournal($obGroup);
$obTransaction = $this->getOBTransaction($journal, $account);
$accountTransaction = $this->getNotOBTransaction($journal, $account);
$journal->date = $openingBalanceDate;
$journal->transactionCurrency()->associate($currency);
// if amount is negative:
@@ -648,8 +648,8 @@ trait AccountServiceTrait
$accountTransaction->transaction_currency_id = $currency->id;
// OB account transaction gains money
$obTransaction->amount = Steam::positive($openingBalance);
$obTransaction->transaction_currency_id = $currency->id;
$obTransaction->amount = Steam::positive($openingBalance);
$obTransaction->transaction_currency_id = $currency->id;
}
if (-1 === bccomp('0', $openingBalance)) {
Log::debug('Amount is positive.');
@@ -658,8 +658,8 @@ trait AccountServiceTrait
$accountTransaction->transaction_currency_id = $currency->id;
// OB account loses money:
$obTransaction->amount = Steam::negative($openingBalance);
$obTransaction->transaction_currency_id = $currency->id;
$obTransaction->amount = Steam::negative($openingBalance);
$obTransaction->transaction_currency_id = $currency->id;
}
// save both
$accountTransaction->save();

View File

@@ -38,6 +38,7 @@ class Calculator
private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private static array $intervals = [];
public function isAvailablePeriodicity(Periodicity $periodicity): bool

View File

@@ -94,6 +94,8 @@ class ExportDataGenerator
// @phpstan-ignore-line
// @phpstan-ignore-line
public function __construct()
{
$this->accounts = new Collection();

View File

@@ -83,12 +83,15 @@ trait PeriodOverview
private Collection $statistics; // temp data holder
// temp data holder
// temp data holder
// temp data holder
private array $transactions; // temp data holder
// temp data holder
// temp data holder
// temp data holder
/**
* This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range)
* and for each period, the amount of money spent and earned. This is a complex operation which is cached for

View File

@@ -43,9 +43,11 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
private Collection $collection; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private readonly bool $convertToPrimary; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $currencies = [];
private array $currencyIds = [];
private array $ids = [];

View File

@@ -43,6 +43,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface
private readonly bool $convertToPrimary; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $currencies = [];
private array $currencyIds = [];
private Carbon $end;

View File

@@ -45,9 +45,11 @@ class PiggyBankEnrichment implements EnrichmentInterface
private array $accountIds = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $accounts = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $amounts = [];
private Collection $collection;
private array $currencies = [];

View File

@@ -40,9 +40,11 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
private array $accountCurrencies = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $accountIds = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private Collection $collection;
private array $currencies = [];
private array $groupIds = [];

View File

@@ -49,6 +49,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
private Collection $collection; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private readonly bool $convertToPrimary;
private ?Carbon $end = null;
private array $mappedObjects = [];

View File

@@ -59,6 +59,8 @@ class TransactionGroupEnrichment implements EnrichmentInterface
// @phpstan-ignore-line
// @phpstan-ignore-line
public function __construct()
{
$this->dateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];

View File

@@ -45,9 +45,11 @@ class WebhookEnrichment implements EnrichmentInterface
private array $deliveries = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $ids = []; // @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
// @phpstan-ignore-line
private array $responses = [];
private array $triggers = [];
private array $webhookDeliveries = [];

View File

@@ -100,7 +100,10 @@ class SearchRuleEngine implements RuleEngineInterface
foreach ($this->rules as $rule) { // @phpstan-ignore-line
$result = $this->fireRule($rule);
if ($result && true === $rule->stop_processing) {
Log::debug(sprintf('Rule #%d has triggered and executed, but calls to stop processing. Since not in the context of a group, do not stop.', $rule->id));
Log::debug(sprintf(
'Rule #%d has triggered and executed, but calls to stop processing. Since not in the context of a group, do not stop.',
$rule->id
));
}
if (false === $result && true === $rule->stop_processing) {
Log::debug(sprintf('Rule #%d has triggered and changed nothing, but calls to stop processing. Do not stop.', $rule->id));

View File

@@ -19,19 +19,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- #11589
- #11541
- #11544
- #11546
- #11431
- #11399
- #11569
- #11563
- #11589
- #11601
- #11614
- #11620
- #11632
- [PR 11589](https://github.com/firefly-iii/firefly-iii/pull/11589) (apply user-selected light/dark mode to form elements (checkboxes, date picker) #8613 #7620) reported by @mateuszkulapl
- [Issue 11541](https://github.com/firefly-iii/firefly-iii/issues/11541) (Display running balance fails for transactions between accounts with different currencies) reported by @SledgehammerPL
- [Issue 11544](https://github.com/firefly-iii/firefly-iii/issues/11544) (Clean up events and handlers) reported by @JC5
- [Issue 11546](https://github.com/firefly-iii/firefly-iii/issues/11546) (Wrong invitation expiry time) reported by @GunoH
- [Discussion 11431](https://github.com/orgs/firefly-iii/discussions/11431) (Settings don't get saved) started by @PVTejas
- [Issue 11399](https://github.com/firefly-iii/firefly-iii/issues/11399) (Unusual behavior in audit logs (multi-currency)) reported by @jgmm81
- [PR 11569](https://github.com/firefly-iii/firefly-iii/pull/11569) (Fix layout overflow issues with long content in v1 and v2 layouts) reported by @gian21391
- [Issue 11563](https://github.com/firefly-iii/firefly-iii/issues/11563) (Tag Report/Insight API Endpoint for Tags Non Functional) reported by @Unsantae
- [PR 11589](https://github.com/firefly-iii/firefly-iii/pull/11589) (apply user-selected light/dark mode to form elements (checkboxes, date picker) #8613 #7620) reported by @mateuszkulapl
- [Issue 11601](https://github.com/firefly-iii/firefly-iii/issues/11601) (Only ungrouped piggy banks are listed when creating a transaction) reported by @jgmm81
- [Issue 11614](https://github.com/firefly-iii/firefly-iii/issues/11614) (Add New Taiwan Dollar to Currency Seeder) reported by @nick322
- [Issue 11620](https://github.com/firefly-iii/firefly-iii/issues/11620) (Add database indexes to improve reporting query performance) reported by @Zakmaf
- [PR 11632](https://github.com/firefly-iii/firefly-iii/pull/11632) (fix v2 layout dashboard transactions load) reported by @mateuszkulapl
- Confirming your new email address would result in an error.
### Security

14
composer.lock generated
View File

@@ -12014,21 +12014,21 @@
},
{
"name": "rector/rector",
"version": "2.3.5",
"version": "2.3.6",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070"
"reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070",
"reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b",
"reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
"phpstan/phpstan": "^2.1.36"
"phpstan/phpstan": "^2.1.38"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -12062,7 +12062,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.3.5"
"source": "https://github.com/rectorphp/rector/tree/2.3.6"
},
"funding": [
{
@@ -12070,7 +12070,7 @@
"type": "github"
}
],
"time": "2026-01-28T15:22:48+00:00"
"time": "2026-02-06T14:25:06+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -79,7 +79,7 @@ return [
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2026-02-06',
'build_time' => 1770383155,
'build_time' => 1770398635,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.