Remove budget relations and state

This commit is contained in:
Klaus Zanders
2025-08-25 14:05:13 +02:00
parent f667aa555e
commit de5c417277
28 changed files with 28 additions and 1150 deletions
@@ -1,61 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Budgets
class ChildBudgetsRowComponent < ::OpPrimer::BorderBoxRowComponent
def budget_relation
model
end
def budget
budget_relation.child_budget
end
def id
link_to "##{budget.id}", budget_path(budget)
end
def subject
link_to budget.subject, budget_path(budget)
end
def project
link_to budget.project.name, project_path(budget.project)
end
def relation_type
I18n.t(budget_relation.relation_type, scope: %i[activerecord attributes budget_relation relation_types])
end
def budget_amount
number_to_currency(budget.budget)
end
end
end
@@ -1,70 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Budgets
class ChildBudgetsTableComponent < ::OpPrimer::BorderBoxTableComponent
columns :id, :subject, :project, :relation_type, :budget_amount
main_column :subject, :proejct, :relation_type, :budget_amount
def sortable?
false
end
def paginated?
false
end
def has_actions?
false
end
def empty_row_message
I18n.t :no_results_title_text
end
def row_class
Budgets::ChildBudgetsRowComponent
end
def mobile_title
I18n.t(:label_budget_child_budgets)
end
def headers
[
[:id, { caption: Budget.human_attribute_name(:id) }],
[:subject, { caption: Budget.human_attribute_name(:subject) }],
[:project, { caption: Budget.human_attribute_name(:project) }],
[:relation_type, { caption: BudgetRelation.human_attribute_name(:relation_type) }],
[:budget_amount, { caption: Budget.human_attribute_name(:budget) }]
]
end
end
end
@@ -1,59 +0,0 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Budgets
class ParentPageHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include ApplicationHelper
def initialize(budget:, project:)
super
@budget = budget
@project = project
end
def breadcrumb_items
[
{ href: project_overview_path(@project.id), text: @project.name },
{ href: projects_budgets_path(@project.id), text: t(:label_budget_plural) },
{ href: budget_path(@budget.id), text: t(:label_budget_id, id: @budget.id) },
t(:button_manage_parent)
]
end
def call
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:button_manage_parent) }
header.with_breadcrumbs(breadcrumb_items)
end
end
end
end
@@ -18,21 +18,6 @@
t(:button_update)
end
end
if authorize_for(:budgets, :parent) && @budget.project.parent_id.present?
header.with_action_button(
tag: :a,
mobile_icon: "git-pull-request",
mobile_label: t(:button_manage_parent),
size: :medium,
href: url_for({ controller: "budgets", action: "parent", id: @budget }),
aria: { label: t(:button_manage_parent) },
data: { test_selector: "budget-parent-button" },
title: t(:button_manage_parent)
) do |button|
button.with_leading_visual_icon(icon: "git-pull-request")
t(:button_manage_parent)
end
end
if authorize_for(:budgets, :copy)
header.with_action_button(
tag: :a,
@@ -38,7 +38,6 @@ module Budgets
attribute :description
attribute :fixed_date
attribute :project
attribute :state
attribute :base_amount
attribute :new_material_budget_item_attributes,
readable: false
@@ -29,7 +29,7 @@
class BudgetsController < ApplicationController
include AttachableServiceCall
before_action :find_budget, only: %i[show edit update copy destroy_info parent update_parent destroy_parent]
before_action :find_budget, only: %i[show edit update copy destroy_info]
before_action :find_budgets, only: :destroy
before_action :check_and_update_belonging_work_packages, only: :destroy
before_action :find_project_by_project_id, only: %i[new create update_material_budget_item update_labor_budget_item]
@@ -47,12 +47,16 @@ class BudgetsController < ApplicationController
helper :sort
include SortHelper
helper :projects
include ProjectsHelper
helper :attachments
include AttachmentsHelper
helper :costlog
include CostlogHelper
helper :budgets
include BudgetsHelper
include PaginationHelper
@@ -74,7 +78,6 @@ class BudgetsController < ApplicationController
def show
@edit_allowed = User.current.allowed_in_project?(:edit_budgets, @project)
@child_budget_relations = @budget.child_budget_relations.includes(child_budget: :project)
respond_to do |format|
format.html { render action: "show", layout: !request.xhr? }
@@ -195,35 +198,6 @@ class BudgetsController < ApplicationController
end
end
def parent
@parent_projects = @project.ancestors
@budget_candidates = Budget.visible(User.current).where(project_id: @parent_projects)
@parent_budget_relation = @budget.parent_budget_relation || BudgetRelation.new
end
def update_parent
parent_relation = @budget.parent_budget_relation || @budget.build_parent_budget_relation
if parent_relation.update(budget_relation_params)
flash[:notice] = t(:notice_successful_update)
redirect_to budget_path(@budget)
else
flash[:error] = t(:notice_failed_update)
render action: :parent, status: :unprocessable_entity
end
end
def destroy_parent
if @budget.parent_budget_relation.destroy
flash[:notice] = t(:notice_relation_destroyed)
redirect_to budget_path(@budget), method: :get
else
flash[:error] = t(:notice_failed_delete)
render action: :parent, status: :unprocessable_entity
end
end
private
def find_budget
@@ -265,10 +239,6 @@ class BudgetsController < ApplicationController
response
end
def budget_relation_params
params.expect(budget_relation: %i[parent_budget_id relation_type])
end
def default_budget_sort
{
"id" => "#{Budget.table_name}.id",
+8 -56
View File
@@ -46,18 +46,6 @@ class Budget < ApplicationRecord
has_many :cost_entries, through: :work_packages
has_many :time_entries, through: :work_packages
has_one :parent_budget_relation, class_name: "BudgetRelation",
foreign_key: "child_budget_id",
dependent: :destroy,
inverse_of: :child_budget
has_one :parent_budget, through: :parent_budget_relation, source: :parent_budget
has_many :child_budget_relations, class_name: "BudgetRelation",
foreign_key: "parent_budget_id",
dependent: :destroy,
inverse_of: :parent_budget
include ActiveModel::ForbiddenAttributesProtection
acts_as_attachable
@@ -68,16 +56,7 @@ class Budget < ApplicationRecord
url: Proc.new { |o| { controller: "budgets", action: "show", id: o.id } }
validates :subject, :project, :author, :fixed_date, presence: true
validates :subject, length: { maximum: 255 }
validates :subject, length: { minimum: 1 }
enum :state, {
planned: "planned",
draft: "draft",
submitted: "submitted",
approved: "approved",
rejected: "rejected"
}, validate: { allow_nil: true }
validates :subject, length: { minimum: 1, maximum: 255 }
class << self
def visible(user)
@@ -99,8 +78,10 @@ class Budget < ApplicationRecord
protected
def copy_attributes(source)
source.attributes.slice("project_id", "subject", "description", "fixed_date", "state",
"base_amount").merge("author" => User.current)
source
.attributes
.slice("project_id", "subject", "description", "fixed_date", "base_amount")
.merge("author" => User.current)
end
def copy_budget_items(source, sink, items:)
@@ -119,34 +100,7 @@ class Budget < ApplicationRecord
end
def budget
base_amount + material_budget + labor_budget + budget_added_by_children
end
def budget_added_by_children
# TODO: Efficient with query
@budget_added_by_children ||= child_budget_relations.add.includes(:child_budget).sum do |rel|
rel.child_budget.budget
end
end
def allocated_to_children
# TODO: Efficient with query
@allocated_to_children ||= child_budget_relations.includes(:child_budget).sum { |rel| rel.child_budget.budget }
end
def allocated_unused
allocated_to_children - spent_on_children
end
def spent_with_children
spent + spent_on_children
end
def spent_on_children
# TODO: Efficient with query
@spent_on_children ||= child_budget_relations.includes(:child_budget).sum do |rel|
rel.child_budget.spent_with_children
end
base_amount + material_budget + labor_budget
end
def type_label
@@ -161,9 +115,7 @@ class Budget < ApplicationRecord
def budget_ratio
return 0.0 if budget.nil? || budget == 0.0
gone = spent + allocated_to_children
((gone / budget) * 100).round
((spent / budget) * 100).round
end
def css_classes
@@ -215,7 +167,7 @@ class Budget < ApplicationRecord
end
def available
budget - spent - allocated_to_children
budget - spent
end
def new_material_budget_item_attributes=(material_budget_item_attributes)
@@ -1,39 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class BudgetRelation < ApplicationRecord
belongs_to :parent_budget, class_name: "Budget"
belongs_to :child_budget, class_name: "Budget"
enum :relation_type, { add: "add", subtract: "subtract" }, default: :subtract
validates :child_budget, uniqueness: true
validates :child_budget, :parent_budget, :relation_type, presence: true
end
@@ -1,39 +0,0 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
class Queries::Projects::Selects::BudgetAllocated < Queries::Selects::Base
def self.key
:budget_allocated
end
def caption
I18n.t(:label_budget_allocated)
end
end
@@ -50,12 +50,6 @@ See COPYRIGHT and LICENSE files for more details.
value: unitless_currency_number(@budget.base_amount) %>
</div>
<div class="form--field">
<%= f.select :state,
Budget.states.keys.map { |state| [I18n.t(state, scope: %i[activerecord attributes budget states]), state] },
{ include_blank: true } %>
</div>
<%= render partial: "budgets/subform/material_budget_subform" %>
<%= render partial: "budgets/subform/labor_budget_subform" %>
@@ -39,13 +39,11 @@ See COPYRIGHT and LICENSE files for more details.
<col>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<%= sort_header_tag("id", caption: "#", default_order: "desc") %>
<%= sort_header_tag("subject", caption: Budget.human_attribute_name(:subject)) %>
<%= sort_header_tag("state", caption: Budget.human_attribute_name(:state)) %>
<th class="currency">
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
@@ -93,11 +91,6 @@ See COPYRIGHT and LICENSE files for more details.
<tr id="budget-<%= budget.id %>" class="<%= budget.css_classes %>">
<td><%= link_to budget.id, budget_path(budget.id) %></td>
<%= content_tag(:td, link_to(h(budget.subject), budget_path(budget.id)), class: "subject") %>
<td>
<% if budget.state.present? %>
<%= I18n.t(budget.state, scope: %i[activerecord attributes budget states]) %>
<% else %> - <% end %>
</td>
<%= content_tag(:td, number_to_currency(budget.budget, precision: 0), class: "currency") %>
<%= content_tag(:td, number_to_currency(budget.spent, precision: 0), class: "currency") %>
<%= content_tag(:td, number_to_currency(budget.available, precision: 0), class: "currency") %>
@@ -112,7 +105,6 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% if budgets.length > 0 %>
<tr>
<td />
<td />
<td />
<td class="currency"><strong><%= number_to_currency(total_budget, precision: 0) %></strong></td>
@@ -1,70 +0,0 @@
<% html_title "#{t(:button_manage_parent)} #{t(:label_budget_id, id: @budget.id)}: #{@budget.subject}" %>
<%= render Budgets::ParentPageHeaderComponent.new(budget: @budget, project: @project) %>
<%-
parent_budget_relation = @parent_budget_relation
parent_candidates = @budget_candidates
%>
<%=
primer_form_with(scope: :budget_relation, action: :update_parent, method: :post) do |budget_parent_form|
render_inline_form(budget_parent_form) do |form|
form.html_content do
render(Primer::Beta::Subhead.new(hide_border: true)) do |subhead|
subhead.with_heading(tag: :h3, size: :medium) { I18n.t("budgets.parent_relation.heading") }
subhead.with_description { I18n.t("budgets.parent_relation.description") }
end
end
form.select_list(
name: :parent_budget_id,
label: BudgetRelation.human_attribute_name(:parent_budget),
required: true,
input_width: :large
) do |select|
parent_candidates.each do |budget|
select.option(
label: "#{budget.project.name}: ##{budget.id} - #{budget.subject}",
value: budget.id,
selected: budget.id == parent_budget_relation.parent_budget_id
)
end
end
form.radio_button_group(name: :relation_type) do |group|
group.radio_button(
value: :add,
checked: parent_budget_relation.add?,
label: I18n.t("budgets.relation_types.add.label"),
caption: I18n.t("budgets.relation_types.add.helptext")
)
group.radio_button(
value: :subtract,
checked: parent_budget_relation.subtract?,
label: I18n.t("budgets.relation_types.subtract.label"),
caption: I18n.t("budgets.relation_types.subtract.helptext")
)
end
form.submit(name: :submit, label: I18n.t(:button_set_parent))
end
end
%>
<% if @budget.parent_budget_relation.present? %>
<%= render(Primer::Beta::Subhead.new(mt: 5, hide_border: true)) do |subhead|
subhead.with_heading(tag: :h3, size: :medium) { I18n.t("budgets.delete_parent_relation.heading") }
subhead.with_description { I18n.t("budgets.delete_parent_relation.description") }
end %>
<%= render Primer::Beta::Button.new(
tag: :a,
scheme: :danger,
title: I18n.t(:button_delete_link_to_parent),
href: parent_budget_path(@budget),
data: { "method" => :delete }
) do |button|
button.with_leading_visual_icon(icon: :trash)
I18n.t(:button_delete_link_to_parent)
end %>
<% end %>
@@ -47,21 +47,6 @@ See COPYRIGHT and LICENSE files for more details.
key: Budget.human_attribute_name(:budget_ratio),
value: extended_progress_bar(@budget.budget_ratio, width: "80px", legend: @budget.budget_ratio)
)
component.with_attribute(
key: Budget.human_attribute_name(:state),
value: @budget.state.present? ? I18n.t(@budget.state, scope: %i[activerecord attributes budget states]) : "-"
)
component.with_attribute(
key: Budget.human_attribute_name(:parent_budget),
value: if @budget.parent_budget_relation.present?
link_to(
"#{@budget.parent_budget.project.name}: #{@budget.parent_budget.subject} (#{I18n.t(@budget.parent_budget_relation.relation_type, scope: %i[activerecord attributes budget_relation relation_types])})",
budget_path(@budget.parent_budget)
)
else
"-"
end
)
component.with_attribute(
key: Budget.human_attribute_name(:description),
value: content_tag(:div, class: "op-uc-container") do
@@ -86,12 +71,6 @@ See COPYRIGHT and LICENSE files for more details.
key: Budget.human_attribute_name(:labor_budget),
value: number_to_currency(@budget.labor_budget)
)
if @budget.child_budget_relations.add.any?
component.with_attribute(
key: Budget.human_attribute_name(:budget_added_by_children),
value: number_to_currency(@budget.budget_added_by_children)
)
end
component.with_attribute(
key: Budget.human_attribute_name(:budget),
value: content_tag(:strong, number_to_currency(@budget.budget))
@@ -100,12 +79,6 @@ See COPYRIGHT and LICENSE files for more details.
key: Budget.human_attribute_name(:spent),
value: number_to_currency(@budget.spent * -1)
)
if @budget.child_budget_relations.subtract.any?
component.with_attribute(
key: Budget.human_attribute_name(:allocated_to_children),
value: number_to_currency(@budget.allocated_to_children * -1)
)
end
component.with_attribute(
key: Budget.human_attribute_name(:available),
value: content_tag(:strong, number_to_currency(@budget.available))
@@ -118,14 +91,6 @@ See COPYRIGHT and LICENSE files for more details.
<%= list_attachments(resource) %>
<%= render partial: "budget_items" %>
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend">
<%= I18n.t(:label_budget_child_budgets) %>
</legend>
<%= render(Budgets::ChildBudgetsTableComponent.new(rows: @child_budget_relations)) %>
</fieldset>
</div>
<div style="clear: both;"></div>
-38
View File
@@ -36,8 +36,6 @@ en:
budget:
author: "Author"
available: "Available"
allocated_to_children: "Allocated"
budget_added_by_children: "Added by children"
budget: "Planned"
budget_ratio: "Spent (ratio)"
description: "Description"
@@ -48,23 +46,8 @@ en:
labor_budget: "Planned labor costs"
material_budget: "Planned unit costs"
base_amount: "Base amount"
parent_budget: "Parent budget"
state: "State"
states:
planned: "Planned"
draft: "Draft"
submitted: "Submitted"
approved: "Approved"
rejected: "Rejected"
work_package:
budget_subject: "Budget title"
budget_relation:
parent_budget: "Parent budget"
child_budget: "Child budget"
relation_type: "Relation to this"
relation_types:
add: "add budget to parent"
subtract: "allocated budget from parent"
models:
budget: "Budget"
material_budget_item: "Unit"
@@ -81,9 +64,6 @@ en:
button_add_cost_type: "Add cost type"
button_cancel_edit_budget: "Cancel editing budget"
button_cancel_edit_costs: "Cancel editing costs"
button_manage_parent: "Manage parent budget"
button_set_parent: "Set parent"
button_delete_link_to_parent: "Delete link to parent budget"
caption_labor: "Labor"
caption_labor_costs: "Actual labor costs"
@@ -105,18 +85,15 @@ en:
label_budget_plural: "Budgets"
label_budget_spent: "Budget spent"
label_budget_spent_ratio: "Budget spent ratio"
label_budget_allocated: "Budget allocated to children"
label_deliverable: "Budget"
label_example_placeholder: "e.g., %{decimal}"
label_view_all_budgets: "View all budgets"
label_yes: "Yes"
label_budget_child_budgets: "Child budgets"
label_budget_totals: "Totals"
label_budget_details: "Budget details"
notice_budget_conflict: "Work packages must be of the same project."
notice_no_budgets_available: "No budgets available."
notice_relation_destroyed: "The budget relation has been removed."
permission_edit_budgets: "Edit budgets"
permission_view_budgets: "View budgets"
@@ -125,18 +102,3 @@ en:
text_budget_reassign_to: "Reassign them to this budget:"
text_budget_delete: "Delete the budget from all work packages"
text_budget_destroy_assigned_wp: "There are %{count} work packages assigned to this budget. What do you want to do?"
budgets:
parent_relation:
heading: "Mark this project as a child budget to another budget"
description: "This will set up a dependency from this budget to its parent. Depending on the type of relation, the budgeted amount will be added to or subtracted from the parent's budget."
delete_parent_relation:
heading: "Delete parent budget relation"
description: "This will remove the dependency from this budget to its parent. The budgeted amount of the parent budget and all its ancestors will also change!"
relation_types:
add:
label: "Add budget to parent"
helptext: "The budgeted amount for this budget will be added to the parent's budget and also mark the amount as allocated to this budget."
subtract:
label: "Subtract budget from parent"
helptext: "The budgeted amount for this budget will be subtracted from the parent's budget and marked as allocated to this budget."
-3
View File
@@ -36,9 +36,6 @@ Rails.application.routes.draw do
resources :budgets, only: %i[show update destroy edit] do
member do
get :parent
post :parent, to: "budgets#update_parent"
delete :parent, to: "budgets#destroy_parent"
get :copy
get :destroy_info
end
@@ -1,43 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CreateBudgetRelation < ActiveRecord::Migration[8.0]
def change
create_table :budget_relations do |t|
t.references :parent_budget, null: false, foreign_key: { to_table: :budgets }
t.references :child_budget, null: false, foreign_key: { to_table: :budgets }
t.references :cost_type, null: true, foreign_key: { to_table: :cost_types }
t.string :relation_type, null: false, default: "add"
t.timestamps
end
end
end
@@ -1,36 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class AddBudgetState < ActiveRecord::Migration[8.0]
def change
add_column :budgets, :state, :string, null: true
add_column :budget_journals, :state, :string, null: true
end
end
@@ -1,37 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class AddBudgetStateIndex < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
add_index :budgets, :state, algorithm: :concurrently
end
end
@@ -1,10 +0,0 @@
# frozen_string_literal: true
class UniqueBudgetRelationChild < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
remove_index :budget_relations, :child_budget_id, if_exists: true, algorithm: :concurrently
add_index :budget_relations, :child_budget_id, unique: true, algorithm: :concurrently
end
end
@@ -1,7 +0,0 @@
# frozen_string_literal: true
class RemoveCostTypeFromBudgetRelation < ActiveRecord::Migration[8.0]
def change
remove_reference(:budget_relations, :cost_type, foreign_key: true)
end
end
+1 -2
View File
@@ -13,7 +13,7 @@ module Budgets
permissible_on: :project
permission :edit_budgets,
{
budgets: %i[index show edit update destroy destroy_info new create copy parent destroy_parent update_parent]
budgets: %i[index show edit update destroy destroy_info new create copy]
},
permissible_on: :project
end
@@ -72,7 +72,6 @@ module Budgets
select Queries::Projects::Selects::BudgetSpent
select Queries::Projects::Selects::BudgetSpentRatio
select Queries::Projects::Selects::BudgetAvailable
select Queries::Projects::Selects::BudgetAllocated
end
end
end
@@ -55,12 +55,7 @@ module Budgets::Patches::Projects::RowComponentPatch
end
def total_ratio
gone = total_spent + total_allocated
@total_ratio ||= total_planned.zero? ? 0 : ((gone / total_planned) * 100).round
end
def total_allocated
@total_allocated ||= budgets.sum(&:allocated_to_children)
@total_ratio ||= total_planned.zero? ? 0 : ((total_spent / total_planned) * 100).round
end
def budgets
@@ -88,12 +83,6 @@ module Budgets::Patches::Projects::RowComponentPatch
end
end
def budget_allocated
with_project_budgets do |project_budgets|
number_to_currency(project_budgets.total_allocated, precision: 0)
end
end
def budget_available
with_project_budgets do |project_budgets|
number_to_currency(project_budgets.total_available, precision: 0)
@@ -38,10 +38,8 @@ FactoryBot.define do
created_at { 3.days.ago }
updated_at { 3.days.ago }
traits_for_enum(:state)
trait :with_base_amount do
base_amount { BigDecimal(250000000) }
base_amount { BigDecimal(250_000) }
end
end
end
@@ -1,38 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
FactoryBot.define do
factory :budget_relation do
parent_budget factory: :budget
child_budget factory: :budget
traits_for_enum(:relation_type)
end
end
@@ -1,407 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require_relative "../spec_helper"
RSpec.describe BudgetRelation do
shared_let(:admin) { create(:admin) }
before do
allow(User).to receive(:current).and_return(admin)
end
describe "calculation logic" do
describe "bottom->up" do
let(:portfolio) { create(:project, project_type: :portfolio, name: "Portfolio") }
let(:portfolio_budget) do
create(:budget, project: portfolio, base_amount: 0, subject: "Portfolio Budget")
end
let(:program1) { create(:project, project_type: :program, parent: portfolio, name: "Program 1") }
let(:program1_budget) { create(:budget, project: program1, base_amount: 0, subject: "Program 1 Budget") }
let(:project1) { create(:project, project_type: :project, parent: program1, name: "Project 1") }
let(:project1_budget) { create(:budget, project: project1, base_amount: 5_000, subject: "Project 1 Budget") }
let(:project2) { create(:project, project_type: :project, parent: program1, name: "Project 2") }
let(:project2_budget) { create(:budget, project: project2, base_amount: 2_500, subject: "Project 2 Budget") }
context "without any relations" do
it "calculates the correct values" do
expect(portfolio_budget).to have_attributes(budget: 0,
allocated_to_children: 0,
spent: 0,
available: 0)
expect(program1_budget).to have_attributes(budget: 0,
allocated_to_children: 0,
spent: 0,
available: 0)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 0,
available: 5_000)
expect(project2_budget).to have_attributes(budget: 2_500,
allocated_to_children: 0,
spent: 0,
available: 2_500)
end
end
context "when setting up project1 to add itself to the program1 budget" do
before do
described_class.create!(parent_budget: program1_budget,
child_budget: project1_budget,
relation_type: :add)
end
it "allocates the project's budget to the program's budget" do
expect(portfolio_budget).to have_attributes(budget: 0,
allocated_to_children: 0,
spent: 0,
available: 0)
expect(program1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 5_000,
spent: 0,
available: 0)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 0,
available: 5_000)
expect(project2_budget).to have_attributes(budget: 2_500,
allocated_to_children: 0,
spent: 0,
available: 2_500)
end
context "when also setting up project2 to add itself to the program1 budget" do
before do
described_class.create!(parent_budget: program1_budget,
child_budget: project2_budget,
relation_type: :add)
end
it "allocates both project's budget to the program's budget" do
expect(portfolio_budget).to have_attributes(budget: 0,
allocated_to_children: 0,
spent: 0,
available: 0)
expect(program1_budget).to have_attributes(budget: 7_500,
allocated_to_children: 7_500,
spent: 0,
available: 0)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 0,
available: 5_000)
expect(project2_budget).to have_attributes(budget: 2_500,
allocated_to_children: 0,
spent: 0,
available: 2_500)
end
context "when setting program1 to add itself to the portfolio budget" do
before do
described_class.create!(parent_budget: portfolio_budget,
child_budget: program1_budget,
relation_type: :add)
end
it "allocates the program's budget to the portfolio's budget" do
expect(portfolio_budget).to have_attributes(budget: 7_500,
allocated_to_children: 7_500,
spent: 0,
available: 0)
expect(program1_budget).to have_attributes(budget: 7_500,
allocated_to_children: 7_500,
spent: 0,
available: 0)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 0,
available: 5_000)
expect(project2_budget).to have_attributes(budget: 2_500,
allocated_to_children: 0,
spent: 0,
available: 2_500)
end
context "when adding some costs to the projects" do
let(:work_package1) { create(:work_package, project: project1, budget: project1_budget) }
let!(:cost_entry1) do
create(:cost_entry, project: project1, entity: work_package1, overridden_costs: 500)
end
let(:work_package2) { create(:work_package, project: project2, budget: project2_budget) }
let!(:cost_entry2) do
create(:cost_entry, project: project2, entity: work_package2, overridden_costs: 750)
end
it "subtracts the costs from the budgets" do
expect(portfolio_budget).to have_attributes(spent: 0,
spent_with_children: 1_250)
expect(program1_budget).to have_attributes(spent: 0,
spent_with_children: 1_250)
expect(project1_budget).to have_attributes(budget: 5_000,
spent: 500,
available: 4_500)
expect(project2_budget).to have_attributes(budget: 2_500,
spent: 750,
available: 1_750)
end
end
end
end
end
end
describe "top->down" do
let(:portfolio) { create(:project, project_type: :portfolio, name: "Portfolio") }
let(:portfolio_budget) { create(:budget, project: portfolio, base_amount: 10_000, subject: "Portfolio Budget") }
let(:program1) { create(:project, project_type: :program, parent: portfolio, name: "Program 1") }
let(:program1_budget) { create(:budget, project: program1, base_amount: 7_000, subject: "Program 1 Budget") }
let(:project1) { create(:project, project_type: :project, parent: program1, name: "Project 1") }
let(:project1_budget) { create(:budget, project: project1, base_amount: 5_000, subject: "Project 1 Budget") }
let(:program2) { create(:project, project_type: :program, parent: portfolio, name: "Program 2") }
let(:program2_budget) { create(:budget, project: program2, base_amount: 3_000, subject: "Program 2 Budget") }
let(:project2) { create(:project, project_type: :project, parent: program2, name: "Project 2") }
let(:project2_budget) { create(:budget, project: project2, base_amount: 2_000, subject: "Project 2 Budget") }
context "without any relations" do
it "calculates the correct values" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 0,
spent: 0,
available: 10_000)
end
end
context "with the portfolio -> program1 relation" do
before do
described_class.create!(parent_budget: portfolio_budget,
child_budget: program1_budget,
relation_type: :subtract)
end
it "allocates the program's budget out of the portfolio budget" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 7_000,
spent: 0,
available: 3_000)
expect(program1_budget).to have_attributes(budget: 7_000,
allocated_to_children: 0,
spent: 0,
available: 7_000)
end
context "with the program1 -> project1 relation" do
before do
described_class.create!(parent_budget: program1_budget,
child_budget: project1_budget,
relation_type: :subtract)
end
it "allocates the project's budget out of the program's budget" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 7_000,
spent: 0,
available: 3_000)
expect(program1_budget).to have_attributes(budget: 7_000,
allocated_to_children: 5_000,
spent: 0,
available: 2_000)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 0,
available: 5_000)
end
end
context "with the portfolio -> program1, program2 relation" do
before do
described_class.create!(parent_budget: portfolio_budget,
child_budget: program2_budget,
relation_type: :subtract)
end
it "allocates the program's budget out of the portfolio's budget" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 10_000,
spent: 0,
available: 0)
expect(program1_budget).to have_attributes(budget: 7_000,
allocated_to_children: 0,
spent: 0,
available: 7_000)
expect(program2_budget).to have_attributes(budget: 3_000,
allocated_to_children: 0,
spent: 0,
available: 3_000)
end
context "with the program2 -> project2 relation" do
before do
described_class.create!(parent_budget: program2_budget,
child_budget: project2_budget,
relation_type: :subtract)
end
it "allocates the project's budget out of the program's budget" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 10_000,
spent: 0,
available: 0)
expect(program2_budget).to have_attributes(budget: 3_000,
allocated_to_children: 2_000,
spent: 0,
available: 1_000)
expect(project2_budget).to have_attributes(budget: 2_000,
allocated_to_children: 0,
spent: 0,
available: 2_000)
end
context "with logged costs on project1" do
let(:work_package) { create(:work_package, project: project2, budget: project2_budget) }
let!(:cost_entry) do
create(:cost_entry, project: project2, entity: work_package, overridden_costs: 500)
end
it "subtracts the cost from project2's budget" do
expect(portfolio_budget.spent).to eq(0)
expect(portfolio_budget.spent_with_children).to eq(500)
expect(program2_budget.spent).to eq(0)
expect(program2_budget.spent_with_children).to eq(500)
expect(project2_budget).to have_attributes(budget: 2_000,
allocated_to_children: 0,
spent: 500,
available: 1_500)
end
end
end
end
end
end
describe "bottom->up and top->down mixed" do
# - The portfolio has a budget of 10.000€.
# - The program does not have an assigned budget but is the sum of the project's budgets.
# - The program budget is taken out of the portfolio budget.
# - The projects have budgets of 5.000€ and 2.500€.
# - The projects have half of their budgets already spent.
let!(:portfolio) { create(:project, project_type: :portfolio, name: "Portfolio") }
let!(:portfolio_budget) { create(:budget, project: portfolio, base_amount: 10_000, subject: "Portfolio Budget") }
let!(:program1) { create(:project, project_type: :program, parent: portfolio, name: "Program 1") }
let!(:program1_budget) { create(:budget, project: program1, base_amount: 0, subject: "Program 1 Budget") }
let!(:portfolio_program_relation) do
create(:budget_relation, parent_budget: portfolio_budget, child_budget: program1_budget, relation_type: :subtract)
end
let!(:project1) { create(:project, project_type: :project, parent: program1, name: "Project 1") }
let!(:project1_budget) { create(:budget, project: project1, base_amount: 5_000, subject: "Project 1 Budget") }
let!(:project1_program_relation) do
create(:budget_relation, parent_budget: program1_budget, child_budget: project1_budget, relation_type: :add)
end
let!(:work_package1) { create(:work_package, project: project1, budget: project1_budget) }
let!(:cost_entry1) do
create(:cost_entry, project: project1, entity: work_package1, overridden_costs: 2_500)
end
let!(:project2) { create(:project, project_type: :project, parent: program1, name: "Project 2") }
let!(:project2_budget) { create(:budget, project: project2, base_amount: 2_500, subject: "Project 2 Budget") }
let!(:project2_program_relation) do
create(:budget_relation, parent_budget: program1_budget, child_budget: project2_budget, relation_type: :add)
end
let!(:work_package2) { create(:work_package, project: project2, budget: project2_budget) }
let!(:cost_entry2) do
create(:cost_entry, project: project2, entity: work_package2, overridden_costs: 1_250)
end
it "calculates the correct values" do
expect(portfolio_budget).to have_attributes(budget: 10_000,
allocated_to_children: 7_500,
spent: 0,
spent_with_children: 3_750,
available: 2_500)
expect(program1_budget).to have_attributes(budget: 7_500,
allocated_to_children: 7_500,
spent: 0,
spent_with_children: 3_750,
available: 0)
expect(project1_budget).to have_attributes(budget: 5_000,
allocated_to_children: 0,
spent: 2_500,
spent_with_children: 2_500,
available: 2_500)
expect(project2_budget).to have_attributes(budget: 2_500,
allocated_to_children: 0,
spent: 1_250,
spent_with_children: 1_250,
available: 1_250)
end
end
end
end
@@ -57,40 +57,34 @@ RSpec.describe PermittedParams do
end
end
describe "#budget" do
describe "budget" do
let(:attribute) { :budget }
context "subject" do
describe "#subject" do
let(:hash) { { "subject" => "subject_test" } }
it_behaves_like "allows params"
end
context "description" do
describe "#description" do
let(:hash) { { "description" => "description_test" } }
it_behaves_like "allows params"
end
context "fixed_date" do
describe "#fixed_date" do
let(:hash) { { "fixed_date" => "2017-03-01" } }
it_behaves_like "allows params"
end
context "base_amount" do
describe "#base_amount" do
let(:hash) { { "base_amount" => "250000.00" } }
it_behaves_like "allows params"
end
context "state" do
let(:hash) { { "state" => "approved" } }
it_behaves_like "allows params"
end
context "project_id" do
describe "#project_id" do
let(:hash) { { "project_id" => 42 } }
it_behaves_like "allows params" do
@@ -98,7 +92,7 @@ RSpec.describe PermittedParams do
end
end
context "existing material budget item" do
describe "#existing_material_budget_item" do
let(:hash) do
{ "existing_material_budget_item_attributes" => { "1" => {
"units" => "100.0",
@@ -111,7 +105,7 @@ RSpec.describe PermittedParams do
it_behaves_like "allows params"
end
context "new material budget item" do
describe "#new_material_budget_item" do
let(:hash) do
{ "new_material_budget_item_attributes" => { "1" => {
"units" => "20",
@@ -124,7 +118,7 @@ RSpec.describe PermittedParams do
it_behaves_like "allows params"
end
context "existing labor budget item" do
describe "#existing_labor_budget_item" do
let(:hash) do
{ "existing_labor_budget_item_attributes" => { "1" => {
"hours" => "20.0",
@@ -137,7 +131,7 @@ RSpec.describe PermittedParams do
it_behaves_like "allows params"
end
context "new labor budget item" do
describe "#new_labor_budget_item" do
let(:hash) do
{ "new_labor_budget_item_attributes" => { "1" => {
"hours" => "5.0",
@@ -44,11 +44,11 @@ module Costs::Patches::PermittedParamsPatch
def budget
params.require(:budget).permit(:subject,
:description,
:fixed_date, :base_amount, :state,
:fixed_date, :base_amount,
{ new_material_budget_item_attributes: %i[units cost_type_id comments amount] },
{ new_labor_budget_item_attributes: %i[hours user_id comments amount] },
{ existing_material_budget_item_attributes: %i[units cost_type_id comments amount] },
existing_labor_budget_item_attributes: %i[hours user_id comments amount])
{ existing_labor_budget_item_attributes: %i[hours user_id comments amount] })
end
def cost_type
@@ -149,7 +149,6 @@ RSpec.describe ProjectQuery do
budget_planned
budget_spent
budget_spent_ratio
budget_allocated
])
end
end
@@ -177,7 +176,6 @@ RSpec.describe ProjectQuery do
budget_available
budget_planned
budget_spent
budget_allocated
budget_spent_ratio
])
end