diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index acceab79dc4..664c308040d 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -3,13 +3,13 @@ class TransactionsController < ApplicationController def index @q = search_params - result = Current.family.entries.account_transactions.search(@q).reverse_chronological - @pagy, @transaction_entries = pagy(result, limit: params[:per_page] || "50") + search_query = Current.family.transactions.search(@q).includes(:entryable).reverse_chronological + @pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50") @totals = { - count: result.select { |t| t.currency == Current.family.currency }.count, - income: result.income_total(Current.family.currency).abs, - expense: result.expense_total(Current.family.currency) + count: search_query.select { |t| t.currency == Current.family.currency }.count, + income: search_query.income_total(Current.family.currency).abs, + expense: search_query.expense_total(Current.family.currency) } end diff --git a/app/models/account/entry.rb b/app/models/account/entry.rb index 4d7334fbb75..8d6a8f402f1 100644 --- a/app/models/account/entry.rb +++ b/app/models/account/entry.rb @@ -60,6 +60,10 @@ def display_name end class << self + def search(params) + Account::EntrySearch.new(params).build_query(all) + end + # arbitrary cutoff date to avoid expensive sync operations def min_supported_date 30.years.ago.to_date @@ -141,49 +145,7 @@ def expense_total(currency = "USD") Money.new(total, currency) end - def search(params) - query = all - query = query.where("account_entries.name ILIKE ?", "%#{sanitize_sql_like(params[:search])}%") if params[:search].present? - query = query.where("account_entries.date >= ?", params[:start_date]) if params[:start_date].present? - query = query.where("account_entries.date <= ?", params[:end_date]) if params[:end_date].present? - - if params[:types].present? - query = query.where(marked_as_transfer: false) unless params[:types].include?("transfer") - - if params[:types].include?("income") && !params[:types].include?("expense") - query = query.where("account_entries.amount < 0") - elsif params[:types].include?("expense") && !params[:types].include?("income") - query = query.where("account_entries.amount >= 0") - end - end - - if params[:amount].present? && params[:amount_operator].present? - case params[:amount_operator] - when "equal" - query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", params[:amount].to_f.abs) - when "less" - query = query.where("ABS(account_entries.amount) < ?", params[:amount].to_f.abs) - when "greater" - query = query.where("ABS(account_entries.amount) > ?", params[:amount].to_f.abs) - end - end - - if params[:accounts].present? || params[:account_ids].present? - query = query.joins(:account) - end - - query = query.where(accounts: { name: params[:accounts] }) if params[:accounts].present? - query = query.where(accounts: { id: params[:account_ids] }) if params[:account_ids].present? - - # Search attributes on each entryable to further refine results - entryable_ids = entryable_search(params) - query = query.where(entryable_id: entryable_ids) unless entryable_ids.nil? - - query - end - private - def entryable_search(params) entryable_ids = [] entryable_search_performed = false diff --git a/app/models/account/entry_search.rb b/app/models/account/entry_search.rb new file mode 100644 index 00000000000..c561765b557 --- /dev/null +++ b/app/models/account/entry_search.rb @@ -0,0 +1,59 @@ +class Account::EntrySearch + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :search, :string + attribute :amount, :string + attribute :amount_operator, :string + attribute :types, :string + attribute :accounts, :string + attribute :account_ids, :string + attribute :start_date, :string + attribute :end_date, :string + + class << self + def from_entryable_search(entryable_search) + new(entryable_search.attributes.slice(*attribute_names)) + end + end + + def build_query(scope) + query = scope + + query = query.where("account_entries.name ILIKE :search OR account_entries.enriched_name ILIKE :search", + search: "%#{ActiveRecord::Base.sanitize_sql_like(search)}%" + ) if search.present? + query = query.where("account_entries.date >= ?", start_date) if start_date.present? + query = query.where("account_entries.date <= ?", end_date) if end_date.present? + + if types.present? + query = query.where(marked_as_transfer: false) unless types.include?("transfer") + + if types.include?("income") && !types.include?("expense") + query = query.where("account_entries.amount < 0") + elsif types.include?("expense") && !types.include?("income") + query = query.where("account_entries.amount >= 0") + end + end + + if amount.present? && amount_operator.present? + case amount_operator + when "equal" + query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", amount.to_f.abs) + when "less" + query = query.where("ABS(account_entries.amount) < ?", amount.to_f.abs) + when "greater" + query = query.where("ABS(account_entries.amount) > ?", amount.to_f.abs) + end + end + + if accounts.present? || account_ids.present? + query = query.joins(:account) + end + + query = query.where(accounts: { name: accounts }) if accounts.present? + query = query.where(accounts: { id: account_ids }) if account_ids.present? + + query + end +end diff --git a/app/models/account/trade.rb b/app/models/account/trade.rb index 70b0c8f32c2..7d4976ba162 100644 --- a/app/models/account/trade.rb +++ b/app/models/account/trade.rb @@ -8,26 +8,8 @@ class Account::Trade < ApplicationRecord validates :qty, presence: true validates :price, :currency, presence: true - class << self - def search(_params) - all - end - - def requires_search?(_params) - false - end - end - - def sell? - qty < 0 - end - - def buy? - qty > 0 - end - def unrealized_gain_loss - return nil if sell? + return nil if qty.negative? current_price = security.current_price return nil if current_price.nil? diff --git a/app/models/account/transaction.rb b/app/models/account/transaction.rb index 6b8f4995ee5..afe5a568227 100644 --- a/app/models/account/transaction.rb +++ b/app/models/account/transaction.rb @@ -12,52 +12,7 @@ class Account::Transaction < ApplicationRecord class << self def search(params) - query = all - if params[:categories].present? - if params[:categories].exclude?("Uncategorized") - query = query - .joins(:category) - .where(categories: { name: params[:categories] }) - else - query = query - .left_joins(:category) - .where(categories: { name: params[:categories] }) - .or(query.where(category_id: nil)) - end - end - - query = query.joins(:merchant).where(merchants: { name: params[:merchants] }) if params[:merchants].present? - - if params[:tags].present? - query = query.joins(:tags) - .where(tags: { name: params[:tags] }) - .distinct - end - - query + Account::TransactionSearch.new(params).build_query(all) end - - def requires_search?(params) - searchable_keys.any? { |key| params.key?(key) } - end - - private - - def searchable_keys - %i[categories merchants tags] - end end - - def eod_balance - entry.amount_money - end - - private - def account - entry.account - end - - def daily_transactions - account.entries.account_transactions - end end diff --git a/app/models/account/transaction_search.rb b/app/models/account/transaction_search.rb new file mode 100644 index 00000000000..f61fae6906b --- /dev/null +++ b/app/models/account/transaction_search.rb @@ -0,0 +1,42 @@ +class Account::TransactionSearch + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :search, :string + attribute :amount, :string + attribute :amount_operator, :string + attribute :types, array: true + attribute :accounts, array: true + attribute :account_ids, array: true + attribute :start_date, :string + attribute :end_date, :string + attribute :categories, array: true + attribute :merchants, array: true + attribute :tags, array: true + + # Returns array of Account::Entry objects to stay consistent with partials, which only deal with Account::Entry + def build_query(scope) + query = scope + + if categories.present? + if categories.exclude?("Uncategorized") + query = query + .joins(:category) + .where(categories: { name: categories }) + else + query = query + .left_joins(:category) + .where(categories: { name: categories }) + .or(query.where(category_id: nil)) + end + end + + query = query.joins(:merchant).where(merchants: { name: merchants }) if merchants.present? + + query = query.joins(:tags).where(tags: { name: tags }) if tags.present? + + entries_scope = Account::Entry.account_transactions.where(entryable_id: query.select(:id)) + + Account::EntrySearch.from_entryable_search(self).build_query(entries_scope) + end +end diff --git a/app/models/account/valuation.rb b/app/models/account/valuation.rb index 93ebf5ffb1f..219ecd9005f 100644 --- a/app/models/account/valuation.rb +++ b/app/models/account/valuation.rb @@ -1,13 +1,3 @@ class Account::Valuation < ApplicationRecord include Account::Entryable - - class << self - def search(_params) - all - end - - def requires_search?(_params) - false - end - end end diff --git a/app/views/account/trades/_header.html.erb b/app/views/account/trades/_header.html.erb index 7ccadfa420c..b89028afe42 100644 --- a/app/views/account/trades/_header.html.erb +++ b/app/views/account/trades/_header.html.erb @@ -34,7 +34,7 @@
<%= trade.security.ticker %>
- <% if trade.buy? %> + <% if trade.qty.positive? %>
<%= t(".purchase_qty_label") %>
<%= trade.qty.abs %>
@@ -53,7 +53,7 @@
<% end %> - <% if trade.buy? && trade.unrealized_gain_loss.present? %> + <% if trade.qty.positive? && trade.unrealized_gain_loss.present? %>
<%= t(".total_return_label") %>
diff --git a/db/migrate/20241219174803_add_parent_category.rb b/db/migrate/20241219174803_add_parent_category.rb new file mode 100644 index 00000000000..615724527a2 --- /dev/null +++ b/db/migrate/20241219174803_add_parent_category.rb @@ -0,0 +1,5 @@ +class AddParentCategory < ActiveRecord::Migration[7.2] + def change + add_column :categories, :parent_category_id, :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index 7beb5097e92..db5d258bfb4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_12_18_132503) do +ActiveRecord::Schema[7.2].define(version: 2024_12_19_174803) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -110,7 +110,7 @@ t.decimal "balance", precision: 19, scale: 4 t.string "currency" t.boolean "is_active", default: true, null: false - t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true + t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true t.uuid "import_id" t.uuid "plaid_account_id" t.boolean "scheduled_for_deletion", default: false @@ -175,6 +175,7 @@ t.uuid "family_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "parent_category_id" t.index ["family_id"], name: "index_categories_on_family_id" end diff --git a/test/models/account/entry_test.rb b/test/models/account/entry_test.rb index 5a9d9ec9587..6d22179ca6f 100644 --- a/test/models/account/entry_test.rb +++ b/test/models/account/entry_test.rb @@ -63,10 +63,6 @@ class Account::EntryTest < ActiveSupport::TestCase assert_equal 2, family.entries.search(params).size - params = params.merge(categories: [ category.name ], merchants: [ merchant.name ]) # transaction specific search param - - assert_equal 1, family.entries.search(params).size - params = { search: "%" } assert_equal 0, family.entries.search(params).size end