Merge pull request #10707 from firefly-iii/release-1754394813

🤖 Automatically merge the PR into the develop branch.
This commit is contained in:
github-actions[bot]
2025-08-05 13:53:39 +02:00
committed by GitHub
14 changed files with 239 additions and 42 deletions

View File

@@ -87,7 +87,7 @@ class ShowController extends Controller
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$categories = $enrichment->enrich($categories);
$categories = $enrichment->enrich($categories);
// make paginator:
$paginator = new LengthAwarePaginator($categories, $count, $pageSize, $this->parameters->get('page'));
@@ -123,7 +123,7 @@ class ShowController extends Controller
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$category = $enrichment->enrichSingle($category);
$category = $enrichment->enrichSingle($category);
$resource = new Item($category, $transformer, 'categories');

View File

@@ -285,6 +285,7 @@ class CategoryReportController extends Controller
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}

View File

@@ -44,7 +44,6 @@ use Illuminate\View\View;
use function Safe\json_decode;
use function Safe\file_get_contents;
use function Safe\strtotime;
/**
* Class PreferencesController.
@@ -278,8 +277,8 @@ class PreferencesController extends Controller
// custom fiscal year
$customFiscalYear = 1 === (int) $request->get('customFiscalYear');
Preferences::set('customFiscalYear', $customFiscalYear);
$fiscalYearString = (string) $request->get('fiscalYearStart');
if('' !== $fiscalYearString) {
$fiscalYearString = (string) $request->get('fiscalYearStart');
if ('' !== $fiscalYearString) {
$fiscalYearStart = Carbon::parse($fiscalYearString, config('app.timezone'))->format('m-d');
Preferences::set('fiscalYearStart', $fiscalYearStart);
}

View File

@@ -449,7 +449,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
if ($accounts instanceof Collection && $accounts->count() > 0) {
@@ -460,15 +460,16 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
}
$collector->setCategories($categories);
$collector->withCategoryInformation();
return $collector->getExtractedJournals();
}
public function collectIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)
->setTypes([TransactionTypeEnum::DEPOSIT->value])
->setTypes([TransactionTypeEnum::DEPOSIT->value])
;
if ($accounts instanceof Collection && $accounts->count() > 0) {
@@ -478,6 +479,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$categories = $this->getCategories();
}
$collector->setCategories($categories);
return $collector->getExtractedJournals();
}
@@ -486,7 +488,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)
->setTypes([TransactionTypeEnum::TRANSFER->value])
->setTypes([TransactionTypeEnum::TRANSFER->value])
;
if ($accounts instanceof Collection && $accounts->count() > 0) {
@@ -496,6 +498,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$categories = $this->getCategories();
}
$collector->setCategories($categories);
return $collector->getExtractedJournals();
}

View File

@@ -27,7 +27,6 @@ namespace FireflyIII\Repositories\Category;
use Carbon\Carbon;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -81,8 +80,11 @@ interface OperationsRepositoryInterface
public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function sumCollectedTransactionsByCategory(array $expenses, Category $category, string $method, bool $convertToPrimary = false): array;
/**

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
@@ -39,7 +41,7 @@ class CategoryEnrichment implements EnrichmentInterface
return $collection;
}
public function enrichSingle(Model|array $model): array|Model
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
@@ -100,9 +102,10 @@ class CategoryEnrichment implements EnrichmentInterface
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray();
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
@@ -116,9 +119,9 @@ class CategoryEnrichment implements EnrichmentInterface
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);
$opsRepository->setUserGroup($this->userGroup);
$expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection);
$income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection);
$transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection);
$expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection);
$income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection);
$transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection);
foreach ($this->collection as $item) {
$id = (int)$item->id;
$this->spent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', false));
@@ -130,5 +133,4 @@ class CategoryEnrichment implements EnrichmentInterface
}
}
}
}

View File

@@ -583,7 +583,7 @@ class Navigation
*/
public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001) {
return 'endOfMonth';
}
@@ -601,7 +601,7 @@ class Navigation
*/
public function preferredRangeFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001) {
return '1M';
}
@@ -619,7 +619,7 @@ class Navigation
*/
public function preferredSqlFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001) {
return '%Y-%m';
}

