diff --git a/app/models/transaction/search.rb b/app/models/transaction/search.rb index 442324461..d60e39fb4 100644 --- a/app/models/transaction/search.rb +++ b/app/models/transaction/search.rb @@ -52,7 +52,7 @@ class Transaction::Search # because those transactions are retirement savings, not daily income/expenses. def totals @totals ||= begin - Rails.cache.fetch("transaction_search_totals/#{cache_key_base}") do + Rails.cache.fetch("transaction_search_totals/v2/#{cache_key_base}") do scope = transactions_scope # Exclude tax-advantaged accounts from totals calculation @@ -69,6 +69,14 @@ class Transaction::Search "COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind NOT IN (?) THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as income_total", Transaction::TRANSFER_KINDS ]), + ActiveRecord::Base.sanitize_sql_array([ + "COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind IN (?) THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as transfer_inflow_total", + Transaction::TRANSFER_KINDS + ]), + ActiveRecord::Base.sanitize_sql_array([ + "COALESCE(SUM(CASE WHEN entries.amount >= 0 AND transactions.kind IN (?) THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as transfer_outflow_total", + Transaction::TRANSFER_KINDS + ]), "COUNT(entries.id) as transactions_count" ) .joins( @@ -82,7 +90,9 @@ class Transaction::Search Totals.new( count: result.transactions_count.to_i, income_money: Money.new(result.income_total, family.currency), - expense_money: Money.new(result.expense_total, family.currency) + expense_money: Money.new(result.expense_total, family.currency), + transfer_inflow_money: Money.new(result.transfer_inflow_total, family.currency), + transfer_outflow_money: Money.new(result.transfer_outflow_total, family.currency) ) end end @@ -99,7 +109,7 @@ class Transaction::Search end private - Totals = Data.define(:count, :income_money, :expense_money) + Totals = Data.define(:count, :income_money, :expense_money, :transfer_inflow_money, :transfer_outflow_money) def apply_active_accounts_filter(query, active_accounts_only_filter) if active_accounts_only_filter diff --git a/app/views/transactions/_summary.html.erb b/app/views/transactions/_summary.html.erb index 117b4e933..b77853cc7 100644 --- a/app/views/transactions/_summary.html.erb +++ b/app/views/transactions/_summary.html.erb @@ -1,19 +1,39 @@ <%# locals: (totals:) %> +<%# Show Inflow/Outflow labels only when the result set contains exclusively transfers + (income and expense are both $0). For mixed filters (e.g. Expense+Transfer), + we keep Income/Expenses labels — transfer amounts aren't included in the summary + bar in that case, though the transaction list still shows both types. %> +<% show_transfers = totals.income_money.zero? && totals.expense_money.zero? && + (totals.transfer_inflow_money.amount > 0 || totals.transfer_outflow_money.amount > 0) %>
-

Total transactions

+

<%= t("transactions.summary.total_transactions") %>

<%= totals.count.round(0) %>

-

Income

-

- <%= totals.income_money.format %> -

+ <% if show_transfers %> +

<%= t("transactions.summary.inflow") %>

+

+ <%= (totals.income_money + totals.transfer_inflow_money).format %> +

+ <% else %> +

<%= t("transactions.summary.income") %>

+

+ <%= totals.income_money.format %> +

+ <% end %>
-

Expenses

-

- <%= totals.expense_money.format %> -

+ <% if show_transfers %> +

<%= t("transactions.summary.outflow") %>

+

+ <%= (totals.expense_money + totals.transfer_outflow_money).format %> +

+ <% else %> +

<%= t("transactions.summary.expenses") %>

+

+ <%= totals.expense_money.format %> +

+ <% end %>
diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 0c4b5b990..78a8e5305 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -108,6 +108,12 @@ en: review_recommended_short: Rev confirm_title: "Merge with posted transaction (%{posted_amount})" reject_title: Keep as separate transactions + summary: + total_transactions: Total transactions + income: Income + expenses: Expenses + inflow: Inflow + outflow: Outflow header: edit_categories: Edit categories edit_imports: Edit imports diff --git a/test/controllers/transactions_controller_test.rb b/test/controllers/transactions_controller_test.rb index d7604f47b..8bb91aa85 100644 --- a/test/controllers/transactions_controller_test.rb +++ b/test/controllers/transactions_controller_test.rb @@ -159,7 +159,9 @@ end totals = OpenStruct.new( count: 1, expense_money: Money.new(10000, "USD"), - income_money: Money.new(0, "USD") + income_money: Money.new(0, "USD"), + transfer_inflow_money: Money.new(0, "USD"), + transfer_outflow_money: Money.new(0, "USD") ) Transaction::Search.expects(:new).with(family, filters: {}, accessible_account_ids: [ account.id ]).returns(search) @@ -181,7 +183,9 @@ end totals = OpenStruct.new( count: 1, expense_money: Money.new(10000, "USD"), - income_money: Money.new(0, "USD") + income_money: Money.new(0, "USD"), + transfer_inflow_money: Money.new(0, "USD"), + transfer_outflow_money: Money.new(0, "USD") ) Transaction::Search.expects(:new).with(family, filters: { "categories" => [ "Food" ], "types" => [ "expense" ] }, accessible_account_ids: [ account.id ]).returns(search) @@ -191,6 +195,31 @@ end assert_response :success end + test "shows inflow/outflow labels when filtering by transfers only" do + family = families(:empty) + sign_in users(:empty) + account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new + + create_transaction(account: account, amount: 100) + + search = Transaction::Search.new(family, filters: { "types" => [ "transfer" ] }) + totals = OpenStruct.new( + count: 2, + expense_money: Money.new(0, "USD"), + income_money: Money.new(0, "USD"), + transfer_inflow_money: Money.new(5000, "USD"), + transfer_outflow_money: Money.new(3000, "USD") + ) + + Transaction::Search.expects(:new).with(family, filters: { "types" => [ "transfer" ] }, accessible_account_ids: [ account.id ]).returns(search) + search.expects(:totals).once.returns(totals) + + get transactions_url(q: { types: [ "transfer" ] }) + assert_response :success + assert_select "#total-income", text: totals.transfer_inflow_money.format + assert_select "#total-expense", text: totals.transfer_outflow_money.format + end + test "mark_as_recurring creates a manual recurring transaction" do family = families(:empty) sign_in users(:empty)