mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Fix parent category sums in budget (#1894)
This commit is contained in:
parent
0dea36ec7d
commit
f5ff5332d5
5 changed files with 104 additions and 30 deletions
|
@ -100,11 +100,11 @@ class Budget < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def income_category_totals
|
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
|
end
|
||||||
|
|
||||||
def expense_category_totals
|
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
|
end
|
||||||
|
|
||||||
def current?
|
def current?
|
||||||
|
|
|
@ -61,30 +61,83 @@ class Demo::Generator
|
||||||
puts "Demo data loaded successfully!"
|
puts "Demo data loaded successfully!"
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_multi_currency_data!
|
def generate_basic_budget_data!(family_names)
|
||||||
puts "Clearing existing data..."
|
puts "Clearing existing data..."
|
||||||
|
|
||||||
destroy_everything!
|
destroy_everything!
|
||||||
|
|
||||||
puts "Data cleared"
|
puts "Data cleared"
|
||||||
|
|
||||||
create_family_and_user!("Demo Family 1", "user@maybe.local", currency: "EUR")
|
family_names.each_with_index do |family_name, index|
|
||||||
|
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
|
||||||
family = Family.find_by(name: "Demo Family 1")
|
end
|
||||||
|
|
||||||
puts "Users reset"
|
puts "Users reset"
|
||||||
|
|
||||||
|
family_names.each do |family_name|
|
||||||
|
family = Family.find_by(name: family_name)
|
||||||
|
|
||||||
|
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 subcategory
|
||||||
|
restaurants = family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, classification: "expense")
|
||||||
|
|
||||||
|
# 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)
|
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_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)
|
||||||
|
|
||||||
puts "Accounts created"
|
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: -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: 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: -5000, currency: "EUR", name: "EUR income Transaction")
|
||||||
create_transaction!(account: eur_checking, amount: 100, currency: "EUR", name: "EUR expense Transaction")
|
create_transaction!(account: eur_checking, amount: 100, currency: "EUR", name: "EUR expense Transaction")
|
||||||
|
|
||||||
puts "Transactions created"
|
puts "Transactions created for #{family_name}"
|
||||||
|
end
|
||||||
|
|
||||||
puts "Demo data loaded successfully!"
|
puts "Demo data loaded successfully!"
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,21 +66,25 @@ class IncomeStatement
|
||||||
totals = totals_query(transactions_scope: family.transactions.active.in_period(period)).select { |t| t.classification == classification }
|
totals = totals_query(transactions_scope: family.transactions.active.in_period(period)).select { |t| t.classification == classification }
|
||||||
classification_total = totals.sum(&:total)
|
classification_total = totals.sum(&:total)
|
||||||
|
|
||||||
category_totals = totals.map do |ct|
|
uncategorized_category = family.categories.uncategorized
|
||||||
# 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
|
category_totals = [ *categories, uncategorized_category ].map do |category|
|
||||||
children_totals = if ct.parent_category_id.nil? && ct.category_id.present?
|
subcategory = categories.find { |c| c.id == category.parent_id }
|
||||||
totals.select { |t| t.parent_category_id == ct.category_id }.sum(&:total)
|
|
||||||
else
|
parent_category_total = totals.select { |t| t.category_id == category.id }&.sum(&:total) || 0
|
||||||
|
|
||||||
|
children_totals = if category == uncategorized_category
|
||||||
0
|
0
|
||||||
|
else
|
||||||
|
totals.select { |t| t.parent_category_id == category.id }&.sum(&:total) || 0
|
||||||
end
|
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
|
weight = (category_total.zero? ? 0 : category_total.to_f / classification_total) * 100
|
||||||
|
|
||||||
CategoryTotal.new(
|
CategoryTotal.new(
|
||||||
category: categories.find { |c| c.id == ct.category_id } || family.categories.uncategorized,
|
category: category,
|
||||||
total: category_total,
|
total: category_total,
|
||||||
currency: family.currency,
|
currency: family.currency,
|
||||||
weight: weight,
|
weight: weight,
|
||||||
|
|
|
@ -12,6 +12,12 @@ namespace :demo_data do
|
||||||
end
|
end
|
||||||
|
|
||||||
task multi_currency: :environment do
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,15 +8,15 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
@income_category = @family.categories.create! name: "Income", classification: "income"
|
@income_category = @family.categories.create! name: "Income", classification: "income"
|
||||||
@food_category = @family.categories.create! name: "Food", classification: "expense"
|
@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
|
@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
|
@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: -1000, category: @income_category)
|
||||||
create_transaction(account: @checking_account, amount: 200, category: @shopping_category)
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
||||||
create_transaction(account: @credit_card_account, amount: 300, category: @food_category)
|
create_transaction(account: @credit_card_account, amount: 300, category: @groceries_category)
|
||||||
create_transaction(account: @credit_card_account, amount: 400, category: @shopping_category)
|
create_transaction(account: @credit_card_account, amount: 400, category: @groceries_category)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculates totals for transactions" do
|
test "calculates totals for transactions" do
|
||||||
|
@ -28,12 +28,23 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
test "calculates expenses for a period" do
|
test "calculates expenses for a period" do
|
||||||
income_statement = IncomeStatement.new(@family)
|
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
|
end
|
||||||
|
|
||||||
test "calculates income for a period" do
|
test "calculates income for a period" do
|
||||||
income_statement = IncomeStatement.new(@family)
|
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
|
end
|
||||||
|
|
||||||
test "calculates median expense" do
|
test "calculates median expense" do
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue