diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index b71318b62..1524b25d0 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -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") diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 842ff50e8..91593f801 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -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 diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 03dcbc2db..753b77300 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -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: [] } ] ) diff --git a/app/models/income_statement/category_stats.rb b/app/models/income_statement/category_stats.rb index 31679a662..f4eb2815f 100644 --- a/app/models/income_statement/category_stats.rb +++ b/app/models/income_statement/category_stats.rb @@ -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 diff --git a/app/models/income_statement/family_stats.rb b/app/models/income_statement/family_stats.rb index c96f13b35..48b0d9507 100644 --- a/app/models/income_statement/family_stats.rb +++ b/app/models/income_statement/family_stats.rb @@ -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 diff --git a/app/models/income_statement/totals.rb b/app/models/income_statement/totals.rb index 2b5bd07b7..758ae6be3 100644 --- a/app/models/income_statement/totals.rb +++ b/app/models/income_statement/totals.rb @@ -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 diff --git a/app/models/transaction/search.rb b/app/models/transaction/search.rb index 3ece18e45..e46a66472 100644 --- a/app/models/transaction/search.rb +++ b/app/models/transaction/search.rb @@ -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( diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index c645d515d..9c52d6acd 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -78,12 +78,6 @@ <% end %> - <% if entry.exclude_from_cashflow? %> - - <%= icon "eye-off", size: "sm", color: "current" %> - - <% end %> - <%# Investment activity label badge %> <% if transaction.investment_activity_label.present? %> "> diff --git a/app/views/transactions/show.html.erb b/app/views/transactions/show.html.erb index 4af991f1a..3d8910dcb 100644 --- a/app/views/transactions/show.html.erb +++ b/app/views/transactions/show.html.erb @@ -203,28 +203,6 @@ <% end %> -
- <%= styled_form_with model: @entry, - url: transaction_path(@entry), - class: "p-3", - data: { controller: "auto-submit-form" } do |f| %> -
-
-

<%= t(".exclude_from_cashflow") %>

-

- <% if @entry.account.investment? || @entry.account.crypto? %> - <%= t(".exclude_from_cashflow_description_investment") %> - <% else %> - <%= t(".exclude_from_cashflow_description") %> - <% end %> -

-
- - <%= f.toggle :exclude_from_cashflow, { data: { auto_submit_form_target: "auto" } } %> -
- <% end %> -
- <% if @entry.account.investment? || @entry.account.crypto? %>
<%= styled_form_with model: @entry, diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 267adce43..3b295b2f4 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -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 diff --git a/db/migrate/20260110120000_add_investment_cashflow_support.rb b/db/migrate/20260110120000_add_investment_cashflow_support.rb index 26cffbc55..39765f4f1 100644 --- a/db/migrate/20260110120000_add_investment_cashflow_support.rb +++ b/db/migrate/20260110120000_add_investment_cashflow_support.rb @@ -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 diff --git a/test/models/income_statement_test.rb b/test/models/income_statement_test.rb index ba5fa3e81..e4af61d66 100644 --- a/test/models/income_statement_test.rb +++ b/test/models/income_statement_test.rb @@ -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