mirror of
https://github.com/we-promise/sure
synced 2026-04-25 17:15:07 +02:00
Remove exclude_from_cashflow flag and consolidate logic into excluded toggle
- Removed `exclude_from_cashflow` attribute across models, controllers, and views. - Updated queries to rely solely on the `excluded` flag for filtering transactions and entries. - Simplified migration by consolidating `exclude_from_cashflow` functionality into the existing `excluded` toggle. - Refactored related tests to remove outdated logic and ensured compatibility with the updated implementation.
This commit is contained in:
@@ -36,7 +36,7 @@ class AccountsController < ApplicationController
|
||||
@chart_view = params[:chart_view] || "balance"
|
||||
@tab = params[:tab]
|
||||
@q = params.fetch(:q, {}).permit(:search, status: [])
|
||||
entries = @account.entries.where(excluded: false).search(@q).reverse_chronological
|
||||
entries = @account.entries.search(@q).reverse_chronological
|
||||
|
||||
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10")
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class ReportsController < ApplicationController
|
||||
.joins(:entry)
|
||||
.joins(entry: :account)
|
||||
.where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, exclude_from_cashflow: false, date: @period.date_range })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
|
||||
.where.not(kind: [ "funds_movement", "one_time", "cc_payment" ])
|
||||
.includes(entry: :account, category: [])
|
||||
|
||||
@@ -350,7 +350,7 @@ class ReportsController < ApplicationController
|
||||
.joins(:entry)
|
||||
.joins(entry: :account)
|
||||
.where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
|
||||
.where(entries: { entryable_type: "Trade", excluded: false, exclude_from_cashflow: false, date: @period.date_range })
|
||||
.where(entries: { entryable_type: "Trade", excluded: false, date: @period.date_range })
|
||||
.includes(entry: :account, category: [])
|
||||
|
||||
# Get sort parameters
|
||||
@@ -519,7 +519,7 @@ class ReportsController < ApplicationController
|
||||
.joins(:entry)
|
||||
.joins(entry: :account)
|
||||
.where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, exclude_from_cashflow: false, date: @period.date_range })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
|
||||
.where.not(kind: [ "funds_movement", "one_time", "cc_payment" ])
|
||||
.includes(entry: :account, category: [])
|
||||
|
||||
@@ -556,7 +556,7 @@ class ReportsController < ApplicationController
|
||||
.joins(:entry)
|
||||
.joins(entry: :account)
|
||||
.where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, exclude_from_cashflow: false, date: @period.date_range })
|
||||
.where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
|
||||
.where.not(kind: [ "funds_movement", "one_time", "cc_payment" ])
|
||||
.includes(entry: :account, category: [])
|
||||
|
||||
@@ -567,7 +567,7 @@ class ReportsController < ApplicationController
|
||||
.joins(:entry)
|
||||
.joins(entry: :account)
|
||||
.where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
|
||||
.where(entries: { entryable_type: "Trade", excluded: false, exclude_from_cashflow: false, date: @period.date_range })
|
||||
.where(entries: { entryable_type: "Trade", excluded: false, date: @period.date_range })
|
||||
.includes(entry: :account, category: [])
|
||||
|
||||
# Group by category, type, and month
|
||||
|
||||
@@ -217,7 +217,7 @@ class TransactionsController < ApplicationController
|
||||
|
||||
def entry_params
|
||||
entry_params = params.require(:entry).permit(
|
||||
:name, :date, :amount, :currency, :excluded, :exclude_from_cashflow, :notes, :nature, :entryable_type,
|
||||
:name, :date, :amount, :currency, :excluded, :notes, :nature, :entryable_type,
|
||||
entryable_attributes: [ :id, :category_id, :merchant_id, :kind, :investment_activity_label, { tag_ids: [] } ]
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ class IncomeStatement::CategoryStats
|
||||
WHERE a.family_id = :family_id
|
||||
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution')
|
||||
AND ae.excluded = false
|
||||
AND ae.exclude_from_cashflow = false
|
||||
AND (t.extra -> 'simplefin' ->> 'pending')::boolean IS DISTINCT FROM true
|
||||
AND (t.extra -> 'plaid' ->> 'pending')::boolean IS DISTINCT FROM true
|
||||
GROUP BY c.id, period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
||||
|
||||
@@ -46,7 +46,6 @@ class IncomeStatement::FamilyStats
|
||||
WHERE a.family_id = :family_id
|
||||
AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution')
|
||||
AND ae.excluded = false
|
||||
AND ae.exclude_from_cashflow = false
|
||||
AND (t.extra -> 'simplefin' ->> 'pending')::boolean IS DISTINCT FROM true
|
||||
AND (t.extra -> 'plaid' ->> 'pending')::boolean IS DISTINCT FROM true
|
||||
GROUP BY period, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
||||
|
||||
@@ -71,8 +71,7 @@ class IncomeStatement::Totals
|
||||
)
|
||||
WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution')
|
||||
AND ae.excluded = false
|
||||
AND ae.exclude_from_cashflow = false
|
||||
AND a.family_id = :family_id
|
||||
AND a.family_id = :family_id
|
||||
AND a.status IN ('draft', 'active')
|
||||
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END;
|
||||
SQL
|
||||
@@ -98,8 +97,7 @@ class IncomeStatement::Totals
|
||||
)
|
||||
WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution')
|
||||
AND ae.excluded = false
|
||||
AND ae.exclude_from_cashflow = false
|
||||
AND a.family_id = :family_id
|
||||
AND a.family_id = :family_id
|
||||
AND a.status IN ('draft', 'active')
|
||||
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
|
||||
SQL
|
||||
@@ -128,8 +126,7 @@ class IncomeStatement::Totals
|
||||
WHERE a.family_id = :family_id
|
||||
AND a.status IN ('draft', 'active')
|
||||
AND ae.excluded = false
|
||||
AND ae.exclude_from_cashflow = false
|
||||
AND ae.date BETWEEN :start_date AND :end_date
|
||||
AND ae.date BETWEEN :start_date AND :end_date
|
||||
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END, CASE WHEN t.category_id IS NULL THEN true ELSE false END
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -49,8 +49,8 @@ class Transaction::Search
|
||||
Rails.cache.fetch("transaction_search_totals/#{cache_key_base}") do
|
||||
result = transactions_scope
|
||||
.select(
|
||||
"COALESCE(SUM(CASE WHEN entries.amount >= 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') AND entries.exclude_from_cashflow = false THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as expense_total",
|
||||
"COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') AND entries.exclude_from_cashflow = false THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as income_total",
|
||||
"COALESCE(SUM(CASE WHEN entries.amount >= 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as expense_total",
|
||||
"COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as income_total",
|
||||
"COUNT(entries.id) as transactions_count"
|
||||
)
|
||||
.joins(
|
||||
|
||||
@@ -78,12 +78,6 @@
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if entry.exclude_from_cashflow? %>
|
||||
<span class="text-secondary" title="<%= t('transactions.transaction.excluded_from_cashflow_tooltip') %>">
|
||||
<%= icon "eye-off", size: "sm", color: "current" %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%# Investment activity label badge %>
|
||||
<% if transaction.investment_activity_label.present? %>
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-alpha-black-50 text-secondary" title="<%= t("transactions.transaction.activity_type_tooltip") %>">
|
||||
|
||||
@@ -203,28 +203,6 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: @entry,
|
||||
url: transaction_path(@entry),
|
||||
class: "p-3",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<div class="flex cursor-pointer items-center gap-4 justify-between">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-primary"><%= t(".exclude_from_cashflow") %></h4>
|
||||
<p class="text-secondary">
|
||||
<% if @entry.account.investment? || @entry.account.crypto? %>
|
||||
<%= t(".exclude_from_cashflow_description_investment") %>
|
||||
<% else %>
|
||||
<%= t(".exclude_from_cashflow_description") %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= f.toggle :exclude_from_cashflow, { data: { auto_submit_form_target: "auto" } } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @entry.account.investment? || @entry.account.crypto? %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: @entry,
|
||||
|
||||
@@ -33,9 +33,6 @@ en:
|
||||
details: Details
|
||||
exclude: Exclude
|
||||
exclude_description: Excluded transactions will be removed from budgeting calculations and reports.
|
||||
exclude_from_cashflow: Exclude from Cashflow
|
||||
exclude_from_cashflow_description: Hide from income/expense reports and Sankey chart. Useful for transactions you don't want in cashflow analysis.
|
||||
exclude_from_cashflow_description_investment: Hide from income/expense reports and Sankey chart. Use for internal investment activity like fund swaps, reinvestments, or money market sweeps.
|
||||
activity_type: Activity Type
|
||||
activity_type_description: Type of investment activity (Buy, Sell, Dividend, etc.). Auto-detected or set manually.
|
||||
one_time_title: One-time %{type}
|
||||
@@ -76,7 +73,6 @@ en:
|
||||
transaction:
|
||||
pending: Pending
|
||||
pending_tooltip: Pending transaction — may change when posted
|
||||
excluded_from_cashflow_tooltip: Excluded from cashflow reports
|
||||
activity_type_tooltip: Investment activity type
|
||||
possible_duplicate: Duplicate?
|
||||
potential_duplicate_tooltip: This may be a duplicate of another transaction
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
class AddInvestmentCashflowSupport < ActiveRecord::Migration[7.2]
|
||||
# No-op: exclude_from_cashflow was consolidated into the existing 'excluded' toggle
|
||||
def change
|
||||
# Flag for excluding from cashflow (user-controllable)
|
||||
# Used for internal investment activity like fund swaps
|
||||
add_column :entries, :exclude_from_cashflow, :boolean, default: false, null: false
|
||||
add_index :entries, :exclude_from_cashflow
|
||||
|
||||
# Holdings snapshot for comparison (provider-agnostic)
|
||||
# Used to detect internal investment activity by comparing holdings between syncs
|
||||
add_column :accounts, :holdings_snapshot_data, :jsonb
|
||||
add_column :accounts, :holdings_snapshot_at, :datetime
|
||||
end
|
||||
end
|
||||
|
||||
@@ -286,35 +286,6 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
assert_equal Money.new(1050, @family.currency), totals.expense_money # 900 + 150
|
||||
end
|
||||
|
||||
# NEW TESTS: exclude_from_cashflow Feature
|
||||
test "excludes transactions with exclude_from_cashflow flag from totals" do
|
||||
# Create an expense transaction and mark it as excluded from cashflow
|
||||
excluded_entry = create_transaction(account: @checking_account, amount: 250, category: @groceries_category)
|
||||
excluded_entry.update!(exclude_from_cashflow: true)
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# Should NOT include the excluded transaction
|
||||
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||
assert_equal Money.new(1000, @family.currency), totals.income_money
|
||||
assert_equal Money.new(900, @family.currency), totals.expense_money
|
||||
end
|
||||
|
||||
test "excludes income transactions with exclude_from_cashflow flag" do
|
||||
# Create income and mark as excluded from cashflow
|
||||
excluded_income = create_transaction(account: @checking_account, amount: -500, category: @income_category)
|
||||
excluded_income.update!(exclude_from_cashflow: true)
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# Should NOT include the excluded income
|
||||
assert_equal 4, totals.transactions_count
|
||||
assert_equal Money.new(1000, @family.currency), totals.income_money # Original income only
|
||||
assert_equal Money.new(900, @family.currency), totals.expense_money
|
||||
end
|
||||
|
||||
test "excludes investment_contribution transactions from income statement" do
|
||||
# Create a transfer to investment account (marked as investment_contribution)
|
||||
investment_contribution = create_transaction(
|
||||
@@ -332,23 +303,4 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
assert_equal Money.new(1000, @family.currency), totals.income_money
|
||||
assert_equal Money.new(900, @family.currency), totals.expense_money
|
||||
end
|
||||
|
||||
test "exclude_from_cashflow works with median calculations" do
|
||||
# Clear existing transactions
|
||||
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
||||
|
||||
# Create expenses: 100, 200, 300
|
||||
create_transaction(account: @checking_account, amount: 100, category: @groceries_category)
|
||||
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
||||
excluded_entry = create_transaction(account: @checking_account, amount: 300, category: @groceries_category)
|
||||
|
||||
# Exclude the 300 transaction from cashflow
|
||||
excluded_entry.update!(exclude_from_cashflow: true)
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
|
||||
# Median should only consider non-excluded transactions (100, 200)
|
||||
# Monthly total = 300, so median = 300.0
|
||||
assert_equal 300.0, income_statement.median_expense(interval: "month")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user