Include uncategorized spending in budget UI (#2877)

This commit is contained in:
Sure Admin (bot)
2026-08-01 07:50:09 +02:00
committed by GitHub
parent 2bdaa7a171
commit 1e800e2f93
6 changed files with 167 additions and 3 deletions

View File

@@ -2,7 +2,7 @@ module BudgetsHelper
def budget_has_over_budget?(budget)
return false unless budget.initialized?
budget.budget_categories.any?(&:any_over_budget?)
budget.budget_categories.any?(&:any_over_budget?) || budget.uncategorized_budget_category.any_over_budget?
end
def budget_categories_view_state(budget)

View File

@@ -208,7 +208,7 @@ class Budget < ApplicationRecord
# Continuous gray segment for empty budgets
return [ { color: "var(--budget-unallocated-fill)", amount: 1, id: unused_segment_id } ] unless allocations_valid?
segments = budget_categories.reject(&:subcategory?).map do |bc|
segments = donut_budget_categories.map do |bc|
{ color: bc.category.color, amount: budget_category_actual_spending(bc), id: bc.id }
end
@@ -219,6 +219,17 @@ class Budget < ApplicationRecord
segments
end
def donut_budget_categories
categories = budget_categories.reject(&:subcategory?).to_a
uncategorized = uncategorized_budget_category
if budget_category_actual_spending(uncategorized).positive?
categories << uncategorized
end
categories
end
# =============================================================================
# Actuals: How much user has spent on each budget category
# =============================================================================

View File

@@ -34,7 +34,7 @@
<% end %>
</div>
<% budget.budget_categories.each do |bc| %>
<% budget.donut_budget_categories.each do |bc| %>
<div id="segment_<%= bc.id %>" class="hidden">
<div class="flex flex-col gap-2 items-center">
<div class="flex items-center gap-3">

View File

@@ -104,4 +104,49 @@ class BudgetsHelperTest < ActionView::TestCase
assert_equal [ child.id ], group.budget_subcategories.map(&:category_id)
assert group.budget_subcategories.first.any_over_budget?
end
test "budget has over budget when uncategorized spending exceeds its allocation" do
family = Family.create!(name: "Helper Budget Repro", currency: "USD")
account = Account.create!(
family: family,
accountable: Depository.new,
name: "Checking",
status: "active",
currency: "USD",
balance: 0
)
category = Category.create!(
name: "Helper Groceries #{SecureRandom.hex(4)}",
family: family,
color: "#407706",
lucide_icon: "shopping-bag"
)
budget = Budget.create!(
family: family,
start_date: Date.current.beginning_of_month,
end_date: Date.current.end_of_month,
currency: "USD",
budgeted_spending: 100
)
BudgetCategory.create!(
budget: budget,
category: category,
budgeted_spending: 100,
currency: "USD"
)
Entry.create!(
account: account,
entryable: Transaction.create!(category: nil),
date: Date.current,
name: "Helper Uncategorized Over Budget",
amount: 125,
currency: "USD"
)
assert budget_has_over_budget?(Budget.find(budget.id))
end
end

View File

@@ -263,6 +263,7 @@ class BudgetTest < ActiveSupport::TestCase
budget.stubs(:available_to_spend).returns(200)
budget.stubs(:budget_category_actual_spending).with(parent_budget_category).returns(63.11)
budget.stubs(:budget_category_actual_spending).with(standalone_budget_category).returns(25)
budget.stubs(:budget_category_actual_spending).with(budget.uncategorized_budget_category).returns(0)
segments = budget.to_donut_segments_json
@@ -280,6 +281,59 @@ class BudgetTest < ActiveSupport::TestCase
assert_equal 200, segments_by_id["unused"][:amount]
end
test "to_donut_segments_json includes uncategorized spending" do
family = @family
account = Account.create!(
family: family,
accountable: Depository.new,
name: "Checking",
status: "active",
currency: "USD",
balance: 0
)
category = Category.create!(
name: "Groceries #{Time.now.to_f}",
family: family,
color: "#407706",
lucide_icon: "shopping-bag"
)
budget = Budget.create!(
family: family,
start_date: Date.current.beginning_of_month,
end_date: Date.current.end_of_month,
currency: "USD",
budgeted_spending: 100
)
BudgetCategory.create!(
budget: budget,
category: category,
budgeted_spending: 100,
currency: "USD"
)
Entry.create!(
account: account,
entryable: Transaction.create!(category: nil),
date: Date.current,
name: "Uncategorized donut spending",
amount: 125,
currency: "USD"
)
budget = Budget.find(budget.id)
uncategorized = budget.uncategorized_budget_category
segments = budget.to_donut_segments_json
uncategorized_segment = segments.find { |segment| segment[:id] == uncategorized.id }
assert_equal 125, budget.actual_spending
assert_equal 125, uncategorized.actual_spending
assert_not_nil uncategorized_segment
assert_equal 125, uncategorized_segment[:amount]
end
test "actual_spending subtracts uncategorized refunds" do
family = families(:dylan_family)
budget = Budget.find_or_bootstrap(family, start_date: Date.current.beginning_of_month)

View File

@@ -0,0 +1,54 @@
require "test_helper"
class BudgetDonutViewTest < ActionView::TestCase
test "renders hover content for uncategorized donut segment" do
family = Family.create!(name: "Budget Donut View Repro", currency: "USD")
account = Account.create!(
family: family,
accountable: Depository.new,
name: "Checking",
status: "active",
currency: "USD",
balance: 0
)
category = Category.create!(
name: "Groceries #{SecureRandom.hex(4)}",
family: family,
color: "#407706",
lucide_icon: "shopping-bag"
)
budget = Budget.create!(
family: family,
start_date: Date.current.beginning_of_month,
end_date: Date.current.end_of_month,
currency: "USD",
budgeted_spending: 100
)
BudgetCategory.create!(
budget: budget,
category: category,
budgeted_spending: 100,
currency: "USD"
)
Entry.create!(
account: account,
entryable: Transaction.create!(category: nil),
date: Date.current,
name: "Uncategorized hover spending",
amount: 125,
currency: "USD"
)
budget = Budget.find(budget.id)
uncategorized = budget.uncategorized_budget_category
html = render(partial: "budgets/budget_donut", locals: { budget: budget })
assert_includes html, "segment_#{uncategorized.id}"
assert_includes html, uncategorized.category.display_name
end
end