View File

@@ -51,14 +51,14 @@ class CategoryTransformer extends AbstractTransformer
{
return [
'id' => $category->id,
'created_at' => $category->created_at->toAtomString(),
'updated_at' => $category->updated_at->toAtomString(),
'name' => $category->name,
'notes' => $category->meta['notes'],
'id' => $category->id,
'created_at' => $category->created_at->toAtomString(),
'updated_at' => $category->updated_at->toAtomString(),
'name' => $category->name,
'notes' => $category->meta['notes'],
// category never has currency settings.
'object_has_currency_setting' => false,
'object_has_currency_setting' => false,
'primary_currency_id' => (string)$this->primaryCurrency->id,
@@ -74,7 +74,7 @@ class CategoryTransformer extends AbstractTransformer
'links' => [
[
'rel' => 'self',
'uri' => '/categories/' . $category->id,
'uri' => '/categories/'.$category->id,
],
],
];

View File

@@ -36,9 +36,9 @@ use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Facades\Log;
use function Safe\json_decode;
/**

10
composer.lock generated
View File

@@ -11128,16 +11128,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.21",
"version": "2.1.22",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6"
"reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6",
"reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4",
"reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4",
"shasum": ""
},
"require": {
@@ -11182,7 +11182,7 @@
"type": "github"
}
],
"time": "2025-07-28T19:35:08+00:00"
"time": "2025-08-04T19:17:37+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",

View File

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

12
package-lock.json generated
View File

@@ -4930,9 +4930,9 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.44.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz",
"integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==",
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5700,9 +5700,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.194",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
"version": "1.5.195",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
"integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
"dev": true,
"license": "ISC"
},

View File

@@ -110,6 +110,8 @@
"/public/v1/js/lib/jquery.autocomplete.min.js": "/public/v1/js/lib/jquery.autocomplete.min.js",
"/public/v1/js/lib/jquery.color-2.1.2.min.js": "/public/v1/js/lib/jquery.color-2.1.2.min.js",
"/public/v1/js/lib/modernizr-custom.js": "/public/v1/js/lib/modernizr-custom.js",
"/public/v1/js/lib/moment/af_ZA.js": "/public/v1/js/lib/moment/af_ZA.js",
"/public/v1/js/lib/moment/ar_SA.js": "/public/v1/js/lib/moment/ar_SA.js",
"/public/v1/js/lib/moment/bg_BG.js": "/public/v1/js/lib/moment/bg_BG.js",
"/public/v1/js/lib/moment/ca_ES.js": "/public/v1/js/lib/moment/ca_ES.js",
"/public/v1/js/lib/moment/cs_CZ.js": "/public/v1/js/lib/moment/cs_CZ.js",

View File

@@ -0,0 +1,188 @@
{
"firefly": {
"administrations_page_title": "Financial administrations",
"administrations_index_menu": "Financial administrations",
"expires_at": "Expires at",
"temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.",
"administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.",
"administrations_page_edit_sub_title_js": "Edit financial administration \"{title}\"",
"table": "Table",
"welcome_back": "What's playing?",
"flash_error": "Error!",
"flash_warning": "Warning!",
"flash_success": "Success!",
"close": "Close",
"select_dest_account": "Please select or type a valid destination account name",
"select_source_account": "Please select or type a valid source account name",
"split_transaction_title": "Description of the split transaction",
"errors_submission": "There was something wrong with your submission. Please check out the errors below.",
"is_reconciled": "Is reconciled",
"split": "Split",
"single_split": "Split",
"not_enough_currencies": "Not enough currencies",
"not_enough_currencies_enabled": "If you have just one currency enabled, there is no need to add exchange rates.",
"transaction_stored_link": "<a href=\"transactions\/show\/{ID}\">Transaction #{ID} (\"{title}\")<\/a> has been stored.",
"webhook_stored_link": "<a href=\"webhooks\/show\/{ID}\">Webhook #{ID} (\"{title}\")<\/a> has been stored.",
"webhook_updated_link": "<a href=\"webhooks\/show\/{ID}\">Webhook #{ID}<\/a> (\"{title}\") has been updated.",
"transaction_updated_link": "<a href=\"transactions\/show\/{ID}\">Transaction #{ID}<\/a> (\"{title}\") has been updated.",
"transaction_new_stored_link": "<a href=\"transactions\/show\/{ID}\">Transaction #{ID}<\/a> has been stored.",
"transaction_journal_information": "Transaction information",
"submission_options": "Submission options",
"apply_rules_checkbox": "Apply rules",
"fire_webhooks_checkbox": "Fire webhooks",
"no_budget_pointer": "You seem to have no budgets yet. You should create some on the <a href=\"budgets\">budgets<\/a>-page. Budgets can help you keep track of expenses.",
"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.",
"source_account": "Source account",
"hidden_fields_preferences": "You can enable more transaction options in your <a href=\"preferences\">preferences<\/a>.",
"destination_account": "Destination account",
"add_another_split": "Add another split",
"submission": "Submission",
"stored_journal": "Successfully created new transaction \":description\"",
"create_another": "After storing, return here to create another one.",
"reset_after": "Reset form after submission",
"submit": "Submit",
"amount": "Amount",
"date": "Date",
"is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s) unless you remove the reconciliation flag.",
"tags": "Tags",
"no_budget": "(no budget)",
"no_bill": "(no subscription)",
"category": "Category",
"attachments": "Attachments",
"notes": "Notes",
"external_url": "External URL",
"update_transaction": "Update transaction",
"after_update_create_another": "After updating, return here to continue editing.",
"store_as_new": "Store as a new transaction instead of updating.",
"split_title_help": "If you create a split transaction, there must be a global description for all splits of the transaction.",
"none_in_select_list": "(none)",
"no_piggy_bank": "(no piggy bank)",
"description": "Description",
"split_transaction_title_help": "If you create a split transaction, there must be a global description for all splits of the transaction.",
"destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.",
"source_account_reconciliation": "You can't edit the source account of a reconciliation transaction.",
"budget": "Budget",
"bill": "Subscription",
"you_create_withdrawal": "You're creating a withdrawal.",
"you_create_transfer": "You're creating a transfer.",
"you_create_deposit": "You're creating a deposit.",
"edit": "Edit",
"delete": "Delete",
"name": "Name",
"profile_whoops": "Whoops!",
"profile_something_wrong": "Something went wrong!",
"profile_try_again": "Something went wrong. Please try again.",
"profile_oauth_clients": "OAuth Clients",
"profile_oauth_no_clients": "You have not created any OAuth clients.",
"profile_oauth_clients_header": "Clients",
"profile_oauth_client_id": "Client ID",
"profile_oauth_client_name": "Name",
"profile_oauth_client_secret": "Secret",
"profile_oauth_create_new_client": "Create New Client",
"profile_oauth_create_client": "Create Client",
"profile_oauth_edit_client": "Edit Client",
"profile_oauth_name_help": "Something your users will recognize and trust.",
"profile_oauth_redirect_url": "Redirect URL",
"profile_oauth_clients_external_auth": "If you're using an external authentication provider like Authelia, OAuth Clients will not work. You can use Personal Access Tokens only.",
"profile_oauth_redirect_url_help": "Your application's authorization callback URL.",
"profile_authorized_apps": "Authorized applications",
"profile_authorized_clients": "Authorized clients",
"profile_scopes": "Scopes",
"profile_revoke": "Revoke",
"profile_personal_access_tokens": "Personal Access Tokens",
"profile_personal_access_token": "Personal Access Token",
"profile_personal_access_token_explanation": "Here is your new personal access token. This is the only time it will be shown so don't lose it! You may now use this token to make API requests.",
"profile_no_personal_access_token": "You have not created any personal access tokens.",
"profile_create_new_token": "Create new token",
"profile_create_token": "Create token",
"profile_create": "Create",
"profile_save_changes": "Save changes",
"default_group_title_name": "(ungrouped)",
"piggy_bank": "Piggy bank",
"profile_oauth_client_secret_title": "Client Secret",
"profile_oauth_client_secret_expl": "Here is your new client secret. This is the only time it will be shown so don't lose it! You may now use this secret to make API requests.",
"profile_oauth_confidential": "Confidential",
"profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as primary desktop or JavaScript SPA applications, are unable to hold secrets securely.",
"multi_account_warning_unknown": "Depending on the type of transaction you create, the source and\/or destination account of subsequent splits may be overruled by whatever is defined in the first split of the transaction.",
"multi_account_warning_withdrawal": "Keep in mind that the source account of subsequent splits will be overruled by whatever is defined in the first split of the withdrawal.",
"multi_account_warning_deposit": "Keep in mind that the destination account of subsequent splits will be overruled by whatever is defined in the first split of the deposit.",
"multi_account_warning_transfer": "Keep in mind that the source + destination account of subsequent splits will be overruled by whatever is defined in the first split of the transfer.",
"webhook_trigger_STORE_TRANSACTION": "After transaction creation",
"webhook_trigger_UPDATE_TRANSACTION": "After transaction update",
"webhook_trigger_DESTROY_TRANSACTION": "After transaction delete",
"webhook_response_TRANSACTIONS": "Transaction details",
"webhook_response_ACCOUNTS": "Account details",
"webhook_response_none_NONE": "No details",
"webhook_delivery_JSON": "JSON",
"actions": "Actions",
"meta_data": "Meta data",
"webhook_messages": "Webhook message",
"inactive": "Inactive",
"no_webhook_messages": "There are no webhook messages",
"inspect": "Inspect",
"create_new_webhook": "Create new webhook",
"webhooks": "Webhooks",
"webhook_trigger_form_help": "Indicate on what event the webhook will trigger",
"webhook_response_form_help": "Indicate what the webhook must submit to the URL.",
"webhook_delivery_form_help": "Which format the webhook must deliver data in.",
"webhook_active_form_help": "The webhook must be active or it won't be called.",
"edit_webhook_js": "Edit webhook \"{title}\"",
"webhook_was_triggered": "The webhook was triggered on the indicated transaction. Please wait for results to appear.",
"view_message": "View message",
"view_attempts": "View failed attempts",
"message_content_title": "Webhook message content",
"message_content_help": "This is the content of the message that was sent (or tried) using this webhook.",
"attempt_content_title": "Webhook attempts",
"attempt_content_help": "These are all the unsuccessful attempts of this webhook message to submit to the configured URL. After some time, Firefly III will stop trying.",
"no_attempts": "There are no unsuccessful attempts. That's a good thing!",
"webhook_attempt_at": "Attempt at {moment}",
"logs": "Logs",
"response": "Response",
"visit_webhook_url": "Visit webhook URL",
"reset_webhook_secret": "Reset webhook secret",
"header_exchange_rates": "Exchange rates",
"exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in <a href=\"https:\/\/docs.firefly-iii.org\/explanation\/financial-concepts\/exchange-rates\/\">the documentation<\/a>.",
"exchange_rates_from_to": "Between {from} and {to} (and the other way around)",
"exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.",
"header_exchange_rates_rates": "Exchange rates",
"header_exchange_rates_table": "Table with exchange rates",
"help_rate_form": "On this day, how many {to} will you get for one {from}?",
"add_new_rate": "Add a new exchange rate",
"save_new_rate": "Save new rate"
},
"form": {
"url": "URL",
"active": "Active",
"interest_date": "Interest date",
"administration_currency": "Primary currency",
"title": "Title",
"date": "Date",
"book_date": "Book date",
"process_date": "Processing date",
"due_date": "Due date",
"foreign_amount": "Foreign amount",
"payment_date": "Payment date",
"invoice_date": "Invoice date",
"internal_reference": "Internal reference",
"webhook_response": "Response",
"webhook_trigger": "Trigger",
"webhook_delivery": "Delivery",
"from_currency_to_currency": "{from} &rarr; {to}",
"to_currency_from_currency": "{to} &rarr; {from}",
"rate": "Rate"
},
"list": {
"title": "Title",
"active": "Is active?",
"primary_currency": "Primary currency",
"trigger": "Trigger",
"response": "Response",
"delivery": "Delivery",
"url": "URL",
"secret": "Secret"
},
"config": {
"html_language": "ar",
"date_time_fns": "MMMM do, yyyy @ HH:mm:ss"
}
}