Project specific cost types

This commit is contained in:
Oliver Günther
2026-05-13 16:19:45 +02:00
parent 27ff34f0c9
commit 5e306a2eae
49 changed files with 2183 additions and 59 deletions
+4 -1
View File
@@ -85,6 +85,8 @@ class Project < ApplicationRecord
}, dependent: :destroy
has_many :time_entries, dependent: :delete_all
has_many :time_entry_activities_projects, dependent: :delete_all
has_many :cost_types_projects, dependent: :delete_all
has_many :cost_types, through: :cost_types_projects
has_many :queries, dependent: :destroy
has_many :news, -> { includes(:author) }, dependent: :destroy
has_many :categories, -> { order("#{Category.table_name}.name") }, dependent: :delete_all
@@ -193,7 +195,8 @@ class Project < ApplicationRecord
scope :templated, -> { where(templated: true) }
scopes :activated_time_activity,
:visible_with_activated_time_activity
:visible_with_activated_time_activity,
:available_cost_types
enum :status_code, {
on_track: 0,
+9 -3
View File
@@ -784,15 +784,21 @@ Redmine::MenuManager.map :project_menu do |menu|
},
versions: { caption: :label_version_plural },
repository: { caption: :label_repository },
time_entry_activities: { caption: :enumeration_activities },
time_and_costs: {
caption: :"cost_types.settings.time_and_costs",
controller: "/projects/settings/time_entry_activities"
},
storage: { caption: :label_required_disk_storage }
}
project_menu_items.each do |key, options|
menu.push :"settings_#{key}",
{ controller: "/projects/settings/#{key}", action: "show" }.merge(options.slice(:action)),
{
controller: options[:controller] || "/projects/settings/#{key}",
action: options[:action] || "show"
},
parent: :settings,
**options.except(:action)
**options.except(:action, :controller)
end
end
@@ -0,0 +1,43 @@
# 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 AddProjectScopingToCostTypes < ActiveRecord::Migration[8.0]
def change
add_column :cost_types, :is_for_all, :boolean, default: true, null: false
create_table :cost_types_projects do |t|
t.references :cost_type, null: false, foreign_key: true
t.references :project, null: false, foreign_key: true
t.timestamps
end
add_index :cost_types_projects, %i[cost_type_id project_id], unique: true
end
end
@@ -0,0 +1,56 @@
<%#-- 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.
++#%>
<%=
component_wrapper do
primer_form_with(
model: @cost_type_project_mapping,
url:,
data: { turbo: true },
method: :post
) do |form|
concat(
render(
Primer::Alpha::Dialog::Body.new(
id: dialog_body_id, test_selector: dialog_body_id, aria: { label: title },
classes: "Overlay-body_autocomplete_height"
)
) do
render(::CostTypes::CostTypeProjects::CostTypeMappingForm.new(form, project_mapping: @cost_type_project_mapping))
end
)
concat(
render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do
concat(render(Primer::Beta::Button.new(data: { "close-dialog-id": dialog_id })) { cancel_button_text })
concat(render(Primer::Beta::Button.new(scheme: :primary, type: :submit)) { submit_button_text })
end
)
end
end
%>
@@ -0,0 +1,71 @@
# 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 Admin
module CostTypes
module CostTypeProjects
class NewCostTypeProjectsFormModalComponent < ApplicationComponent
include OpTurbo::Streamable
DIALOG_ID = "new-cost-type-projects-modal"
DIALOG_BODY_ID = "new-cost-type-projects-modal-body"
def initialize(cost_type_project_mapping:, cost_type:, **)
@cost_type_project_mapping = cost_type_project_mapping
@cost_type = cost_type
super(@cost_type_project_mapping, **)
end
private
def url
url_helpers.admin_cost_type_projects_path(@cost_type)
end
def dialog_id = DIALOG_ID
def dialog_body_id = DIALOG_BODY_ID
attr_reader :cost_type_project_mapping, :cost_type
def title
I18n.t(:label_add_projects)
end
def cancel_button_text
I18n.t("button_cancel")
end
def submit_button_text
I18n.t("button_add")
end
end
end
end
end
@@ -0,0 +1,46 @@
<%#-- 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.
++#%>
<%=
render(
Primer::Alpha::Dialog.new(
id: dialog_id,
title:,
test_selector: dialog_id,
size: :large
)
) do |dialog|
dialog.with_header(
show_divider: false,
visually_hide_title: false,
variant: :large
)
render(form_modal_component)
end
%>
@@ -0,0 +1,66 @@
# 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 Admin
module CostTypes
module CostTypeProjects
class NewCostTypeProjectsModalComponent < ApplicationComponent
include OpTurbo::Streamable
def initialize(cost_type_project_mapping:, cost_type:, **)
@cost_type_project_mapping = cost_type_project_mapping
@cost_type = cost_type
super(@cost_type_project_mapping, **)
end
def render?
!cost_type.is_for_all?
end
private
attr_reader :cost_type_project_mapping, :cost_type
def dialog_id = NewCostTypeProjectsFormModalComponent::DIALOG_ID
def dialog_body_id = NewCostTypeProjectsFormModalComponent::DIALOG_BODY_ID
def title
I18n.t(:label_add_projects)
end
def form_modal_component
Admin::CostTypes::CostTypeProjects::NewCostTypeProjectsFormModalComponent.new(
cost_type_project_mapping:, cost_type:
)
end
end
end
end
end
@@ -0,0 +1,41 @@
<%#-- 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.
++#%>
<%= component_wrapper(tag: "tr", class: row_css_class, data: { turbo: true }) do %>
<% columns.each do |column| %>
<td class="<%= column_css_class(column) %>">
<%= column_value(column) %>
</td>
<% end %>
<td class="buttons">
<% button_links.each do |link| %>
<%= link %>
<% end %>
</td>
<% end %>
@@ -0,0 +1,67 @@
# 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 Admin
module CostTypes
module CostTypeProjects
class RowComponent < ::Projects::RowComponent
include OpTurbo::Streamable
def wrapper_uniq_by
"project-#{project.id}"
end
def more_menu_items
@more_menu_items ||= [more_menu_detach_project].compact
end
private
def more_menu_detach_project
{
scheme: :default,
icon: nil,
label: I18n.t("projects.settings.project_custom_fields.actions.remove_from_project"),
href: detach_from_project_url,
data: { turbo_method: :delete }
}
end
def detach_from_project_url
url_helpers.admin_cost_type_project_path(
cost_type_id: @table.params[:cost_type].id,
cost_types_project: { project_id: project.id },
page: current_page
)
end
end
end
end
end
@@ -0,0 +1,51 @@
# 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 Admin
module CostTypes
module CostTypeProjects
class TableComponent < ::Projects::TableComponent
include ::Projects::Concerns::TableComponent::StreamablePaginationLinksConstraints
def columns
@columns ||= query.selects.grep_v(Queries::Selects::NotExistingSelect)
end
def sortable?
false
end
def use_quick_action_table_headers?
false
end
end
end
end
end
@@ -0,0 +1,36 @@
<%#-- 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.
++#%>
<%=
render(Primer::OpenProject::PageHeader.new(test_selector: "cost-types--page-header")) do |header|
header.with_title { page_title }
header.with_breadcrumbs(breadcrumbs_items, selected_item_font_weight: :normal)
helpers.render_tab_header_nav(header, tabs, test_selector: :cost_type_detail_header)
end
%>
@@ -0,0 +1,77 @@
# 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 Admin
module CostTypes
class EditFormHeaderComponent < ApplicationComponent
def initialize(cost_type:, selected:, **)
@cost_type = cost_type
@selected = selected
super(cost_type, **)
end
def tabs
return [] unless @cost_type.persisted?
[
{
name: "edit",
path: edit_admin_cost_type_path(@cost_type),
label: t(:label_details)
},
{
name: "rates",
path: rates_admin_cost_type_path(@cost_type),
label: t("cost_types.admin.rates.title")
},
{
name: "cost_type_projects",
path: admin_cost_type_projects_path(@cost_type),
label: t(:label_project_plural)
}
]
end
private
def page_title
@cost_type.persisted? ? @cost_type.name : "#{t(:label_new)} #{::CostType.model_name.human}"
end
def breadcrumbs_items
[
{ href: admin_index_path, text: t(:label_administration) },
{ href: admin_cost_types_path, text: t(:label_cost_type_plural) },
page_title
]
end
end
end
end
@@ -0,0 +1,42 @@
<%#-- 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.
++#%>
<%=
render(Primer::OpenProject::PageHeader.new(test_selector: "time-and-costs--page-header")) do |header|
header.with_title { page_title }
header.with_breadcrumbs(breadcrumbs_items, selected_item_font_weight: :normal)
header.with_tab_nav(label: I18n.t("cost_types.settings.time_and_costs"), test_selector: :time_and_costs_tabs) do |tab_nav|
tabs.each do |tab|
tab_nav.with_tab(selected: tab[:name] == @selected, href: tab[:path]) do |t|
t.with_text { tab[:label] }
end
end
end
end
%>
@@ -0,0 +1,70 @@
# 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 Projects
module Settings
module TimeAndCosts
class PageHeaderComponent < ApplicationComponent
def initialize(project:, selected:, **)
@project = project
@selected = selected
super(project, **)
end
def tabs
[
{
name: :time_entry_activities,
path: helpers.project_settings_time_entry_activities_path(@project),
label: I18n.t(:enumeration_activities)
},
{
name: :cost_types,
path: helpers.project_settings_cost_types_path(@project),
label: I18n.t("cost_types.settings.cost_types.heading")
}
]
end
def page_title
I18n.t("cost_types.settings.time_and_costs")
end
def breadcrumbs_items
[
{ href: helpers.project_overview_path(@project.id), text: @project.name },
{ href: helpers.project_settings_general_path(@project.id), text: I18n.t(:label_project_settings) },
page_title
]
end
end
end
end
end
@@ -0,0 +1,72 @@
# 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 CostTypes
module CostTypeProjects
class BaseContract < ::ModelContract
include UnchangedProject
MANAGE_PERMISSION = :manage_project_activities
attribute :project_id
attribute :cost_type_id
validate :validate_manage_allowed_in_source_project
validate :validate_manage_allowed_in_destination_project
validate :not_for_all
def validate_manage_allowed_in_source_project
if model.new_record?
errors.add :base, :error_unauthorized unless user.allowed_in_project?(MANAGE_PERMISSION, model.project)
return
end
with_unchanged_project_id do
errors.add :base, :error_unauthorized unless user.allowed_in_project?(MANAGE_PERMISSION, model.project)
end
end
def validate_manage_allowed_in_destination_project
return if model.new_record?
return unless model.project_id_changed?
unless user.allowed_in_project?(MANAGE_PERMISSION, model.project)
errors.add :base, :error_unauthorized
end
end
def not_for_all
return if model.cost_type.nil? || !model.cost_type.is_for_all?
errors.add :cost_type_id, :is_for_all_cannot_modify
end
end
end
end
@@ -0,0 +1,36 @@
# 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 CostTypes
module CostTypeProjects
class UpdateContract < BaseContract
end
end
end
@@ -0,0 +1,157 @@
# 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 Admin::CostTypes::CostTypeProjectsController < ApplicationController
include OpTurbo::ComponentStream
include FlashMessagesOutputSafetyHelper
layout "admin"
before_action :require_admin
before_action :find_cost_type
before_action :available_cost_types_projects_query, only: %i[index destroy]
before_action :initialize_cost_type_project, only: :new
before_action :find_projects_to_activate_for_cost_type, only: :create
before_action :find_cost_type_project_to_destroy, only: :destroy
menu_item :cost_types
def index; end
def new
respond_with_dialog Admin::CostTypes::CostTypeProjects::NewCostTypeProjectsModalComponent.new(
cost_type_project_mapping: @cost_type_project,
cost_type: @cost_type
)
end
def create
create_service = ::CostTypes::CostTypeProjects::BulkCreateService
.new(user: current_user, projects: @projects, model: @cost_type,
include_sub_projects: include_sub_projects?)
.call
create_service.on_success { render_project_list(url_for_action: :index) }
create_service.on_failure do
render_error_flash_message_via_turbo_stream(
message: join_flash_messages(create_service.errors)
)
end
respond_to_with_turbo_streams(status: create_service.success? ? :ok : :unprocessable_entity)
end
def destroy
delete_service = ::CostTypes::CostTypeProjects::DeleteService
.new(user: current_user, model: @cost_type_project)
.call
delete_service.on_success { render_project_list(url_for_action: :index) }
delete_service.on_failure do
render_error_flash_message_via_turbo_stream(
message: join_flash_messages(delete_service.errors.full_messages)
)
end
respond_to_with_turbo_streams(status: delete_service.success? ? :ok : :unprocessable_entity)
end
private
def render_project_list(url_for_action: action_name)
update_via_turbo_stream(
component: Admin::CostTypes::CostTypeProjects::TableComponent.new(
query: available_cost_types_projects_query,
params: params.merge({ cost_type: @cost_type, url_for_action: })
)
)
end
def find_cost_type
@cost_type = CostType.find(params.expect(:cost_type_id))
end
def find_projects_to_activate_for_cost_type
if (project_ids = params.to_unsafe_h.dig(:cost_types_project, :project_ids)).present?
@projects = Project.visible.find(project_ids)
else
initialize_cost_type_project
@cost_type_project.errors.add(:project_ids, :blank)
update_via_turbo_stream(
component: Admin::CostTypes::CostTypeProjects::NewCostTypeProjectsFormModalComponent.new(
cost_type_project_mapping: @cost_type_project,
cost_type: @cost_type
),
status: :bad_request
)
respond_with_turbo_streams
end
rescue ActiveRecord::RecordNotFound
respond_with_project_not_found_turbo_streams
end
def find_cost_type_project_to_destroy
project_id = params.expect(cost_types_project: [:project_id]).fetch(:project_id)
@cost_type_project = CostTypesProject.find_by!(cost_type: @cost_type, project: project_id)
rescue ActiveRecord::RecordNotFound
respond_with_project_not_found_turbo_streams
end
def available_cost_types_projects_query
@available_cost_types_projects_query = ProjectQuery.new(
name: "cost-types-projects-#{@cost_type.id}"
) do |query|
query.where(:available_cost_types_projects, "=", [@cost_type.id])
query.select(:name)
query.order("lft" => "asc")
end
end
def initialize_cost_type_project
@cost_type_project = ::CostTypes::CostTypeProjects::SetAttributesService
.new(user: current_user, model: CostTypesProject.new, contract_class: EmptyContract)
.call(cost_type: @cost_type)
.result
end
def respond_with_project_not_found_turbo_streams
render_error_flash_message_via_turbo_stream message: t(:notice_project_not_found)
render_project_list(url_for_action: :index)
respond_with_turbo_streams
end
def include_sub_projects?
ActiveRecord::Type::Boolean.new.cast(params.to_unsafe_h.dig(:cost_types_project, :include_sub_projects))
end
end
@@ -38,6 +38,7 @@ module Admin
helper :sort
include SortHelper
helper :cost_types
include CostTypesHelper
@@ -65,24 +66,6 @@ module Admin
render action: "index", layout: !request.xhr?
end
def edit
render action: :edit, layout: !request.xhr?
end
def update
@cost_type.attributes = permitted_params.cost_type
if @cost_type.save
flash[:notice] = t(:notice_successful_update)
redirect_back_or_default({ action: "index" })
else
render action: :edit, status: :unprocessable_entity, layout: !request.xhr?
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = t(:notice_locking_conflict)
end
def new
@cost_type = CostType.new
@@ -91,6 +74,10 @@ module Admin
render action: :edit, layout: !request.xhr?
end
def edit
render action: :edit, layout: !request.xhr?
end
def create # rubocop:disable Metrics/AbcSize
@cost_type = CostType.new(permitted_params.cost_type)
@@ -106,6 +93,20 @@ module Admin
flash.now[:error] = t(:notice_locking_conflict)
end
def update # rubocop:disable Metrics/AbcSize
@cost_type.attributes = permitted_params.cost_type
if @cost_type.save
flash[:notice] = t(:notice_successful_update)
redirect_to edit_admin_cost_type_path(@cost_type)
else
render action: :edit, status: :unprocessable_entity, layout: !request.xhr?
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = t(:notice_locking_conflict)
end
def destroy
@cost_type.deleted_at = DateTime.now
@cost_type.default = false
@@ -37,6 +37,12 @@ class CostlogController < ApplicationController
include CostlogHelper
def new
unless @project&.cost_types_available?
flash[:error] = I18n.t("cost_types.errors.no_cost_types_available") # rubocop:disable Rails/ActionControllerFlashBeforeRender
redirect_back_or_default(@work_package ? polymorphic_path(@work_package) : project_path(@project))
return
end
new_default_cost_entry
render action: "edit"
@@ -140,7 +146,7 @@ class CostlogController < ApplicationController
ce.entity = @work_package
ce.user = User.current
ce.spent_on = Time.zone.today
# notice that cost_type is set to default cost_type in the model
ce.cost_type = CostType.default_for_project(@project) if @project
end
end
@@ -0,0 +1,68 @@
# 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 Projects::Settings::CostTypesController < Projects::SettingsController
menu_item :settings_time_and_costs
before_action :find_cost_type, only: :toggle
def index
@cost_types = CostType.active.order(:name)
end
def toggle
if @cost_type.is_for_all?
respond_redirect(error: I18n.t("activerecord.errors.messages.is_for_all_cannot_modify"))
return
end
mapping = CostTypesProject.find_or_initialize_by(project_id: @project.id, cost_type_id: @cost_type.id)
if mapping.persisted?
mapping.destroy!
else
mapping.save!
end
respond_redirect
end
private
def find_cost_type
@cost_type = CostType.active.find(params.expect(:id))
end
def respond_redirect(error: nil)
flash[:error] = error if error
flash[:notice] = I18n.t(:notice_successful_update) unless error
redirect_to project_settings_cost_types_path(@project)
end
end
@@ -29,7 +29,7 @@
#++
class Projects::Settings::TimeEntryActivitiesController < Projects::SettingsController
menu_item :settings_time_entry_activities
menu_item :settings_time_and_costs
def update
TimeEntryActivitiesProject.upsert_all(update_params, unique_by: %i[project_id activity_id])
@@ -0,0 +1,85 @@
# 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 CostTypes
module CostTypeProjects
class CostTypeMappingForm < ApplicationForm
include OpPrimer::ComponentHelpers
form do |form|
form.project_autocompleter(
name: :id,
label: Project.model_name.human,
visually_hide_label: true,
validation_message: project_ids_error_message,
autocomplete_options: {
appendTo: "##{Admin::CostTypes::CostTypeProjects::NewCostTypeProjectsFormModalComponent::DIALOG_ID}",
with_search_icon: true,
openDirectly: false,
focusDirectly: false,
multiple: true,
dropdownPosition: "bottom",
disabledProjects: projects_with_cost_type_mapping,
inputName: "cost_types_project[project_ids]"
}
)
form.check_box(
name: :include_sub_projects,
label: I18n.t(:label_include_sub_projects),
checked: false,
label_arguments: { class: "no-wrap" }
)
end
def initialize(project_mapping:)
super()
@project_mapping = project_mapping
end
private
def project_ids_error_message
@project_mapping
.errors
.messages_for(:project_ids)
.to_sentence
.presence
end
def projects_with_cost_type_mapping
CostTypesProject
.where(cost_type_id: @project_mapping.cost_type_id)
.pluck(:project_id)
.to_h { |id| [id, id] }
end
end
end
end
+15
View File
@@ -30,6 +30,8 @@ class CostType < ApplicationRecord
has_many :material_budget_items
has_many :cost_entries, dependent: :destroy
has_many :rates, class_name: "CostRate", dependent: :destroy
has_many :cost_types_projects, dependent: :destroy
has_many :projects, through: :cost_types_projects
validates :unit, :unit_plural, presence: true
validates :name, presence: true, uniqueness: { case_sensitive: false }
@@ -39,12 +41,25 @@ class CostType < ApplicationRecord
include ActiveModel::ForbiddenAttributesProtection
scope :active, -> { where(deleted_at: nil) }
scope :for_all, -> { where(is_for_all: true) }
scope :available_for_project, ->(project) {
project_id = project.is_a?(Project) ? project.id : project
where(is_for_all: true)
.or(where(id: CostTypesProject.where(project_id:).select(:cost_type_id)))
}
# finds the default CostType
def self.default
CostType.find_by(default: true) || CostType.first
end
# Returns the default cost type for the given project, falling back to the first
# cost type available in that project when the global default is not available there.
def self.default_for_project(project)
available = available_for_project(project).active
available.find_by(default: true) || available.first
end
def is_default?
default
end
@@ -0,0 +1,36 @@
# 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.
#++
# Join table for cost types on projects, used when CostType#is_for_all is not set
# to find which cost types are activated.
class CostTypesProject < ApplicationRecord
belongs_to :cost_type
belongs_to :project
end
@@ -0,0 +1,52 @@
# 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 Projects::Scopes
module AvailableCostTypes
extend ActiveSupport::Concern
class_methods do
def with_available_cost_types(cost_type_ids)
where(id: cost_types_projects_subquery(cost_type_ids:))
end
def without_available_cost_types(cost_type_ids)
where.not(id: cost_types_projects_subquery(cost_type_ids:))
end
private
def cost_types_projects_subquery(cost_type_ids:)
CostTypesProject.select(:project_id)
.where(cost_type_id: cost_type_ids)
end
end
end
end
@@ -0,0 +1,66 @@
# 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::Filters::AvailableCostTypesProjectsFilter < Queries::Projects::Filters::Base
def self.key
:available_cost_types_projects
end
def type
:list
end
def allowed_values
@allowed_values ||= CostType.where(is_for_all: false).pluck(:name, :id)
end
def available?
User.current.admin?
end
def apply_to(_query_scope)
case operator
when "="
super.with_available_cost_types(values)
when "!"
super.without_available_cost_types(values)
else
raise "unsupported operator"
end
end
def where
nil
end
def human_name
I18n.t(:label_available_cost_types_projects)
end
end
@@ -0,0 +1,49 @@
# 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 CostTypes
module CostTypeProjects
class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService
def initialize(user:, projects:, model:, include_sub_projects: false)
mapping_context = ::BulkServices::ProjectMappings::MappingContext.new(
mapping_model_class: CostTypesProject,
model:,
projects:,
model_foreign_key_id:,
include_sub_projects:
)
super(user:, mapping_context:)
end
def permission = :manage_project_activities
def model_foreign_key_id = :cost_type_id
end
end
end
@@ -0,0 +1,37 @@
# 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 CostTypes
module CostTypeProjects
class DeleteService < ::BaseServices::Delete
def default_contract_class = CostTypes::CostTypeProjects::UpdateContract
end
end
end
@@ -0,0 +1,36 @@
# 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 CostTypes
module CostTypeProjects
class SetAttributesService < ::BaseServices::SetAttributes
end
end
end
@@ -0,0 +1,70 @@
<%#-- 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.
++#%>
<% html_title t(:label_administration), "#{CostType.model_name.human} #{h @cost_type.name}", t(:label_project_plural) %>
<%=
render(
Admin::CostTypes::EditFormHeaderComponent.new(
cost_type: @cost_type,
selected: :cost_type_projects
)
)
%>
<%=
unless @cost_type.is_for_all?
render(Primer::OpenProject::SubHeader.new(test_selector: "add-projects-sub-header")) do |component|
component.with_action_button(scheme: :primary,
leading_icon: :"op-include-projects",
label: I18n.t(:label_add_projects),
tag: :a,
href: new_admin_cost_type_project_path(@cost_type),
data: { controller: "async-dialog" }) do
I18n.t(:label_add_projects)
end
end
end
%>
<%=
if @cost_type.is_for_all?
render Primer::Beta::Blankslate.new(border: true) do |component|
component.with_visual_icon(icon: :checklist)
component.with_heading(tag: :h2).with_content(I18n.t("cost_types.admin.cost_type_projects.is_for_all_blank_slate.heading"))
component.with_description { I18n.t("cost_types.admin.cost_type_projects.is_for_all_blank_slate.description") }
end
else
render(
Admin::CostTypes::CostTypeProjects::TableComponent.new(
query: @available_cost_types_projects_query,
params: params.merge({ cost_type: @cost_type })
)
)
end
%>
@@ -36,17 +36,12 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { title }
header.with_breadcrumbs(
[
{ href: admin_index_path, text: t("label_administration") },
{ href: admin_time_settings_path, text: t(:project_module_costs) },
{ href: admin_cost_types_path, text: t(:label_cost_type_plural) },
title
]
render(
Admin::CostTypes::EditFormHeaderComponent.new(
cost_type: @cost_type,
selected: :edit
)
end
)
%>
<%= labelled_tabular_form_for @cost_type,
@@ -67,6 +62,9 @@ See COPYRIGHT and LICENSE files for more details.
<div class="form--field">
<%= f.check_box :default %>
</div>
<div class="form--field">
<%= f.check_box :is_for_all %>
</div>
<h3><%= t :caption_rate_history %></h3>
<div class="generic-table--container">
@@ -0,0 +1,77 @@
<%#-- 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.
++#%>
<%=
render(
Projects::Settings::TimeAndCosts::PageHeaderComponent.new(
project: @project,
selected: :cost_types
)
)
%>
<% enabled_ids = CostTypesProject.where(project_id: @project.id).pluck(:cost_type_id).to_set %>
<% if @cost_types.empty? %>
<%=
render Primer::Beta::Blankslate.new(border: true) do |c|
c.with_visual_icon(icon: :checklist)
c.with_heading(tag: :h2).with_content(I18n.t("cost_types.settings.cost_types.heading"))
c.with_description { I18n.t("cost_types.admin.cost_type_projects.no_projects.description") }
end
%>
<% else %>
<table class="generic-table">
<thead>
<tr>
<th><%= CostType.model_name.human %></th>
<th></th>
</tr>
</thead>
<tbody>
<% @cost_types.each do |cost_type| %>
<tr>
<td><%= h cost_type.name %></td>
<td>
<% if cost_type.is_for_all? %>
<em><%= t(:label_for_all) rescue "For all projects" %></em>
<% else %>
<%= button_to(
enabled_ids.include?(cost_type.id) ? t(:button_disable) : t(:button_enable),
toggle_project_settings_cost_type_path(@project, cost_type),
method: :put,
class: "button",
form: { data: { turbo: false } }
) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
@@ -28,14 +28,12 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t(:enumeration_activities) }
header.with_breadcrumbs(
[{ href: project_overview_path(@project.id), text: @project.name },
{ href: project_settings_general_path(@project.id), text: I18n.t(:label_project_settings) },
t(:enumeration_activities)]
render(
Projects::Settings::TimeAndCosts::PageHeaderComponent.new(
project: @project,
selected: :time_entry_activities
)
end
)
%>
<% if TimeEntryActivity.any? %>
+20
View File
@@ -47,6 +47,8 @@ en:
unit: "Unit name"
unit_plural: "Pluralized unit name"
default: "Cost type by default"
is_for_all: "For all projects"
rates: "Rates"
work_package:
costs_by_type: "Spent units"
labor_costs: "Labor costs"
@@ -252,6 +254,24 @@ en:
validation:
start_time_different_date: "Date part of startTime (%{start_time}) must be the same as the spentOn (%{spent_on}) date."
label_available_cost_types_projects: "Available cost types projects"
cost_types:
errors:
no_cost_types_available: "No cost types are available in this project. Please contact an administrator."
admin:
cost_type_projects:
is_for_all_blank_slate:
heading: "This cost type is enabled in all projects"
description: "Uncheck \"For all projects\" on the details tab to limit this cost type to specific projects."
no_projects:
heading: "No projects assigned"
description: "Add projects so this cost type can be used in them."
settings:
time_and_costs: "Time & Costs"
cost_types:
heading: "Cost types"
none_active: "No cost types are currently active in this project."
costs:
widgets:
actual_costs:
+8
View File
@@ -62,6 +62,9 @@ Rails.application.routes.draw do
scope "projects/:project_id", as: "project", module: "projects" do
namespace "settings" do
resource :time_entry_activities, only: %i[show update]
resources :cost_types, only: %i[index] do
member { put :toggle }
end
end
end
@@ -93,6 +96,11 @@ Rails.application.routes.draw do
put :set_rate
patch :restore
end
scope module: :cost_types do
resources :projects, controller: :cost_type_projects, only: %i[index new create]
resource :project, controller: :cost_type_projects, only: :destroy
end
end
resource :costs,
+9 -1
View File
@@ -67,7 +67,10 @@ module Costs
require: :member
permission :manage_project_activities,
{ "projects/settings/time_entry_activities": %i[show update] },
{
"projects/settings/time_entry_activities": %i[show update],
"projects/settings/cost_types": %i[index toggle]
},
permissible_on: :project,
require: :member
@@ -222,6 +225,7 @@ module Costs
current_user.allowed_in_project?(:log_own_costs, represented.project)
} do
next unless represented.costs_enabled? && represented.persisted?
next unless represented.project&.cost_types_available?
{
href: new_work_packages_cost_entry_path(represented),
@@ -349,6 +353,10 @@ module Costs
::Queries::Register.register(::Query) do
select Costs::QueryCurrencySelect
end
::Queries::Register.register(::ProjectQuery) do
filter ::Queries::Projects::Filters::AvailableCostTypesProjectsFilter
end
end
end
end
@@ -56,6 +56,7 @@ module Costs::Patches::PermittedParamsPatch
:unit,
:unit_plural,
:default,
:is_for_all,
{ new_rate_attributes: %i[valid_from rate] },
existing_rate_attributes: %i[valid_from rate])
end
@@ -49,5 +49,9 @@ module Costs::Patches::ProjectPatch
def costs_enabled?
module_enabled?(:costs)
end
def cost_types_available?
CostType.available_for_project(self).active.exists?
end
end
end
@@ -132,7 +132,10 @@ RSpec.describe CostlogController do
end
describe "WHEN user allowed to create new cost_entry" do
let(:expected_cost_type) { cost_type }
before do
cost_type.save!
grant_current_user_permissions user, %i[view_project view_work_packages log_costs]
end
@@ -154,7 +157,10 @@ RSpec.describe CostlogController do
end
describe "WHEN user is allowed to create new own cost_entry" do
let(:expected_cost_type) { cost_type }
before do
cost_type.save!
grant_current_user_permissions user, %i[view_project view_work_packages log_own_costs]
end
@@ -172,6 +178,53 @@ RSpec.describe CostlogController do
describe "WHEN user is not a project member" do
it_behaves_like "not_found new"
end
describe "WHEN no cost type is available in the project" do
let(:scoped_cost_type) { create(:cost_type, is_for_all: false) }
before do
CostType.destroy_all
scoped_cost_type # only project-scoped cost type, not mapped to this project
grant_current_user_permissions user, %i[view_project view_work_packages log_costs]
get :new, params:
end
it "redirects with an error flash explaining no cost types are available" do
expect(response).to be_redirect
expect(flash[:error]).to eq(I18n.t("cost_types.errors.no_cost_types_available"))
end
end
describe "WHEN the project's default cost type is global" do
let(:expected_cost_type) { cost_type }
before do
CostType.destroy_all
cost_type.is_for_all = true
cost_type.default = true
cost_type.save!
grant_current_user_permissions user, %i[view_project view_work_packages log_costs]
end
it_behaves_like "successful new"
end
describe "WHEN the global default cost type is unavailable in the project, " \
"but another non-global cost type is enabled" do
let(:scoped_cost_type) { create(:cost_type, is_for_all: false) }
let(:global_default) { create(:cost_type, is_for_all: false, default: true) }
let(:expected_cost_type) { scoped_cost_type }
before do
CostType.destroy_all
global_default
scoped_cost_type
CostTypesProject.create!(project:, cost_type: scoped_cost_type)
grant_current_user_permissions user, %i[view_project view_work_packages log_costs]
end
it_behaves_like "successful new"
end
end
describe "GET edit" do
@@ -3,6 +3,7 @@ require "spec_helper"
RSpec.describe "Work package table log unit costs", :js do
let(:user) { create(:admin) }
let(:work_package) { create(:work_package) }
let!(:cost_type) { create(:cost_type, is_for_all: true) }
let(:wp_table) { Pages::WorkPackagesTable.new }
let(:menu) { Components::WorkPackages::ContextMenu.new }
@@ -62,7 +62,7 @@ RSpec.describe "Time entry activity", :js do
visit project_settings_general_path(project)
click_on "Time tracking activities"
click_on "Time & Costs"
expect(page).to have_field("Development", checked: true)
@@ -73,6 +73,11 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do
embed_links: true)
end
# Create a cost type that enables the log unit cost paths
let!(:cost_type) do
create(:cost_type, is_for_all: true)
end
before do
allow(User).to receive(:current).and_return user
end
@@ -301,6 +306,38 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do
end
end
end
describe "logCosts gating by available cost types" do
let(:additional_permissions) { %i[log_costs view_time_entries view_cost_entries view_cost_rates] }
before { CostType.destroy_all }
context "when at least one cost type is available in the project" do
before { create(:cost_type, is_for_all: true) }
it "has the logCosts link" do
expect(subject).to have_json_path("_links/logCosts/href")
end
end
context "when no cost type is available in the project" do
before { create(:cost_type, is_for_all: false) }
it "omits the logCosts link" do
expect(subject).not_to have_json_path("_links/logCosts/href")
end
end
context "when a non-global cost type is explicitly enabled in the project" do
let!(:scoped_cost_type) { create(:cost_type, is_for_all: false) }
before { CostTypesProject.create!(project:, cost_type: scoped_cost_type) }
it "has the logCosts link" do
expect(subject).to have_json_path("_links/logCosts/href")
end
end
end
end
describe "costs module disabled" do
+60 -16
View File
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -29,30 +31,21 @@
require_relative "../spec_helper"
RSpec.describe CostType do
let(:klass) { CostType }
let(:cost_type) do
klass.new name: "ct1",
unit: "singular",
unit_plural: "plural"
end
before do
# as the spec_helper loads fixtures and they are probably needed by other tests
# we delete them here so they do not interfere.
# on the long run, fixtures should be removed
CostType.destroy_all
described_class.new name: "ct1",
unit: "singular",
unit_plural: "plural"
end
describe "class" do
describe "active" do
describe "WHEN a CostType instance is deleted" do
before do
cost_type.deleted_at = Time.now
cost_type.deleted_at = Time.zone.now
cost_type.save!
end
it { expect(klass.active.size).to eq(0) }
it { expect(described_class.active.size).to eq(0) }
end
describe "WHEN a CostType instance is not deleted" do
@@ -60,8 +53,59 @@ RSpec.describe CostType do
cost_type.save!
end
it { expect(klass.active.size).to eq(1) }
it { expect(klass.active[0]).to eq(cost_type) }
it { expect(described_class.active.size).to eq(1) }
it { expect(described_class.active[0]).to eq(cost_type) }
end
end
end
describe ".available_for_project" do
let(:project) { create(:project) }
let(:other_project) { create(:project) }
let!(:global_ct) { create(:cost_type, is_for_all: true) }
let!(:scoped_ct) { create(:cost_type, is_for_all: false) }
let!(:unrelated_ct) { create(:cost_type, is_for_all: false) }
before do
CostTypesProject.create!(cost_type: scoped_ct, project: project)
CostTypesProject.create!(cost_type: unrelated_ct, project: other_project)
end
it "returns global cost types plus those explicitly mapped to the project" do
expect(described_class.available_for_project(project)).to contain_exactly(global_ct, scoped_ct)
end
it "accepts a project_id integer too" do
expect(described_class.available_for_project(project.id)).to contain_exactly(global_ct, scoped_ct)
end
end
describe ".default_for_project" do
let(:project) { create(:project) }
context "when the global default is available in the project" do
let!(:default_ct) { create(:cost_type, is_for_all: true, default: true) }
let!(:_other) { create(:cost_type, is_for_all: true) }
it "returns the default" do
expect(described_class.default_for_project(project)).to eq(default_ct)
end
end
context "when no default cost type is available in the project" do
let!(:default_ct) { create(:cost_type, for_all_projects: false, default: true) }
let!(:available) { create(:cost_type, for_all_projects: true) }
it "falls back to the first available cost type" do
expect(described_class.default_for_project(project)).to eq(available)
end
end
context "when no cost type is available in the project" do
let!(:_unrelated) { create(:cost_type, is_for_all: false) }
it "returns nil" do
expect(described_class.default_for_project(project)).to be_nil
end
end
end
@@ -0,0 +1,58 @@
# 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 CostTypesProject do
let(:project) { create(:project) }
let(:cost_type) { create(:cost_type, is_for_all: false) }
it "creates a mapping with both belongs_to associations" do
mapping = described_class.create!(project:, cost_type:)
expect(mapping.project).to eq(project)
expect(mapping.cost_type).to eq(cost_type)
end
it "is destroyed when the cost type is destroyed" do
described_class.create!(project:, cost_type:)
expect { cost_type.destroy }.to change(described_class, :count).by(-1)
end
it "is deleted when the project is destroyed" do
described_class.create!(project:, cost_type:)
expect { project.destroy }.to change(described_class, :count).by(-1)
end
it "enforces uniqueness of (project_id, cost_type_id) at the DB level" do
described_class.create!(project:, cost_type:)
expect { described_class.create!(project:, cost_type:) }
.to raise_error(ActiveRecord::RecordNotUnique)
end
end
@@ -0,0 +1,58 @@
# 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 Project, "#cost_types_available?" do
let(:project) { create(:project) }
before { CostType.destroy_all }
it "is true when at least one cost type is for all projects" do
create(:cost_type, is_for_all: true)
expect(project.cost_types_available?).to be true
end
it "is true when a cost type is explicitly enabled in the project" do
cost_type = create(:cost_type, is_for_all: false)
CostTypesProject.create!(project:, cost_type:)
expect(project.cost_types_available?).to be true
end
it "is false when there are no cost types enabled in the project" do
create(:cost_type, is_for_all: false)
expect(project.cost_types_available?).to be false
end
it "ignores soft-deleted cost types" do
create(:cost_type, :deleted, is_for_all: true)
expect(project.cost_types_available?).to be false
end
end
@@ -0,0 +1,94 @@
# 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 Queries::Projects::Filters::AvailableCostTypesProjectsFilter do
let(:instance) { described_class.create!(name: described_class.key) }
describe ".key" do
it { expect(described_class.key).to eq(:available_cost_types_projects) }
end
describe "#allowed_values" do
before { CostType.destroy_all }
let!(:scoped_a) { create(:cost_type, is_for_all: false, name: "Disk") }
let!(:scoped_b) { create(:cost_type, is_for_all: false, name: "License") }
let!(:_global) { create(:cost_type, is_for_all: true, name: "Travel") }
it "lists only non-global cost types (so filter validates even before any mapping exists)" do
expect(instance.allowed_values).to contain_exactly(
["Disk", scoped_a.id],
["License", scoped_b.id]
)
end
end
describe "filter is registered with ProjectQuery" do
it "is in the ProjectQuery filter registry" do
expect(Queries::Register.filters[ProjectQuery]).to include(described_class)
end
end
describe "applying the filter via ProjectQuery" do
let(:admin) { create(:admin) }
let!(:cost_type) { create(:cost_type, is_for_all: false) }
let!(:mapped_project) { create(:project) }
let!(:unmapped_project) { create(:project) }
before do
login_as(admin)
CostTypesProject.create!(cost_type:, project: mapped_project)
end
it "returns only projects mapped to the given cost type" do
query = ProjectQuery.new(name: "t") do |q|
q.where(:available_cost_types_projects, "=", [cost_type.id])
q.select(:name)
end
expect(query).to be_valid
expect(query.results.pluck(:id)).to contain_exactly(mapped_project.id)
end
it "returns no projects when the cost type has no mappings (and query stays valid)" do
cost_type_without_mappings = create(:cost_type, is_for_all: false)
query = ProjectQuery.new(name: "t2") do |q|
q.where(:available_cost_types_projects, "=", [cost_type_without_mappings.id])
q.select(:name)
end
expect(query).to be_valid
expect(query.results).to be_empty
end
end
end
@@ -0,0 +1,89 @@
# 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 Projects::Settings::CostTypesController, :skip_csrf, type: :rails_request do
shared_let(:project) { create(:project, enabled_module_names: %i[costs]) }
let(:permissions) { %i[manage_project_activities] }
let(:user) { create(:user, member_with_permissions: { project => permissions }) }
let!(:global_ct) { create(:cost_type, is_for_all: true) }
let!(:scoped_ct) { create(:cost_type, is_for_all: false) }
before { login_as(user) }
describe "GET #index" do
it "renders the list of cost types" do
get project_settings_cost_types_path(project)
expect(response).to have_http_status(:ok)
end
end
describe "PUT #toggle" do
context "with a non-global cost type not yet enabled in the project" do
it "creates the mapping" do
expect do
put toggle_project_settings_cost_type_path(project, scoped_ct)
end.to change { CostTypesProject.where(project:, cost_type: scoped_ct).count }.from(0).to(1)
expect(response).to redirect_to(project_settings_cost_types_path(project))
end
end
context "with a non-global cost type already enabled" do
before { CostTypesProject.create!(project:, cost_type: scoped_ct) }
it "removes the mapping" do
expect do
put toggle_project_settings_cost_type_path(project, scoped_ct)
end.to change { CostTypesProject.where(project:, cost_type: scoped_ct).count }.from(1).to(0)
end
end
context "with a global cost type" do
it "rejects toggling" do
expect do
put toggle_project_settings_cost_type_path(project, global_ct)
end.not_to change(CostTypesProject, :count)
expect(flash[:error]).to be_present
end
end
end
context "when user lacks :manage_project_activities" do
let(:permissions) { %i[view_work_packages] }
it "blocks index" do
get project_settings_cost_types_path(project)
expect(response).to have_http_status(:forbidden)
end
end
end
@@ -0,0 +1,43 @@
# 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"
require Rails.root.join("spec/services/bulk_services/project_mappings/behaves_like_bulk_project_mapping_create_service")
RSpec.describe CostTypes::CostTypeProjects::BulkCreateService do
shared_let(:cost_type) { create(:cost_type, is_for_all: false) }
it_behaves_like "BulkServices project mappings create service" do
let(:model) { cost_type }
let(:model_mapping_class) { CostTypesProject }
let(:model_foreign_key_id) { :cost_type_id }
let(:required_permission) { :manage_project_activities }
end
end
@@ -0,0 +1,72 @@
# 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 CostTypes::CostTypeProjects::DeleteService do
shared_let(:project) { create(:project) }
shared_let(:cost_type) { create(:cost_type, is_for_all: false) }
let!(:mapping) { CostTypesProject.create!(project:, cost_type:) }
context "with an admin user" do
let(:user) { create(:admin) }
it "removes the mapping" do
expect { described_class.new(user:, model: mapping).call }
.to change(CostTypesProject, :count).by(-1)
end
end
context "with a user lacking permissions" do
let(:user) { create(:user, member_with_permissions: { project => %i[view_work_packages] }) }
it "does not remove the mapping" do
result = described_class.new(user:, model: mapping).call
expect(result).to be_failure
expect(CostTypesProject.where(id: mapping.id)).to exist
end
end
context "when the cost type is is_for_all" do
let(:user) { create(:admin) }
before { cost_type.update!(is_for_all: true) }
it "refuses to delete the mapping" do
result = described_class.new(user:, model: mapping).call
expect(result).to be_failure
expect(result.errors.symbols_for(:cost_type_id)).to include(:is_for_all_cannot_modify)
expect(CostTypesProject.where(id: mapping.id)).to exist
end
end
end