#-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. #++ class BudgetsController < ApplicationController include AttachableServiceCall 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, only: %i[new create update_material_budget_item update_labor_budget_item] before_action :find_optional_project, only: :index before_action :authorize_global, only: :index before_action :authorize, except: [ # unrestricted actions :index, :update_material_budget_item, :update_labor_budget_item ] helper :sort include SortHelper helper :projects include ProjectsHelper helper :attachments include AttachmentsHelper helper :costlog include CostlogHelper helper :budgets include BudgetsHelper include PaginationHelper include ::Costs::NumberHelper def index sort_init 'id', 'desc' sort_update default_budget_sort @budgets = visible_sorted_budgets respond_to do |format| format.html do render action: 'index', layout: !request.xhr? end format.csv { send_data(budgets_to_csv(@budgets), type: 'text/csv; header=present', filename: 'export.csv') } end end def show @edit_allowed = User.current.allowed_to?(:edit_budgets, @project) respond_to do |format| format.html { render action: 'show', layout: !request.xhr? } end end def new @budget ||= Budget.new @budget.project_id = @project.id @budget.fixed_date ||= Date.today render layout: !request.xhr? end def copy source = Budget.find(params[:id].to_i) @budget = if source Budget.new_copy(source) else Budget.new end @budget.fixed_date ||= Date.today render action: :new, layout: !request.xhr? end def create call = attachable_create_call ::Budgets::CreateService, args: permitted_params.budget.merge(project: @project) @budget = call.result if call.success? flash[:notice] = t(:notice_successful_create) redirect_to(params[:continue] ? { action: 'new' } : { action: 'show', id: @budget }) else @errors = call.errors render action: 'new', layout: !request.xhr? end end def edit @budget.attributes = permitted_params.budget if params[:budget] end def update call = attachable_update_call ::Budgets::UpdateService, model: @budget, args: permitted_params.budget if call.success? flash[:notice] = t(:notice_successful_update) redirect_to(params[:back_to] || { action: 'show', id: @budget }) else @budget = call.result @errors = call.errors render action: 'edit' end rescue ActiveRecord::StaleObjectError # Optimistic locking exception flash.now[:error] = t(:notice_locking_conflict) end def destroy @budgets.each(&:destroy) flash[:notice] = t(:notice_successful_delete) redirect_to action: 'index', project_id: @project end def destroy_info @possible_other_budgets = @project.budgets.where.not(id: @budget.id) end def update_material_budget_item @element_id = params[:element_id] cost_type = CostType.where(id: params[:cost_type_id]).first if cost_type && params[:units].present? volume = Rate.parse_number_string_to_number(params[:units]) @costs = begin volume * cost_type.rate_at(params[:fixed_date]).rate rescue StandardError 0.0 end @unit = volume == 1.0 ? cost_type.unit : cost_type.unit_plural else @costs = 0.0 @unit = cost_type.try(:unit_plural) || '' end respond_to do |format| format.json do render json: render_item_as_json(@element_id, @costs, @unit, @project, :view_cost_rates) end end end def update_labor_budget_item @element_id = params[:element_id] user = User.where(id: params[:user_id]).first if user && params[:hours] hours = Rate.parse_number_string_to_number(params[:hours]) @costs = begin hours * user.rate_at(params[:fixed_date], @project).rate rescue StandardError 0.0 end else @costs = 0.0 end respond_to do |format| format.json do render json: render_item_as_json(@element_id, @costs, @unit, @project, :view_hourly_rates) end end end private def find_budget # This function comes directly from issues_controller.rb (Redmine 0.8.4) @budget = Budget.includes(:project, :author).find_by(id: params[:id]) @project = @budget.project if @budget rescue ActiveRecord::RecordNotFound render_404 end def find_budgets # This function comes directly from issues_controller.rb (Redmine 0.8.4) @budgets = Budget.where(id: params[:id] || params[:ids]) raise ActiveRecord::RecordNotFound if @budgets.empty? projects = @budgets.map(&:project).compact.uniq if projects.size == 1 @project = projects.first else # TODO: let users bulk edit/move/destroy budgets from different projects render_error 'Can not bulk edit/move/destroy cost objects from different projects' and return false end rescue ActiveRecord::RecordNotFound render_404 end def find_project @project = Project.find(params[:project_id]) rescue ActiveRecord::RecordNotFound render_404 end def find_optional_project @project = Project.find(params[:project_id]) unless params[:project_id].blank? rescue ActiveRecord::RecordNotFound render_404 end def render_item_as_json(element_id, costs, unit, project, permission) response = { "#{element_id}_unit_name" => ActionController::Base.helpers.sanitize(unit), "#{element_id}_currency" => Setting.plugin_costs['costs_currency'] } if current_user.allowed_to?(permission, project) response["#{element_id}_costs"] = number_to_currency(costs) response["#{element_id}_cost_value"] = unitless_currency_number(costs) end response end def default_budget_sort { 'id' => "#{Budget.table_name}.id", 'subject' => "#{Budget.table_name}.subject", 'fixed_date' => "#{Budget.table_name}.fixed_date" } end def visible_sorted_budgets Budget .visible(current_user) .order(sort_clause) .includes(:author) .where(project_id: @project.id) .page(page_param) .per_page(per_page_param) end def check_and_update_belonging_work_packages if params[:todo] update_belonging_work_packages end budget = Budget.find(params[:id]) if budget.work_packages.any? redirect_to destroy_info_budget_path(budget) end end def update_belonging_work_packages reassign_to_id = params[:reassign_to_id] budget_id = params[:id] budget_exists = Budget.visible(current_user).exists?(reassign_to_id) if params[:todo] == 'reassign' reassign_to = budget_exists ? reassign_to_id : nil WorkPackage .where(budget_id: budget_id) .update_all(budget_id: reassign_to, updated_at: DateTime.now) end end