diff --git a/app/assets/javascripts/angular/openproject-costs-app.js b/app/assets/javascripts/angular/openproject-costs-app.js new file mode 100644 index 0000000000..3f2d5839a9 --- /dev/null +++ b/app/assets/javascripts/angular/openproject-costs-app.js @@ -0,0 +1,38 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +// +// 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 doc/COPYRIGHT.rdoc for more details. +//++ + +// main app +var openprojectCostsApp = angular.module('openproject'); + +openprojectCostsApp.run(['HookService', function(HookService) { + HookService.register('workPackageOverviewAttributes', function(params) { + var isEmpty = params.workPackage.embedded.summarizedCostEntries.length == 0; + + return ((params.type == "spentUnits" && !isEmpty) ? "summarized-cost-entries" : undefined); + }); +}]); diff --git a/app/assets/javascripts/angular/work_packages/directives/summarized-cost-entries-directive.js b/app/assets/javascripts/angular/work_packages/directives/summarized-cost-entries-directive.js new file mode 100644 index 0000000000..c8db3f3a6c --- /dev/null +++ b/app/assets/javascripts/angular/work_packages/directives/summarized-cost-entries-directive.js @@ -0,0 +1,50 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +// +// 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 doc/COPYRIGHT.rdoc for more details. +//++ + +angular.module('openproject.workPackages.directives') + +.directive('summarizedCostEntries', ['PathHelper', function(PathHelper) { + return { + restrict: 'E', + templateUrl: '/templates/work_packages/summarized_cost_entries.html', + link: function(scope, element, attributes) { + if (scope.workPackage.embedded.summarizedCostEntries) { + scope.costTypes = scope.workPackage.embedded.summarizedCostEntries; + } + + scope.linkToSummary = function(costType) { + var link = PathHelper.staticWorkPackagePath(scope.workPackage.props.id); + + link += '/cost_entries?cost_type_id=' + costType.props.id; + link += '&project_id=' + scope.workPackage.props.projectId; + + return link; + }; + } + }; +}]); diff --git a/app/assets/stylesheets/costs/costs.css.sass b/app/assets/stylesheets/costs/costs.css.sass new file mode 100644 index 0000000000..c5dac24146 --- /dev/null +++ b/app/assets/stylesheets/costs/costs.css.sass @@ -0,0 +1,36 @@ +/*-- copyright + * OpenProject Backlogs Plugin + * + * Copyright (C)2013 the OpenProject Foundation (OPF) + * Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda + * Copyright (C)2010-2011 friflaj + * Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns + * Copyright (C)2009-2010 Mark Maglana + * Copyright (C)2009 Joe Heck, Nate Lowrie + * + * 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 Backlogs is a derivative work based on ChiliProject Backlogs. + * The copyright follows: + * Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj + * Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany + * + * 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 doc/COPYRIGHT.rdoc for more details. + */ + +@import costs/costs_legacy diff --git a/app/assets/stylesheets/costs/costs.css.erb b/app/assets/stylesheets/costs/costs_legacy.css similarity index 100% rename from app/assets/stylesheets/costs/costs.css.erb rename to app/assets/stylesheets/costs/costs_legacy.css diff --git a/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb b/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb new file mode 100644 index 0000000000..5946131d94 --- /dev/null +++ b/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb @@ -0,0 +1,23 @@ +<%#-- copyright +OpenProject Costs Plugin + +Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +version 3. + +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. + +++#%> + +<%= javascript_include_tag 'angular/openproject-costs-app.js' %> +<%= javascript_include_tag 'angular/work_packages/directives/summarized-cost-entries-directive.js' %> +<%= stylesheet_link_tag 'costs/costs.css' %> diff --git a/config/locales/js-de.yml b/config/locales/js-de.yml new file mode 100644 index 0000000000..1e35610608 --- /dev/null +++ b/config/locales/js-de.yml @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject Backlogs Plugin +# +# Copyright (C)2013-2014 the OpenProject Foundation (OPF) +# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda +# Copyright (C)2010-2011 friflaj +# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns +# Copyright (C)2009-2010 Mark Maglana +# Copyright (C)2009 Joe Heck, Nate Lowrie +# +# 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 Backlogs is a derivative work based on ChiliProject Backlogs. +# The copyright follows: +# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj +# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +de: + js: + work_packages: + properties: + costObject: "Budget" + spentHours: "Aufgewendete Zeit" + overallCosts: "Gesamtkosten" + spentUnits: "Gebuchte Einheiten" diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml new file mode 100644 index 0000000000..fa7fb92269 --- /dev/null +++ b/config/locales/js-en.yml @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject Backlogs Plugin +# +# Copyright (C)2013-2014 the OpenProject Foundation (OPF) +# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda +# Copyright (C)2010-2011 friflaj +# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns +# Copyright (C)2009-2010 Mark Maglana +# Copyright (C)2009 Joe Heck, Nate Lowrie +# +# 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 Backlogs is a derivative work based on ChiliProject Backlogs. +# The copyright follows: +# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj +# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +en: + js: + work_packages: + properties: + costObject: "Budget" + spentHours: "Spent time" + overallCosts: "Overall costs" + spentUnits: "Spent units" diff --git a/lib/api/v3/cost_objects/cost_object_model.rb b/lib/api/v3/cost_objects/cost_object_model.rb new file mode 100644 index 0000000000..61407148fc --- /dev/null +++ b/lib/api/v3/cost_objects/cost_object_model.rb @@ -0,0 +1,52 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'reform' +require 'reform/form/coercion' + +module API + module V3 + module CostObjects + class CostObjectModel < Reform::Form + property :project_id, type: Integer + property :author, type: String + property :subject, type: String + property :description, type: String + property :type, type: String + property :fixed_date, type: DateTime + property :created_on, type: DateTime + property :updated_on, type: DateTime + + def author + ::API::V3::Users::UserModel.new(model.author) unless model.author.nil? + end + end + end + end +end diff --git a/lib/api/v3/cost_objects/cost_object_representer.rb b/lib/api/v3/cost_objects/cost_object_representer.rb new file mode 100644 index 0000000000..ddcbff267c --- /dev/null +++ b/lib/api/v3/cost_objects/cost_object_representer.rb @@ -0,0 +1,69 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'roar/decorator' +require 'roar/representer/json/hal' + +module API + module V3 + module CostObjects + class CostObjectRepresenter < Roar::Decorator + include Roar::Representer::JSON::HAL + include Roar::Representer::Feature::Hypermedia + include OpenProject::StaticRouting::UrlHelpers + + self.as_strategy = API::Utilities::CamelCasingStrategy.new + + def initialize(model, options = {}, *expand) + @expand = expand + + super(model) + end + + property :_type, exec_context: :decorator + + property :id, render_nil: true + property :project_id + property :project_name, getter: -> (*) { model.project.try(:name) } + property :subject, render_nil: true + property :description, render_nil: true + property :type, render_nil: true + property :fixed_date, getter: -> (*) { model.created_on.utc.iso8601 }, render_nil: true + property :created_at, getter: -> (*) { model.created_on.utc.iso8601 }, render_nil: true + property :updated_at, getter: -> (*) { model.updated_on.utc.iso8601 }, render_nil: true + + property :author, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !author.nil? } + + def _type + 'CostObject' + end + end + end + end +end diff --git a/lib/api/v3/cost_types/cost_type_model.rb b/lib/api/v3/cost_types/cost_type_model.rb new file mode 100644 index 0000000000..90f9f98c45 --- /dev/null +++ b/lib/api/v3/cost_types/cost_type_model.rb @@ -0,0 +1,55 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'reform' +require 'reform/form/coercion' + +module API + module V3 + module CostTypes + class CostTypeModel < Reform::Form + + def initialize(model, options = {}) + @units = options[:units] + + super(model) + end + + property :id, type: Integer + property :name, type: String + property :unit, type: String + property :unit_plural, type: String + + def units + @units ? @units : model.cost_entries.sum(&:units) + end + end + end + end +end diff --git a/lib/api/v3/cost_types/cost_type_representer.rb b/lib/api/v3/cost_types/cost_type_representer.rb new file mode 100644 index 0000000000..801b77e009 --- /dev/null +++ b/lib/api/v3/cost_types/cost_type_representer.rb @@ -0,0 +1,64 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'roar/decorator' +require 'roar/representer/json/hal' + +module API + module V3 + module CostTypes + class CostTypeRepresenter < Roar::Decorator + include Roar::Representer::JSON::HAL + include Roar::Representer::Feature::Hypermedia + include OpenProject::StaticRouting::UrlHelpers + + self.as_strategy = API::Utilities::CamelCasingStrategy.new + + def initialize(model, options = {}, *expand) + @expand = expand + @work_package = options[:work_package] + + super(model) + end + + property :_type, exec_context: :decorator + + property :id, render_nil: true + property :name, render_nil: true + property :units, render_nil: true + property :unit, render_nil: true + property :unit_plural, render_nil: true + + def _type + 'CostType' + end + end + end + end +end diff --git a/lib/open_project/costs/attributes_helper.rb b/lib/open_project/costs/attributes_helper.rb new file mode 100644 index 0000000000..0ba83171cc --- /dev/null +++ b/lib/open_project/costs/attributes_helper.rb @@ -0,0 +1,99 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +module OpenProject::Costs + class AttributesHelper + + def initialize(work_package) + @work_package = work_package + end + + def time_entries_sum + @time_entries_sum ||= compute_time_entries_sum + end + + def overall_costs + @overall_costs ||= compute_overall_costs + end + + def summarized_cost_entries + @summarized_cost_entries ||= compute_summarized_cost_entries + end + + private + + def compute_time_entries_sum + time_entries.sum(&:hours) if user_allowed_to?(:view_time_entries, :view_own_time_entries) + end + + def compute_overall_costs + if material_costs || labor_costs + sum_costs = 0 + sum_costs += material_costs if material_costs + sum_costs += labor_costs if labor_costs + else + sum_costs = nil + end + sum_costs + end + + def compute_summarized_cost_entries + return {} if cost_entries.blank? || !user_allowed_to?(:view_cost_entries, :view_own_cost_entries) + + last_cost_type = "" + + cost_entries.sort_by(&:id).each_with_object({}) do |entry, hash| + if entry.cost_type == last_cost_type + hash[last_cost_type][:units] += entry.units + else + last_cost_type = entry.cost_type + + hash[last_cost_type] = {} + hash[last_cost_type][:units] = entry.units + hash[last_cost_type][:unit] = entry.cost_type.unit + hash[last_cost_type][:unit_plural] = entry.cost_type.unit_plural + end + end + end + + def time_entries + @work_package.time_entries.visible(User.current, @work_package.project) + end + + def material_costs + cost_entries_with_rate = cost_entries.select{|c| c.costs_visible_by?(User.current)} + cost_entries_with_rate.blank? ? nil : cost_entries_with_rate.collect(&:real_costs).sum + end + + def labor_costs + time_entries_with_rate = time_entries.select{|c| c.costs_visible_by?(User.current)} + time_entries_with_rate.blank? ? nil : time_entries_with_rate.collect(&:real_costs).sum + end + + def cost_entries + @cost_entries ||= @work_package.cost_entries.visible(User.current, @work_package.project) + end + + def user_allowed_to?(*privileges) + privileges.inject(false) do |result, privilege| + result || User.current.allowed_to?(privilege, @work_package.project) + end + end + end +end diff --git a/lib/open_project/costs/engine.rb b/lib/open_project/costs/engine.rb index a02c3d9526..1a5339ba3b 100644 --- a/lib/open_project/costs/engine.rb +++ b/lib/open_project/costs/engine.rb @@ -103,7 +103,58 @@ module OpenProject::Costs patches [:WorkPackage, :Project, :Query, :User, :TimeEntry, :Version, :PermittedParams, :ProjectsController, :ApplicationHelper, :UsersHelper] - assets %w(costs/costs.css costs/costs.js) + + extend_api_response(:v3, :work_packages, :work_package) do + include Redmine::I18n + include ActionView::Helpers::NumberHelper + + property :cost_object, + exec_context: :decorator, + embedded: true, + class: ::API::V3::CostObjects::CostObjectModel, + decorator: ::API::V3::CostObjects::CostObjectRepresenter, + if: -> (*) { !represented.work_package.cost_object.nil? } + + property :spent_hours, + exec_context: :decorator, + if: -> (*) { current_user_allowed_to_view_spent_hours } + + property :overall_costs, exec_context: :decorator + + property :summarized_cost_entries, embedded: true, exec_context: :decorator + + send(:define_method, :cost_object) do + ::API::V3::CostObjects::CostObjectModel.new(represented.work_package.cost_object) + end + + send(:define_method, :spent_hours) do + self.attributes_helper.time_entries_sum + end + + send(:define_method, :current_user_allowed_to_view_spent_hours) do + current_user_allowed_to(:view_time_entries, represented.work_package) || + current_user_allowed_to(:view_own_time_entries, represented.work_package) + end + + send(:define_method, :overall_costs) do + number_to_currency(self.attributes_helper.overall_costs) + end + + send(:define_method, :summarized_cost_entries) do + self.attributes_helper.summarized_cost_entries + .map { |s| ::API::V3::CostTypes::CostTypeModel.new(s[0], units: s[1][:units]) } + .map { |c| ::API::V3::CostTypes::CostTypeRepresenter.new(c, work_package: represented.work_package) } + end + + send(:define_method, :attributes_helper) do + @attributes_helper ||= OpenProject::Costs::AttributesHelper.new(represented.work_package) + end + end + + assets %w(angular/work_packages/directives/summarized-cost-entries-directive.js + angular/openproject-costs-app.js + costs/costs.css + costs/costs.js) initializer "costs.register_hooks" do require 'open_project/costs/hooks' @@ -112,6 +163,7 @@ module OpenProject::Costs require 'open_project/costs/hooks/project_hook' require 'open_project/costs/hooks/work_package_action_menu' require 'open_project/costs/hooks/work_packages_show_attributes' + require 'open_project/costs/hooks/work_packages_overview_attributes' end initializer 'costs.register_observers' do |app| @@ -125,6 +177,12 @@ module OpenProject::Costs ActionView::Helpers::NumberHelper.send(:include, OpenProject::Costs::Patches::NumberHelperPatch) end + # Initializer to combine this engines static assets with the static assets of the hosting site. + # Thanks to http://jbavari.github.io/blog/2013/10/26/rev-up-your-rails-engine-for-static-assets/ + initializer "static assets" do |app| + app.middleware.insert_before(::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public") + end + config.to_prepare do # loading the class so that acts_as_journalized gets registered VariableCostObject diff --git a/lib/open_project/costs/hooks/work_package_hook.rb b/lib/open_project/costs/hooks/work_package_hook.rb index e20847121a..ccf725bc8e 100644 --- a/lib/open_project/costs/hooks/work_package_hook.rb +++ b/lib/open_project/costs/hooks/work_package_hook.rb @@ -30,6 +30,8 @@ class OpenProject::Costs::Hooks::WorkPackageHook < Redmine::Hook::ViewListener render_on :view_work_packages_move_bottom, :partial => 'hooks/costs/view_work_packages_move_bottom' + render_on :view_work_package_overview_attributes, partial: 'hooks/costs/view_work_package_overview_attributes' + # Updates the cost object after a move # # Context: diff --git a/lib/open_project/costs/hooks/work_packages_overview_attributes.rb b/lib/open_project/costs/hooks/work_packages_overview_attributes.rb new file mode 100644 index 0000000000..fa79548b63 --- /dev/null +++ b/lib/open_project/costs/hooks/work_packages_overview_attributes.rb @@ -0,0 +1,47 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +module OpenProject::Costs::Hooks + class WorkPackagesOverviewHook < Redmine::Hook::ViewListener + + def work_packages_overview_attributes(context = {}) + project = context[:project] + attributes = context[:attributes] + + return unless project && project.module_enabled?(:costs_module) + + attributes.reject!{ |attribute| attribute == :spentTime } + + attributes << :costObject + attributes << :spentHours if user_allowed_to?(project, :view_time_entries, :view_own_time_entries) + attributes << :overallCosts + attributes << :spentUnits if user_allowed_to?(project, :view_cost_entries, :view_own_cost_entries) + + attributes + end + + private + + def user_allowed_to?(project, *privileges) + privileges.inject(false) do |result, privilege| + result || User.current.allowed_to?(privilege, project) + end + end + end +end diff --git a/lib/open_project/costs/hooks/work_packages_show_attributes.rb b/lib/open_project/costs/hooks/work_packages_show_attributes.rb index 6b4afb0e0c..f3f3319cc1 100644 --- a/lib/open_project/costs/hooks/work_packages_show_attributes.rb +++ b/lib/open_project/costs/hooks/work_packages_show_attributes.rb @@ -39,93 +39,45 @@ module OpenProject::Costs::Hooks private - def cost_entries - @work_package.cost_entries.visible(User.current, @work_package.project) - end - - def material_costs - cost_entries_with_rate = cost_entries.select{|c| c.costs_visible_by?(User.current)} - cost_entries_with_rate.blank? ? nil : cost_entries_with_rate.collect(&:real_costs).sum - end - - def time_entries - @work_package.time_entries.visible(User.current, @work_package.project) - end - - def labor_costs - time_entries_with_rate = time_entries.select{|c| c.costs_visible_by?(User.current)} - time_entries_with_rate.blank? ? nil : time_entries_with_rate.collect(&:real_costs).sum - end - - def overall_costs - if material_costs || labor_costs - sum_costs = 0 - sum_costs += material_costs if material_costs - sum_costs += labor_costs if labor_costs - else - sum_costs = nil - end - sum_costs - end - def cost_work_package_attributes attributes = [] + attributes_helper = OpenProject::Costs::AttributesHelper.new(@work_package) + attributes << work_package_show_table_row(:cost_object) do @work_package.cost_object ? link_to_cost_object(@work_package.cost_object) : empty_element_tag end - if User.current.allowed_to?(:view_time_entries, @project) || - User.current.allowed_to?(:view_own_time_entries, @project) + if attributes_helper.time_entries_sum attributes << work_package_show_table_row(:spent_hours) do - # TODO: put inside controller or model - summed_hours = time_entries.sum(&:hours) + summed_hours = attributes_helper.time_entries_sum summed_hours > 0 ? link_to(l_hours(summed_hours), work_package_time_entries_path(@work_package)) : empty_element_tag end - end + attributes << work_package_show_table_row(:overall_costs) do - overall_costs.nil? ? - empty_element_tag : - number_to_currency(overall_costs) + attributes_helper.overall_costs ? + number_to_currency(attributes_helper.overall_costs) : + empty_element_tag end - if User.current.allowed_to?(:view_cost_entries, @project) || - User.current.allowed_to?(:view_own_cost_entries, @project) - + if attributes_helper.summarized_cost_entries attributes << work_package_show_table_row(:spent_units) do - summarized_cost_entries(cost_entries, @work_package) + summarized_cost_entry_links(attributes_helper.summarized_cost_entries, @work_package) end end attributes end - def summarized_cost_entries(cost_entries, work_package, create_link=true) - last_cost_type = "" - - return empty_element_tag if cost_entries.blank? - result = cost_entries.sort_by(&:id).inject(Hash.new) do |result, entry| - if entry.cost_type == last_cost_type - result[last_cost_type][:units] += entry.units - else - last_cost_type = entry.cost_type - - result[last_cost_type] = {} - result[last_cost_type][:units] = entry.units - result[last_cost_type][:unit] = entry.cost_type.unit - result[last_cost_type][:unit_plural] = entry.cost_type.unit_plural - end - result - end - + def summarized_cost_entry_links(cost_entries, work_package, create_link=true) str_array = [] - result.each do |k, v| + cost_entries.each do |k, v| txt = pluralize(v[:units], v[:unit], v[:unit_plural]) if create_link # TODO why does this have project_id, work_package_id and cost_type_id params? diff --git a/public/templates/work_packages/summarized_cost_entries.html b/public/templates/work_packages/summarized_cost_entries.html new file mode 100644 index 0000000000..15529060c7 --- /dev/null +++ b/public/templates/work_packages/summarized_cost_entries.html @@ -0,0 +1,12 @@ + + + + {{ costType.props.units }} + + {{ costType.props.unit }} + {{ costType.props.unitPlural }} + + + - + + diff --git a/spec/lib/api/v3/cost_objects/cost_object_model_spec.rb b/spec/lib/api/v3/cost_objects/cost_object_model_spec.rb new file mode 100644 index 0000000000..de672d1dde --- /dev/null +++ b/spec/lib/api/v3/cost_objects/cost_object_model_spec.rb @@ -0,0 +1,51 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +require 'spec_helper' + +describe ::API::V3::CostObjects::CostObjectModel do + include Capybara::RSpecMatchers + + let(:project) { FactoryGirl.build(:project) } + let(:user) { FactoryGirl.build(:user, member_in_project: project) } + let(:cost_object) { FactoryGirl.build(:cost_object, + author: user, + project: project, + updated_on: Date.today) } + + subject(:model) { ::API::V3::CostObjects::CostObjectModel.new(cost_object) } + + describe 'attributes' do + it { expect(subject.project_id).to eq(cost_object.project_id) } + + it { expect(subject.author.id).to eq(cost_object.author_id) } + + it { expect(subject.subject).to eq(cost_object.subject) } + + it { expect(subject.description).to eq(cost_object.description) } + + it { expect(subject.type).to eq(cost_object.type) } + + it { expect(subject.fixed_date).to eq(cost_object.fixed_date) } + + it { expect(subject.created_on).to eq(cost_object.created_on) } + + it { expect(subject.updated_on).to eq(cost_object.updated_on) } + end +end diff --git a/spec/lib/api/v3/cost_objects/cost_object_representer_spec.rb b/spec/lib/api/v3/cost_objects/cost_object_representer_spec.rb new file mode 100644 index 0000000000..587baee594 --- /dev/null +++ b/spec/lib/api/v3/cost_objects/cost_object_representer_spec.rb @@ -0,0 +1,58 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +require 'spec_helper' + +describe ::API::V3::CostObjects::CostObjectRepresenter do + let(:project) { FactoryGirl.build(:project, id: 999) } + let(:user) { FactoryGirl.build(:user, + member_in_project: project, + created_on: 1.day.ago, + updated_on: Date.today) } + let(:cost_object) { FactoryGirl.build(:cost_object, + author: user, + project: project, + created_on: 1.day.ago, + updated_on: Date.today) } + + let(:representer) { described_class.new(model) } + + let(:model) { ::API::V3::CostObjects::CostObjectModel.new(cost_object) } + + context 'generation' do + subject(:generated) { representer.to_json } + + it { should include_json('CostObject'.to_json).at_path('_type') } + + describe 'cost_object' do + it { should have_json_path('id') } + + it { should have_json_path('description') } + + it { should have_json_path('projectId') } + it { should have_json_path('projectName') } + + it { should have_json_path('subject') } + it { should have_json_path('type') } + + it { should have_json_path('createdAt') } + it { should have_json_path('updatedAt') } + end + end +end diff --git a/spec/lib/api/v3/cost_types/cost_type_model_spec.rb b/spec/lib/api/v3/cost_types/cost_type_model_spec.rb new file mode 100644 index 0000000000..dc0eb0224f --- /dev/null +++ b/spec/lib/api/v3/cost_types/cost_type_model_spec.rb @@ -0,0 +1,53 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +require 'spec_helper' + +describe ::API::V3::CostTypes::CostTypeModel do + include Capybara::RSpecMatchers + + let(:project) { FactoryGirl.build(:project) } + let(:user) { FactoryGirl.build(:user, member_in_project: project) } + let(:cost_type) { FactoryGirl.build(:cost_type) } + let!(:cost_entry) { FactoryGirl.build(:cost_entry, + work_package: nil, + project: project, + units: 3, + spent_on: Date.today, + user: user, + comments: "Entry 1") } + + subject(:model) { ::API::V3::CostTypes::CostTypeModel.new(cost_type) } + + describe 'attributes' do + it { expect(subject.name).to eq(cost_type.name) } + + it { expect(subject.unit).to eq(cost_type.unit) } + + it { expect(subject.unit_plural).to eq(cost_type.unit_plural) } + + it { expect(subject.units).to eq(cost_type.cost_entries.sum(&:units)) } + + describe 'units' do + subject(:model) { ::API::V3::CostTypes::CostTypeModel.new(cost_type, units: 42) } + + it { expect(subject.units).to eq(42) } + end + end +end diff --git a/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb b/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb new file mode 100644 index 0000000000..b3e126e387 --- /dev/null +++ b/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb @@ -0,0 +1,58 @@ +#-- copyright +# OpenProject Costs Plugin +# +# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# version 3. +# +# 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. +#++ + +require 'spec_helper' + +describe ::API::V3::CostTypes::CostTypeRepresenter do + let(:project) { FactoryGirl.build(:project, id: 999) } + let(:user) { FactoryGirl.build(:user, + member_in_project: project, + created_on: 1.day.ago, + updated_on: Date.today) } + let(:work_package) { FactoryGirl.build(:work_package, + project: project) } + let(:cost_type) { FactoryGirl.build(:cost_type) } + let!(:cost_entry) { FactoryGirl.build(:cost_entry, + work_package: nil, + project: project, + units: 3, + spent_on: Date.today, + user: user, + comments: "Entry 1") } + + let(:representer) { described_class.new(model, work_package: work_package) } + + let(:model) { ::API::V3::CostTypes::CostTypeModel.new(cost_type) } + + context 'generation' do + subject(:generated) { representer.to_json } + + it { should include_json('CostType'.to_json).at_path('_type') } + + describe 'cost_type' do + it { should have_json_path('id') } + + it { should have_json_path('name') } + + it { should have_json_path('units') } + it { should have_json_path('unit') } + it { should have_json_path('unitPlural') } + end + end +end diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb new file mode 100644 index 0000000000..5fc36c1835 --- /dev/null +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -0,0 +1,89 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe ::API::V3::WorkPackages::WorkPackageRepresenter do + let(:project) { FactoryGirl.create(:project) } + let(:role) { FactoryGirl.create(:role, permissions: [:view_time_entries, + :view_cost_entries, + :view_cost_rates]) } + let(:user) { FactoryGirl.create(:user, + member_in_project: project, + member_through_role: role) } + + let(:cost_object) { FactoryGirl.create(:cost_object, project: project) } + let(:cost_entry_1) { FactoryGirl.create(:cost_entry, + work_package: work_package, + project: project, + units: 3, + spent_on: Date.today, + user: user, + comments: "Entry 1") } + let(:cost_entry_2) { FactoryGirl.create(:cost_entry, + work_package: work_package, + project: project, + units: 3, + spent_on: Date.today, + user: user, + comments: "Entry 2") } + + let(:work_package) { FactoryGirl.create(:work_package, + project_id: project.id, + cost_object: cost_object) } + let(:model) { ::API::V3::WorkPackages::WorkPackageModel.new(work_package: work_package) } + let(:representer) { described_class.new(model, current_user: user) } + + + before(:each) do + allow(User).to receive(:current).and_return user + end + + describe 'generation' do + before do + cost_entry_1 + cost_entry_2 + end + + subject(:generated) { representer.to_json } + + describe 'work_package' do + it { should_not have_json_path('spentTime') } + + it { should have_json_path('spentHours') } + + it { should have_json_path('overallCosts') } + + describe 'embedded' do + it { should have_json_path('_embedded/costObject') } + + it { should have_json_path('_embedded/summarizedCostEntries') } + end + end + end +end