mirror of
https://github.com/we-promise/sure
synced 2026-04-25 17:15:07 +02:00
Show inflow/outflow totals for transfer filter (#1134)
* Show inflow/outflow totals when filtering by transfers When filtering transactions by "Transfer" type, the summary bar previously showed $0 for both Income and Expenses because transfers were excluded from those sums. Now computes transfer inflow/outflow in the same SQL pass and switches labels to "Inflow"/"Outflow" when transfer amounts are non-zero. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add mixed filter comment and transfer-only test coverage Document the intentional mixed filter behavior where transfer amounts are excluded from the summary bar when non-transfer types are present. Add test exercising Inflow/Outflow label switching for transfer-only results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
f1991eaefe
commit
f42b593b9e
@@ -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
|
||||
|
||||
@@ -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) %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 bg-container rounded-xl shadow-border-xs md:divide-x divide-y md:divide-y-0 divide-alpha-black-100 theme-dark:divide-alpha-white-200">
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-secondary">Total transactions</p>
|
||||
<p class="text-sm text-secondary"><%= t("transactions.summary.total_transactions") %></p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-transactions"><%= totals.count.round(0) %></p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-secondary">Income</p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-income">
|
||||
<%= totals.income_money.format %>
|
||||
</p>
|
||||
<% if show_transfers %>
|
||||
<p class="text-sm text-secondary"><%= t("transactions.summary.inflow") %></p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-income">
|
||||
<%= (totals.income_money + totals.transfer_inflow_money).format %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-secondary"><%= t("transactions.summary.income") %></p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-income">
|
||||
<%= totals.income_money.format %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-secondary">Expenses</p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-expense">
|
||||
<%= totals.expense_money.format %>
|
||||
</p>
|
||||
<% if show_transfers %>
|
||||
<p class="text-sm text-secondary"><%= t("transactions.summary.outflow") %></p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-expense">
|
||||
<%= (totals.expense_money + totals.transfer_outflow_money).format %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-secondary"><%= t("transactions.summary.expenses") %></p>
|
||||
<p class="text-primary font-medium text-xl privacy-sensitive" id="total-expense">
|
||||
<%= totals.expense_money.format %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user