diff --git a/app/models/budget.rb b/app/models/budget.rb index 0dfb16e8..66a4acb7 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -100,11 +100,11 @@ class Budget < ApplicationRecord end def income_category_totals - income_totals.category_totals.reject { |ct| ct.category.subcategory? }.sort_by(&:weight).reverse + income_totals.category_totals.reject { |ct| ct.category.subcategory? || ct.total.zero? }.sort_by(&:weight).reverse end def expense_category_totals - expense_totals.category_totals.reject { |ct| ct.category.subcategory? }.sort_by(&:weight).reverse + expense_totals.category_totals.reject { |ct| ct.category.subcategory? || ct.total.zero? }.sort_by(&:weight).reverse end def current? diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index d60be41e..369f18c8 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -61,30 +61,83 @@ class Demo::Generator puts "Demo data loaded successfully!" end - def generate_multi_currency_data! + def generate_basic_budget_data!(family_names) puts "Clearing existing data..." destroy_everything! puts "Data cleared" - create_family_and_user!("Demo Family 1", "user@maybe.local", currency: "EUR") - - family = Family.find_by(name: "Demo Family 1") + family_names.each_with_index do |family_name, index| + create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local") + end puts "Users reset" - usd_checking = family.accounts.create!(name: "USD Checking", currency: "USD", balance: 10000, accountable: Depository.new) - eur_checking = family.accounts.create!(name: "EUR Checking", currency: "EUR", balance: 4900, accountable: Depository.new) + family_names.each do |family_name| + family = Family.find_by(name: family_name) - puts "Accounts created" + ActiveRecord::Base.transaction do + # Create parent categories + food = family.categories.create!(name: "Food & Drink", color: COLORS.sample, classification: "expense") + transport = family.categories.create!(name: "Transportation", color: COLORS.sample, classification: "expense") - create_transaction!(account: usd_checking, amount: -11000, currency: "USD", name: "USD income Transaction") - create_transaction!(account: usd_checking, amount: 1000, currency: "USD", name: "USD expense Transaction") - create_transaction!(account: eur_checking, amount: -5000, currency: "EUR", name: "EUR income Transaction") - create_transaction!(account: eur_checking, amount: 100, currency: "EUR", name: "EUR expense Transaction") + # Create subcategory + restaurants = family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, classification: "expense") - puts "Transactions created" + # Create checking account + checking = family.accounts.create!( + accountable: Depository.new, + name: "Demo Checking", + balance: 3000, + currency: "USD" + ) + + # Create one transaction for each category + create_transaction!(account: checking, amount: 100, name: "Grocery Store", category: food, date: 2.days.ago) + create_transaction!(account: checking, amount: 50, name: "Restaurant Meal", category: restaurants, date: 1.day.ago) + create_transaction!(account: checking, amount: 20, name: "Gas Station", category: transport, date: Date.current) + end + + puts "Basic budget data created for #{family_name}" + end + + puts "Demo data loaded successfully!" + end + + def generate_multi_currency_data!(family_names) + puts "Clearing existing data..." + + destroy_everything! + + puts "Data cleared" + + family_names.each_with_index do |family_name, index| + create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", currency: "EUR") + end + + puts "Users reset" + + family_names.each do |family_name| + puts "Generating demo data for #{family_name}" + family = Family.find_by(name: family_name) + + usd_checking = family.accounts.create!(name: "USD Checking", currency: "USD", balance: 10000, accountable: Depository.new) + eur_checking = family.accounts.create!(name: "EUR Checking", currency: "EUR", balance: 4900, accountable: Depository.new) + eur_credit_card = family.accounts.create!(name: "EUR Credit Card", currency: "EUR", balance: 2300, accountable: CreditCard.new) + + create_transaction!(account: eur_credit_card, amount: 1000, currency: "EUR", name: "EUR cc expense 1") + create_transaction!(account: eur_credit_card, amount: 1000, currency: "EUR", name: "EUR cc expense 2") + create_transaction!(account: eur_credit_card, amount: 300, currency: "EUR", name: "EUR cc expense 3") + + create_transaction!(account: usd_checking, amount: -11000, currency: "USD", name: "USD income Transaction") + create_transaction!(account: usd_checking, amount: 1000, currency: "USD", name: "USD expense Transaction") + create_transaction!(account: usd_checking, amount: 1000, currency: "USD", name: "USD expense Transaction") + create_transaction!(account: eur_checking, amount: -5000, currency: "EUR", name: "EUR income Transaction") + create_transaction!(account: eur_checking, amount: 100, currency: "EUR", name: "EUR expense Transaction") + + puts "Transactions created for #{family_name}" + end puts "Demo data loaded successfully!" end diff --git a/app/models/income_statement.rb b/app/models/income_statement.rb index 52ad030c..cacc3f33 100644 --- a/app/models/income_statement.rb +++ b/app/models/income_statement.rb @@ -66,21 +66,25 @@ class IncomeStatement totals = totals_query(transactions_scope: family.transactions.active.in_period(period)).select { |t| t.classification == classification } classification_total = totals.sum(&:total) - category_totals = totals.map do |ct| - # If parent category is nil, it's a top-level category. This means we need to - # sum itself + SUM(children) to get the overall category total - children_totals = if ct.parent_category_id.nil? && ct.category_id.present? - totals.select { |t| t.parent_category_id == ct.category_id }.sum(&:total) - else + uncategorized_category = family.categories.uncategorized + + category_totals = [ *categories, uncategorized_category ].map do |category| + subcategory = categories.find { |c| c.id == category.parent_id } + + parent_category_total = totals.select { |t| t.category_id == category.id }&.sum(&:total) || 0 + + children_totals = if category == uncategorized_category 0 + else + totals.select { |t| t.parent_category_id == category.id }&.sum(&:total) || 0 end - category_total = ct.total + children_totals + category_total = parent_category_total + children_totals weight = (category_total.zero? ? 0 : category_total.to_f / classification_total) * 100 CategoryTotal.new( - category: categories.find { |c| c.id == ct.category_id } || family.categories.uncategorized, + category: category, total: category_total, currency: family.currency, weight: weight, diff --git a/lib/tasks/demo_data.rake b/lib/tasks/demo_data.rake index 46fa4c37..2f5a1b6d 100644 --- a/lib/tasks/demo_data.rake +++ b/lib/tasks/demo_data.rake @@ -12,6 +12,12 @@ namespace :demo_data do end task multi_currency: :environment do - Demo::Generator.new.generate_multi_currency_data! + families = [ "Demo Family 1", "Demo Family 2" ] + Demo::Generator.new.generate_multi_currency_data!(families) + end + + task basic_budget: :environment do + families = [ "Demo Family 1" ] + Demo::Generator.new.generate_basic_budget_data!(families) end end diff --git a/test/models/income_statement_test.rb b/test/models/income_statement_test.rb index a291dafc..be97c941 100644 --- a/test/models/income_statement_test.rb +++ b/test/models/income_statement_test.rb @@ -8,15 +8,15 @@ class IncomeStatementTest < ActiveSupport::TestCase @income_category = @family.categories.create! name: "Income", classification: "income" @food_category = @family.categories.create! name: "Food", classification: "expense" - @shopping_category = @family.categories.create! name: "Shopping", classification: "expense", parent: @food_category + @groceries_category = @family.categories.create! name: "Groceries", classification: "expense", parent: @food_category @checking_account = @family.accounts.create! name: "Checking", currency: @family.currency, balance: 5000, accountable: Depository.new @credit_card_account = @family.accounts.create! name: "Credit Card", currency: @family.currency, balance: 1000, accountable: CreditCard.new - create_transaction(account: @checking_account, amount: -1000, category: @food_category) - create_transaction(account: @checking_account, amount: 200, category: @shopping_category) - create_transaction(account: @credit_card_account, amount: 300, category: @food_category) - create_transaction(account: @credit_card_account, amount: 400, category: @shopping_category) + create_transaction(account: @checking_account, amount: -1000, category: @income_category) + create_transaction(account: @checking_account, amount: 200, category: @groceries_category) + create_transaction(account: @credit_card_account, amount: 300, category: @groceries_category) + create_transaction(account: @credit_card_account, amount: 400, category: @groceries_category) end test "calculates totals for transactions" do @@ -28,12 +28,23 @@ class IncomeStatementTest < ActiveSupport::TestCase test "calculates expenses for a period" do income_statement = IncomeStatement.new(@family) - assert_equal 200 + 300 + 400, income_statement.expense_totals(period: Period.last_30_days).total + expense_totals = income_statement.expense_totals(period: Period.last_30_days) + + expected_total_expense = 200 + 300 + 400 + + assert_equal expected_total_expense, expense_totals.total + assert_equal expected_total_expense, expense_totals.category_totals.find { |ct| ct.category.id == @groceries_category.id }.total + assert_equal expected_total_expense, expense_totals.category_totals.find { |ct| ct.category.id == @food_category.id }.total end test "calculates income for a period" do income_statement = IncomeStatement.new(@family) - assert_equal 1000, income_statement.income_totals(period: Period.last_30_days).total + income_totals = income_statement.income_totals(period: Period.last_30_days) + + expected_total_income = 1000 + + assert_equal expected_total_income, income_totals.total + assert_equal expected_total_income, income_totals.category_totals.find { |ct| ct.category.id == @income_category.id }.total end test "calculates median expense" do