Merge pull request #54 from finnlabs/feature/work_package_migration

Feature/work package migration
pull/6827/head
cratz 11 years ago
commit aae1f5aef5
  1. 2
      CHANGELOG.md
  2. 0
      README.md
  3. 4
      app/controllers/cost_objects_controller.rb
  4. 46
      app/controllers/costlog_controller.rb
  5. 6
      app/helpers/costlog_helper.rb
  6. 27
      app/models/cost_object.rb
  7. 33
      app/models/journal/cost_object_journal.rb
  8. 34
      app/models/journal/variable_cost_object_journal.rb
  9. 11
      app/models/variable_cost_object.rb
  10. 16
      app/views/cost_objects/_show_variable_cost_object.html.erb
  11. 4
      app/views/costlog/_list.html.erb
  12. 10
      app/views/costlog/index.html.erb
  13. 24
      app/views/hooks/_view_issues_context_menu_end.html.erb
  14. 2
      app/views/hooks/_view_projects_settings_members_table_header.html.erb
  15. 4
      app/views/hooks/_view_work_package_show_action_menu.html.erb
  16. 4
      app/views/hooks/_view_work_packages_bulk_edit_details_bottom.html.erb
  17. 24
      app/views/hooks/_view_work_packages_context_menu_end.html.erb
  18. 4
      app/views/hooks/_view_work_packages_form_details_bottom.html.erb
  19. 4
      app/views/hooks/_view_work_packages_move_bottom.html.erb
  20. 8
      app/views/hooks/_view_work_packages_show_details_bottom.html.erb
  21. 20
      app/views/work_packages/_action_menu.html.erb
  22. 2
      app/views/work_packages/destroy.html.erb
  23. 6
      config/locales/de.yml
  24. 14
      config/locales/en.yml
  25. 2
      db/migrate/014_add_denormalized_costs_fields.rb
  26. 2
      db/migrate/018_higher_precision_for_currency.rb
  27. 40
      db/migrate/20130918084158_add_cost_object_journals.rb
  28. 5
      db/migrate/20130918084919_add_cost_object_id_to_work_package_journals.rb
  29. 2
      features/activity.feature
  30. 2
      features/cost_types/deletion.feature
  31. 12
      features/credit_unit_costs.feature
  32. 15
      features/set_rate.feature
  33. 30
      features/step_definitions/cost_steps.rb
  34. 60
      features/view_own_rates.feature
  35. 19
      lib/open_project/costs/engine.rb
  36. 2
      lib/open_project/costs/hooks/work_package_action_menu.rb
  37. 40
      lib/open_project/costs/hooks/work_package_hook.rb
  38. 21
      lib/open_project/costs/patches/issue_observer.rb
  39. 27
      lib/open_project/costs/patches/issue_patch.rb
  40. 77
      lib/open_project/costs/patches/issues_controller_patch.rb
  41. 89
      lib/open_project/costs/patches/issues_helper_patch.rb
  42. 2
      lib/open_project/costs/patches/projects_controller_patch.rb
  43. 8
      lib/open_project/costs/patches/query_patch.rb
  44. 4
      lib/open_project/costs/patches/time_entry_patch.rb
  45. 2
      lib/open_project/costs/patches/version_patch.rb
  46. 21
      lib/open_project/costs/patches/work_package_observer.rb
  47. 6
      lib/open_project/costs/patches/work_package_patch.rb
  48. 40
      lib/open_project/costs/patches/work_packages_controller_patch.rb
  49. 76
      lib/open_project/costs/patches/work_packages_helper_patch.rb
  50. 4
      openproject-costs.gemspec
  51. 76
      spec/controllers/costlog_controller_spec.rb
  52. 2
      spec/factories/cost_entry_factory.rb
  53. 24
      spec/models/cost_entry_spec.rb
  54. 37
      spec/models/issue_spec.rb
  55. 16
      spec/models/time_entry_spec.rb
  56. 22
      spec/models/user_deletion_spec.rb
  57. 12
      spec/models/variable_cost_object_spec.rb
  58. 37
      spec/models/work_package_spec.rb

@ -1,3 +1,5 @@
* `#2050` Migrate to new data model.
= 5.0.1.pre4 - 2013-07-11
* Allows for assigning budgets to work_packages

@ -26,7 +26,7 @@ class CostObjectsController < ApplicationController
include CostlogHelper
helper :cost_objects
include CostObjectsHelper
include Redmine::Export::PDF
include WorkPackage::PdfExporter
include PaginationHelper
menu_item :new_budget, :only => [:new]
@ -35,7 +35,7 @@ class CostObjectsController < ApplicationController
def index
respond_to do |format|
format.html { }
format.csv { limit = Setting.issues_export_limit.to_i }
format.csv { limit = Setting.work_packages_export_limit.to_i }
end
sort_columns = {'id' => "#{CostObject.table_name}.id",

@ -1,7 +1,7 @@
class CostlogController < ApplicationController
unloadable
menu_item :issues
menu_item :work_packages
before_filter :find_project, :authorize, :only => [:edit,
:new,
:create,
@ -14,7 +14,7 @@ class CostlogController < ApplicationController
helper :sort
include SortHelper
helper :issues
helper :work_packages
include CostlogHelper
include PaginationHelper
@ -23,7 +23,7 @@ class CostlogController < ApplicationController
sort_update 'spent_on' => 'spent_on',
'user' => 'user_id',
'project' => "#{Project.table_name}.name",
'issue' => 'work_package_id',
'work_package' => 'work_package_id',
'cost_type' => 'cost_type_id',
'units' => 'units',
'costs' => 'costs'
@ -31,11 +31,11 @@ class CostlogController < ApplicationController
cond = ARCondition.new
if @project.nil?
cond << Project.allowed_to_condition(User.current, :view_cost_entries)
elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?)
elsif @work_package.nil?
cond << @project.project_condition(Setting.display_subprojects_work_packages?)
else
root_cond = "#{WorkPackage.table_name}.root_id #{(@issue.root_id.nil?) ? "IS NULL" : "= #{@issue.root_id}"}"
cond << "#{root_cond} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
root_cond = "#{WorkPackage.table_name}.root_id #{(@work_package.root_id.nil?) ? "IS NULL" : "= #{@work_package.root_id}"}"
cond << "#{root_cond} AND #{WorkPackage.table_name}.lft >= #{@work_package.lft} AND #{WorkPackage.table_name}.rgt <= #{@work_package.rgt}"
end
cond << Project.allowed_to_condition(User.current, :view_cost_entries, :project => @project)
@ -49,7 +49,7 @@ class CostlogController < ApplicationController
respond_to do |format|
format.html {
@entries = CostEntry.includes(:project, :cost_type, :user, {:work_package => :tracker})
@entries = CostEntry.includes(:project, :cost_type, :user, {:work_package => :type})
.where(cond.conditions)
.order(sort_clause)
.page(page_param)
@ -135,12 +135,12 @@ private
if params[:id]
@cost_entry = CostEntry.find(params[:id])
@project = @cost_entry.project
elsif params[:issue_id]
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif params[:work_package_id]
@issue = WorkPackage.find(params[:work_package_id])
@project = @issue.project
@work_package = WorkPackage.find(params[:work_package_id])
@project = @work_package.project
elsif params[:work_package_id]
@work_package = WorkPackage.find(params[:work_package_id])
@project = @work_package.project
elsif params[:project_id]
@project = Project.find(params[:project_id])
else
@ -152,12 +152,12 @@ private
end
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
if !params[:work_package_id].blank?
@work_package = WorkPackage.find(params[:work_package_id])
@project = @work_package.project
elsif !params[:work_package_id].blank?
@issue = WorkPackage.find(params[:work_package_id])
@project = @issue.project
@work_package = WorkPackage.find(params[:work_package_id])
@project = @work_package.project
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
@ -173,10 +173,10 @@ private
@cost_entry.user :
User.find_by_id(user_id)
issue_id = params[:cost_entry].delete(:work_package_id)
@issue = @cost_entry.present? && @cost_entry.work_package_id == issue_id ?
work_package_id = params[:cost_entry].delete(:work_package_id)
@work_package = @cost_entry.present? && @cost_entry.work_package_id == work_package_id ?
@cost_entry.work_package :
WorkPackage.find_by_id(issue_id)
WorkPackage.find_by_id(work_package_id)
cost_type_id = params[:cost_entry].delete(:cost_type_id)
@cost_type = @cost_entry.present? && @cost_entry.cost_type_id == cost_type_id ?
@ -233,7 +233,7 @@ private
def new_default_cost_entry
@cost_entry = CostEntry.new.tap do |ce|
ce.project = @project
ce.work_package = @issue
ce.work_package = @work_package
ce.user = User.current
ce.spent_on = Date.today
# notice that cost_type is set to default cost_type in the model
@ -242,7 +242,7 @@ private
def update_cost_entry_from_params
@cost_entry.user = @user
@cost_entry.work_package = @issue
@cost_entry.work_package = @work_package
@cost_entry.cost_type = @cost_type
@cost_entry.attributes = permitted_params.cost_entry

@ -3,9 +3,9 @@ module CostlogHelper
def render_costlog_breadcrumb
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
links << link_to_issue(@issue, :subject => false) if @issue
links << link_to(l(:label_project_all), {:project_id => nil, :work_package_id => nil})
links << link_to(h(@project), {:project_id => @project, :work_package_id => nil}) if @project
links << link_to_work_package(@work_package, :subject => false) if @work_package
breadcrumb links
end

@ -14,14 +14,13 @@ class CostObject < ActiveRecord::Base
acts_as_attachable :after_remove => :attachment_removed
unless respond_to? :acts_as_journalized
acts_as_event :title => Proc.new {|o| "#{l(:label_cost_object)} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => '/cost_objects', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:timestamp => "#{table_name}.updated_on",
:author_key => :author_id
end
acts_as_journalized :event_type => 'cost-object',
:event_title => Proc.new {|o| "#{l(:label_cost_object)} ##{o.id}: #{o.subject}"},
:event_url => Proc.new {|o| {:controller => 'cost_objects', :action => 'show', :id => o.id}},
:activity_find_options => {:include => [:project, :author]},
:activity_timestamp => "#{table_name}.updated_on",
:activity_author_key => :author_id,
:activity_permission => :view_cost_objects
validates_presence_of :subject, :project, :author, :kind
validates_length_of :subject, :maximum => 255
@ -55,16 +54,16 @@ class CostObject < ActiveRecord::Base
self[:type] = type
end
# Assign all the issues with +version_id+ to this Cost Object
def assign_issues_by_version(version_id)
# Assign all the work_packages with +version_id+ to this Cost Object
def assign_work_packages_by_version(version_id)
version = Version.find_by_id(version_id)
return 0 if version.nil? || version.fixed_issues.blank?
return 0 if version.nil? || version.fixed_work_packages.blank?
version.fixed_issues.each do |issue|
issue.update_attribute(:cost_object_id, self.id)
version.fixed_work_packages.each do |work_package|
work_package.update_attribute(:cost_object_id, self.id)
end
return version.fixed_issues.size
return version.fixed_work_packages.size
end
# Change the Cost Object type to another type. Valid types are

@ -0,0 +1,33 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class Journal::CostObjectJournal < ActiveRecord::Base
# include ActiveModel::ForbiddenAttributesProtection
self.table_name = "cost_object_journals"
belongs_to :journal
# attr_accessible :project_id, :author_id, :subject, :description, :fixed_date, :created_on
@@journaled_attributes = [:project_id,
:author_id,
:subject,
:description,
:fixed_date]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class Journal::VariableCostObjectJournal < Journal::CostObjectJournal
# include ActiveModel::ForbiddenAttributesProtection
# self.table_name = "cost_object_journals"
# belongs_to :journal
# attr_accessible :type, :id, :project_id, :author_id, :subject, :description, :fixed_date, :created_on
# @@journaled_attributes = [:project_id,
# :author_id,
# :subject,
# :description,
# :type,
# :fixed_date]
# def journaled_attributes
# attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
# end
end

@ -16,17 +16,6 @@ class VariableCostObject < CostObject
after_update :save_material_budget_items
after_update :save_labor_budget_items
if respond_to? :acts_as_journalized
acts_as_journalized :event_type => 'cost-object',
:event_title => Proc.new {|o| "#{l(:label_cost_object)} ##{o.journaled.id}: #{o.subject}"},
:event_url => Proc.new {|o| {:controller => 'cost_objects', :action => 'show', :id => o.journaled.id}},
:activity_type => superclass.plural_name,
:activity_find_options => {:include => [:project, :author]},
:activity_timestamp => "#{table_name}.updated_on",
:activity_author_key => :author_id,
:activity_permission => :view_cost_objects
end
# override acts_as_journalized method
def activity_type

@ -28,13 +28,13 @@
<h4><%= l(:caption_material_costs) %></h4>
<table class="material_budget_items list">
<thead><tr>
<th><%= CostEntry.human_attribute_name(:issue)%></th>
<th><%= CostEntry.human_attribute_name(:work_package)%></th>
<th><%= CostEntry.human_attribute_name(:units) %></th>
<th><%= CostEntry.human_attribute_name(:cost_type) %></th>
<th class="currency"><%= CostEntry.human_attribute_name(:costs) %></th>
</tr></thead>
<tbody>
<% @cost_object.cost_entries.visible(User.current, @project).all(:include => [:cost_type]).group_by(&:work_package).each do |issue, cost_entries|
<% @cost_object.cost_entries.visible(User.current, @project).all(:include => [:cost_type]).group_by(&:work_package).each do |work_package, cost_entries|
entries = cost_entries.inject(Hash.new) do |results, entry|
result = results[entry.cost_type.id.to_s]
unless result
@ -50,8 +50,8 @@
entries.each do |c|
%>
<tr>
<td class="subject"><%= link_to_issue issue %></td>
<td><%= link_to pluralize(c.units, c.cost_type.unit, c.cost_type.unit_plural), {:controller => "/costlog", :action => "index", :cost_type_id => c.cost_type, :issue_id => issue} %></td>
<td class="subject"><%= link_to_work_package work_package %></td>
<td><%= link_to pluralize(c.units, c.cost_type.unit, c.cost_type.unit_plural), {:controller => "/costlog", :action => "index", :cost_type_id => c.cost_type, :work_package_id => work_package} %></td>
<td><%= c.cost_type %></td>
<td class="currency"><%= c.costs_visible_by?(User.current) ? number_to_currency(c.real_costs) : "" %></td>
</tr>
@ -103,13 +103,13 @@
<h4><%= l(:caption_labor_costs) %></h4>
<table class="labor_budget_items list">
<thead><tr>
<th><%= TimeEntry.human_attribute_name(:issue) %></th>
<th><%= TimeEntry.human_attribute_name(:work_package) %></th>
<th><%= TimeEntry.human_attribute_name(:hours) %></th>
<th><%= TimeEntry.human_attribute_name(:user) %></th>
<th class="currency"><%= TimeEntry.human_attribute_name(:costs) %></th>
</tr></thead>
<tbody>
<% @cost_object.time_entries.visible(User.current, @project).all.group_by(&:work_package).each do |issue, time_entries|
<% @cost_object.time_entries.visible(User.current, @project).all.group_by(&:work_package).each do |work_package, time_entries|
entries = time_entries.inject(Hash.new) do |results, entry|
result = results[entry.user.id.to_s]
unless result
@ -126,8 +126,8 @@
entries.each do |t|
%>
<tr>
<td class="subject"><%= link_to_issue issue %></td>
<td class="hours"><%= link_to "#{t.hours}h", {:controller => "/timelog", :action => "index", :issue_id => issue} %></td>
<td class="subject"><%= link_to_work_package work_package %></td>
<td class="hours"><%= link_to "#{t.hours}h", {:controller => "/timelog", :action => "index", :work_package_id => work_package} %></td>
<td><%=h t.user.name %></td>
<td class="currency"><%= number_to_currency(t.real_costs) %></td>
</tr>

@ -4,7 +4,7 @@
<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
<%= sort_header_tag('user', :caption => Member.model_name.human) %>
<%= sort_header_tag('project', :caption => Project.model_name.human )%>
<%= sort_header_tag('issue', :caption => Issue.model_name.human, :default_order => 'desc') %>
<%= sort_header_tag('work_package', :caption => WorkPackage.model_name.human, :default_order => 'desc') %>
<th><%= CostEntry.human_attribute_name(:comment) %></th>
<%= sort_header_tag('units', :caption => l(:label_units)) %>
<%= sort_header_tag('costs', :caption => l(:label_overall_costs)) %>
@ -19,7 +19,7 @@
<td class="project"><%=h entry.project %></td>
<td class="subject">
<% if entry.work_package -%>
<%= link_to_issue entry.work_package -%>
<%= link_to_work_package entry.work_package -%>
<% end -%>
</td>

@ -1,15 +1,15 @@
<%= render :partial => 'shared/costs_header' %>
<div class="contextual">
<%= link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-pieces' %>
<%= link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'edit', :project_id => @project, :work_package_id => @work_package}, :class => 'icon icon-pieces' %>
</div>
<%= render_costlog_breadcrumb %>
<h2><%= Issue.human_attribute_name(:spent_costs) %></h2>
<h2><%= WorkPackage.human_attribute_name(:spent_costs) %></h2>
<%= form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
<%= hidden_field_tag('project_id', params[:project_id]) if @project %>
<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
<%= hidden_field_tag 'work_package_id', params[:work_package_id] if @work_package %>
<%= hidden_field_tag 'cost_type_id', params[:cost_type_id] if @cost_type %>
<%= render :partial => 'date_range' %>
<% end %>
@ -19,8 +19,8 @@
<%= pagination_links_full @entries %>
<% end %>
<% html_title Issue.human_attribute_name(:spent_costs), l(:label_details) %>
<% html_title WorkPackage.human_attribute_name(:spent_costs), l(:label_details) %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => Issue.human_attribute_name(:spent_costs)) %>
<%= auto_discovery_link_tag(:atom, {:work_package_id => @work_package, :format => 'atom', :key => User.current.rss_key}, :title => WorkPackage.human_attribute_name(:spent_costs)) %>
<% end %>

@ -1,24 +0,0 @@
<% if not project.nil? and project.module_enabled? :costs_module %>
<%
cost_objects_any = false
possible_cost_objects = issues.inject(issues.first.project.cost_objects) do |intersect, issue|
cost_objects_any |= issue.project.cost_objects.any?
issue.project.cost_objects & intersect
end
%>
<li class="folder">
<a href="#" class="submenu"><%= l(:label_cost_object) %></a>
<ul>
<% unless possible_cost_objects.empty? -%>
<% possible_cost_objects.each do |co| -%>
<li>
<%= context_menu_link co.subject, {:controller => '/issues', :action => 'bulk_edit', :ids => issues.collect(&:id), 'cost_object_id' => co, :back_url => back}, :method => :post,
:selected => (@issue && co == @issue.cost_object), :disabled => !can[:edit] %>
</li>
<% end -%>
<% else -%>
<li><%= l(cost_objects_any ? :notice_cost_object_conflict : :notice_no_cost_objects_available)%></li>
<% end -%>
</ul>
</li>
<% end %>

@ -5,4 +5,4 @@
<% if User.current.allowed_to?(:edit_hourly_rates, project) %>
<th><%= l(:caption_set_rate) %></th>
<% end %>
<% end %>
<% end %>

@ -1,4 +1,4 @@
<% if @project.module_enabled? :costs_module %>
<% if @project && @project.module_enabled?(:costs_module) %>
<hr />
<%= li_unless_nil(link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'new', :work_package_id => @work_package}, :class => 'icon icon-pieces') %>
<%= li_unless_nil(link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'new', :work_package_id => issue}, :class => 'icon icon-pieces') %>
<% end %>

@ -1,8 +1,8 @@
<% if not @project.nil? and @project.module_enabled? :costs_module %>
<% if @project && @project.module_enabled?(:costs_module) %>
<% myselect = select_tag('cost_object_id',
content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_none), :value => 'none') +
options_from_collection_for_select(CostObject.find_all_by_project_id(@project.id, :order => 'subject ASC'), :id, :subject))
%>
<%= content_tag :p, (content_tag "label", CostObject.model_name.human, :for => 'cost_object_id') + myselect %>
<% end %>
<% end %>

@ -0,0 +1,24 @@
<% if @project && @project.module_enabled?(:costs_module) %>
<%
cost_objects_any = false
possible_cost_objects = work_packages.inject(work_packages.first.project.cost_objects) do |intersect, work_package|
cost_objects_any |= work_package.project.cost_objects.any?
work_package.project.cost_objects & intersect
end
%>
<li class="folder">
<a href="#" class="submenu"><%= l(:label_cost_object) %></a>
<ul>
<% unless possible_cost_objects.empty? -%>
<% possible_cost_objects.each do |co| -%>
<li>
<%= context_menu_link co.subject, {:controller => '/work_packages', :action => 'bulk_edit', :ids => work_packages.collect(&:id), 'cost_object_id' => co, :back_url => back}, :method => :post,
:selected => (issue && co == issue.cost_object), :disabled => !can[:edit] %>
</li>
<% end -%>
<% else -%>
<li><%= l(cost_objects_any ? :notice_cost_object_conflict : :notice_no_cost_objects_available)%></li>
<% end -%>
</ul>
</li>
<% end %>

@ -1,5 +1,5 @@
<% if not @project.nil? and @project.module_enabled? :costs_module %>
<% if @project && @project.module_enabled?(:costs_module) %>
<p>
<%= form.select :cost_object_id, CostObject.find_all_by_project_id(@project, :order => 'subject ASC').collect { |d| [d.subject, d.id] }, :include_blank => true%>
</p>
<% end %>
<% end %>

@ -1,8 +1,8 @@
<% if not project.nil? and project.module_enabled? :costs_module %>
<% if @project && @project.module_enabled?(:costs_module) %>
<p>
<label><%= CostObject.model_name.human %></label>
<%= select_tag('cost_object_id', (@target_project == @project ? content_tag('option', l(:label_no_change_option), :value => '') : "") +
content_tag('option', l(:label_none), :value => 'none') +
options_from_collection_for_select(@target_project.cost_objects, :id, :subject)) %>
</p>
<% end %>
<% end %>

@ -1,25 +1,25 @@
<% if @project.module_enabled? :costs_module && @issue%>
<% if @project.module_enabled?(:costs_module) && issue %>
<%# Only render this partial, if the plugin is enabled for the current project %>
<style>
<%# disables core's spent-time as it is not displayed if the user has just the view_own_time_entries permission %>
.spent-time { display: none }
</style>
<% attributes_array = cost_issues_attributes(@issue) %>
<% attributes_array = cost_work_package_attributes(issue) %>
<% (0..(attributes_array.size - 1)).step(2) do |i| %>
<tr>
<th>
<%= attributes_array[i].first %>:
</th>
<td>
<%= attributes_array[i].last %>
<%= attributes_array[i].to_a.last %>
</td>
<% if i + 1 < attributes_array.size %>
<th>
<%= attributes_array[i + 1].first %>:
</th>
<td>
<%= attributes_array[i + 1].last %>
<%= attributes_array[i + 1].to_a.last %>
</td>
<% end %>
</tr>

@ -1,19 +1,19 @@
<% content_for :action_menu_main do %>
<%= li_unless_nil(link_to_if_authorized(l(:button_update), { :controller => '/issues',
<%= li_unless_nil(link_to_if_authorized(l(:button_update), { :controller => '/work_packages',
:action => 'edit',
:id => @issue },
:id => work_package },
:class => 'edit icon icon-edit',
:accesskey => accesskey(:edit))) %>
<%= li_unless_nil(watcher_link(@issue,
<%= li_unless_nil(watcher_link(work_package,
User.current,
{ :class => 'watcher_link',
:replace => User.current.allowed_to?(:view_issue_watchers, @project) ? ['#watchers', '.watcher_link'] : ['.watcher_link'] })) %>
:replace => User.current.allowed_to?(:view_work_package_watchers, @project) ? ['#watchers', '.watcher_link'] : ['.watcher_link'] })) %>
<% end %>
<% content_for :action_menu_more do %>
<%= li_unless_nil(link_to_if_authorized l(:button_log_time), {:controller => '/timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add') %>
<%= li_unless_nil(link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-pieces') %>
<%= li_unless_nil(link_to_if_authorized l(:button_duplicate), {:controller => '/issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-duplicate') %>
<%= li_unless_nil(link_to_if_authorized l(:button_copy), {:controller => '/issue_moves', :action => 'new', :id => @issue, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy') %>
<%= li_unless_nil(link_to_if_authorized l(:button_move), {:controller => '/issue_moves', :action => 'new', :id => @issue}, :class => 'icon icon-move') %>
<%= li_unless_nil(link_to_if_authorized l(:button_delete), {:controller => '/issues', :action => 'destroy', :id => @issue}, :confirm => (@issue.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :method => :post, :class => 'icon icon-del') %>
<%= li_unless_nil(link_to_if_authorized l(:button_log_time), {:controller => '/timelog', :action => 'new', :work_package_id => work_package}, :class => 'icon icon-time-add') %>
<%= li_unless_nil(link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'new', :work_package_id => work_package}, :class => 'icon icon-pieces') %>
<%= li_unless_nil(link_to_if_authorized l(:button_duplicate), {:controller => '/work_packages', :action => 'new', :project_id => @project, :copy_from => work_package }, :class => 'icon icon-duplicate') %>
<%= li_unless_nil(link_to_if_authorized l(:button_copy), {:controller => '/work_package_moves', :action => 'new', :id => work_package, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy') %>
<%= li_unless_nil(link_to_if_authorized l(:button_move), {:controller => '/work_package_moves', :action => 'new', :id => work_package}, :class => 'icon icon-move') %>
<%= li_unless_nil(link_to_if_authorized l(:button_delete), {:controller => '/work_packages', :action => 'destroy', :id => work_package}, :confirm => (work_package.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :method => :post, :class => 'icon icon-del') %>
<% end %>

@ -1,7 +1,7 @@
<h2><%= l(:label_confirmation) %></h2>
<%= form_tag do %>
<%= @issues.collect {|i| hidden_field_tag 'ids[]', i.id } %>
<%= @work_packages.collect {|i| hidden_field_tag 'ids[]', i.id } %>
<div class="box">
<p><strong><%=
if @hours > 0 && !@entries.blank?

@ -3,7 +3,7 @@ de:
activerecord:
attributes:
cost_entry:
issue: "Ticket"
work_package: "Ticket"
work_package: "Arbeitspacket"
overridden_costs: "Überschriebene Kosten"
spent: "Gebucht"
@ -24,7 +24,7 @@ de:
cost_type:
unit: "Einheit"
unit_plural: "Einheit plural"
issue:
work_package:
cost_object_subject: "Budgettitel"
labor_costs: "Personaleinzelkosten"
material_costs: "Stückkosten"
@ -121,7 +121,7 @@ de:
label_group_by_add: "Gruppierungsfeld hinzufügen"
label_hourly_rate: "Stundensatz"
label_include_deleted: "Gelöschte anzeigen"
label_issue_filter_add: "Ticketfilter hinzufügen"
label_work_package_filter_add: "Ticketfilter hinzufügen"
label_kind: "Art"
label_less_or_equal: "<="
label_log_costs: "Stückkosten buchen"

@ -3,7 +3,7 @@ en:
activerecord:
attributes:
cost_entry:
issue: "Issue"
work_package: "WorkPackage"
work_package: "Work package"
overridden_costs: "Overridden costs"
spent: "Spent"
@ -24,7 +24,7 @@ en:
cost_type:
unit: "Unit Name"
unit_plural: "Pluralized Unit Name"
issue:
work_package:
cost_object_subject: "Budget title"
labor_costs: "Labor costs"
material_costs: "Unit costs"
@ -121,7 +121,7 @@ en:
label_group_by_add: "Add grouping field"
label_hourly_rate: "Hourly rate"
label_include_deleted: "Include deleted"
label_issue_filter_add: "Add issue filter"
label_work_package_filter_add: "Add work package filter"
label_kind: "Type"
label_less_or_equal: "<="
label_log_costs: "Log unit costs"
@ -138,7 +138,7 @@ en:
label_view_all_cost_objects: "View all Budgets"
label_yes: "Yes"
notice_cost_object_conflict: "Issues must be of the same project."
notice_cost_object_conflict: "WorkPackages must be of the same project."
notice_no_cost_objects_available: "No budgets available."
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
@ -161,10 +161,10 @@ en:
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_cost_object_change_type_confirmation: "Are you sure? This operation will destroy information of the specific budget type."
text_destroy_cost_entries_question: "%{cost_entries} were reported on the issues you are about to delete. What do you want to do ?"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
text_destroy_time_and_cost_entries_question: "%{hours} hours, %{cost_entries} were reported on the issues you are about to delete. What do you want to do ?"
text_reassign_time_and_cost_entries: "Reassign reported hours and costs to this issue:"
text_destroy_time_and_cost_entries_question: "%{hours} hours, %{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_reassign_time_and_cost_entries: "Reassign reported hours and costs to this work package:"
text_warning_hidden_elements: "Some entries may have been excluded from the aggregation."
week: "week"

@ -29,7 +29,7 @@ class AddDenormalizedCostsFields < ActiveRecord::Migration
CostEntry.all.each {|e| e.update_costs!}
TimeEntry.all.each {|e| e.update_costs!}
Issue.all.each{|i| i.update_costs!}
Issue.all.each{|i| i.update_costs!} if @issues_table_exists
end
end
end

@ -34,7 +34,7 @@ class HigherPrecisionForCurrency < ActiveRecord::Migration
CostEntry.all.each {|e| e.update_costs!}
TimeEntry.all.each {|e| e.update_costs!}
Issue.all.each{|i| i.update_costs!}
Issue.all.each{|i| i.update_costs!} if @issue_table_exists
end
end
end

@ -0,0 +1,40 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
class AddCostObjectJournals < ActiveRecord::Migration
def change
create_table :cost_object_journals do |t|
t.integer :journal_id, :null => false
t.integer :project_id, :null => false
t.integer :author_id, :null => false
t.string :subject, :null => false
t.text :description, :null => false
t.date :fixed_date, :null => false
t.datetime :created_on
end
end
end

@ -0,0 +1,5 @@
class AddCostObjectIdToWorkPackageJournals < ActiveRecord::Migration
def change
add_column :work_package_journals, :cost_object_id, :integer, null: true
end
end

@ -2,7 +2,7 @@ Feature: Cost Object activities
Background:
Given there is a standard cost control project named "project1"
And I am already logged in as "admin"
And I am already admin
Scenario: cost object is a selectable activity type
When I go to the activity page of the project "project1"

@ -3,7 +3,7 @@ Feature: Cost type deletion
Background:
Given there is 1 cost type with the following:
| name | cost_type1 |
And I am already logged in as "admin"
And I am already admin
Scenario: Deleting a cost type
When I delete the cost type "cost_type1"

@ -3,10 +3,10 @@ Feature: Credit unit costs
Background:
Given there is a standard cost control project named "project1"
And the project "project1" has 1 issue with the following:
| subject | issue1 |
| subject | work_package1 |
And the role "Manager" may have the following rights:
| view_issues |
| edit_issues |
| view_work_packages |
| edit_work_packages |
| view_work_packages |
| edit_work_packages |
| log_costs |
@ -16,11 +16,11 @@ Feature: Credit unit costs
| unit_plural | multi_unit |
@javascript
Scenario: Crediting units costs to an issue
Scenario: Crediting units costs to an work_package
When I am already logged in as "manager"
And I go to the page of the issue "issue1"
And I go to the page of the issue "work_package1"
And I select "Log unit costs" from the action menu
And I fill in "cost_entry_units" with "100"
And I select "cost_type_1" from "Cost type"
And I press "Save"
Then I should be on the page of the issue "issue1"
Then I should be on the page of the issue "work_package1"

@ -2,28 +2,31 @@ Feature: Updating Hourly Rates
Background:
Given there is a standard cost control project named "project1"
And there is 1 user with:
| login | admin |
| admin | true |
And I am already logged in as "admin"
@javascript
Scenario: The project member has a hourly rate valid from today
Given there is an hourly rate with the following:
| project | project1 |
| user | admin |
| user | manager |
| valid_from | Date.today |
| rate | 20 |
When I go to the members tab of the settings page of the project "project1"
And I set the hourly rate of user "admin" to "30"
And I go to the hourly rates page of user "admin" of the project called "project1"
And I set the hourly rate of user "manager" to "30"
And I go to the hourly rates page of user "manager" of the project called "project1"
Then I should see 1 hourly rate
@javascript
Scenario: The project member does not have a hourly rate valid from today
Given there is an hourly rate with the following:
| project | project1 |
| user | admin |
| user | manager |
| valid_from | Date.today - 1 |
| rate | 20 |
When I go to the members tab of the settings page of the project "project1"
And I set the hourly rate of user "admin" to "30"
And I go to the hourly rates page of user "admin" of the project called "project1"
And I set the hourly rate of user "manager" to "30"
And I go to the hourly rates page of user "manager" of the project called "project1"
Then I should see 2 hourly rates

@ -3,7 +3,7 @@ Given /^the project "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |pro
as_admin count do
ce = CostEntry.generate
ce.project = p
ce.work_package = Issue.generate_for_project!(p)
ce.work_package = FactoryGirl.create(:work_package, project: p)
ce.save!
end
end
@ -34,7 +34,7 @@ end
Given /^the [Uu]ser "([^\"]*)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |user, count|
u = User.find_by_login user
p = u.projects.last
i = Issue.generate_for_project!(p)
i = FactoryGirl.create(:work_package, project: p)
as_admin count do
ce = FactoryGirl.create(:cost_entry)
ce.user = u
@ -46,7 +46,7 @@ end
Given /^the project "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the following:$/ do |project, count, table|
p = Project.find_by_name(project) || Project.find_by_identifier(project)
i = Issue.generate_for_project!(p)
i = FactoryGirl.create(:work_package, project: p)
as_admin count do
ce = CostEntry.generate
ce.project = p
@ -56,8 +56,8 @@ Given /^the project "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the
end
end
Given /^the issue "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the following:$/ do |issue, count, table|
i = Issue.find(:last, :conditions => ["subject = '#{issue}'"])
Given /^the work package "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the following:$/ do |work_package, count, table|
i = WorkPackage.find(:last, :conditions => ["subject = '#{work_package}'"])
as_admin count do
ce = FactoryGirl.build(:cost_entry, :spent_on => (table.rows_hash["date"] ? table.rows_hash["date"].to_date : Date.today),
:units => table.rows_hash["units"],
@ -77,16 +77,16 @@ Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
Given there is 1 project with the following:
| Name | #{name} |
| Identifier | #{name.gsub(' ', '_').downcase} |
And the project "#{name}" has the following trackers:
And the project "#{name}" has the following types:
| name |
| tracker1 |
| type1 |
And the project "#{name}" has 1 subproject
And the project "#{name}" has 1 issue with:
| subject | #{name}issue |
| subject | #{name}work_package |
And there is a role "Manager"
And the role "Manager" may have the following rights:
| view_own_hourly_rate |
| view_issues |
| view_work_packages |
| view_work_packages |
| view_own_time_entries |
| view_own_cost_entries |
@ -99,7 +99,7 @@ Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
| View own cost entries |
And there is a role "Reporter"
And the role "Reporter" may have the following rights:
| Create issues |
| Create work packages |
And there is a role "Supplier"
And the role "Supplier" may have the following rights:
| View own hourly rate |
@ -119,21 +119,21 @@ Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
}
end
Given /^users have times and the cost type "([^\"]*)" logged on the issue "([^\"]*)" with:$/ do |cost_type, issue, table|
i = Issue.find(:last, :conditions => ["subject = '#{issue}'"])
raise "No such issue: #{issue}" unless i
Given /^users have times and the cost type "([^\"]*)" logged on the work package "([^\"]*)" with:$/ do |cost_type, work_package, table|
i = WorkPackage.find(:last, :conditions => ["subject = '#{work_package}'"])
raise "No such work_package: #{work_package}" unless i
table.rows_hash.collect do |k,v|
user = k.split.first
if k.end_with? "hours"
steps %Q{
And the issue "#{issue}" has 1 time entry with the following:
And the issue "#{work_package}" has 1 time entry with the following:
| hours | #{v} |
| user | #{user} |
}
elsif k.end_with? "units"
steps %Q{
And the issue "#{issue}" has 1 cost entry with the following:
And the issue "#{work_package}" has 1 cost entry with the following:
| units | #{v} |
| user | #{user} |
| cost type | #{cost_type} |

@ -5,60 +5,60 @@ Feature: Permission View Own hourly and cost rates
Given there is a standard cost control project named "Standard Project"
And the role "Supplier" may have the following rights:
| view_own_hourly_rate |
| view_issues |
| view_work_packages |
| view_work_packages |
| view_own_time_entries |
| view_own_cost_entries |
| view_cost_rates |
| log_costs |
And there is 1 User with:
| Login | testuser |
| Firstname | Bob |
| Lastname | Bobbit |
| Login | testuser |
| Firstname | Bob |
| Lastname | Bobbit |
| default rate | 10.00 |
And the user "testuser" is a "Supplier" in the project "Standard Project"
And the project "Standard Project" has 1 issue with the following:
| subject | test_issue |
And the issue "test_issue" has 1 time entry with the following:
| subject | test_work_package |
And the issue "test_work_package" has 1 time entry with the following:
| hours | 1.00 |
| user | testuser |
And there is 1 cost type with the following:
| name | Translation |
| cost rate | 7.00 |
And the issue "test_issue" has 1 cost entry with the following:
And the work package "test_work_package" has 1 cost entry with the following:
| units | 2.00 |
| user | testuser |
| cost type | Translation |
| cost type | Translation |
And the user "manager" has:
| hourly rate | 11.00 |
And the issue "test_issue" has 1 time entry with the following:
| hours | 3.00 |
| user | manager |
And the issue "test_issue" has 1 cost entry with the following:
| units | 5.00 |
| user | manager |
| cost type | Translation |
| hourly rate | 11.00 |
And the issue "test_work_package" has 1 time entry with the following:
| hours | 3.00 |
| user | manager |
And the work package "test_work_package" has 1 cost entry with the following:
| units | 5.00 |
| user | manager |
| cost type | Translation |
And I am already logged in as "testuser"
And I am on the page for the issue "test_issue"
And I am on the page for the issue "test_work_package"
Then I should see "1.00 hour"
And I should see "2.0 Translations"
And I should see "24.00 EUR"
And I should not see "33.00 EUR" # labour costs only of Manager
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager
And I am on the issues page for the project called "Standard Project"
And I should see "2.0 Translations"
And I should see "24.00 EUR"
And I should not see "33.00 EUR" # labour costs only of Manager
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager
And I am on the work_packages page for the project called "Standard Project"
And I toggle the Options fieldset
And I select to see columns
And I select to see columns
| Overall costs |
| Labor costs |
| Unit costs |
And I follow "Apply"
Then I should see "24.00 EUR"
Then I should see "24.00 EUR"
And I should see "10.00 EUR"
And I should see "14.00 EUR"
And I should not see "33.00 EUR" # labour costs only of Manager
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager
And I should not see "33.00 EUR" # labour costs only of Manager
And I should not see "35.00 EUR" # material costs only of Manager
And I should not see "43.00 EUR" # labour costs of me and Manager
And I should not see "49.00 EUR" # material costs of me and Manager

@ -9,10 +9,10 @@ module OpenProject::Costs
end
initializer "costs.register_hooks" do
require 'open_project/costs/hooks'
require 'open_project/costs/hooks/issue_hook'
require 'open_project/costs/hooks/project_hook'
require 'open_project/costs/hooks/work_package_action_menu'
require_dependency 'open_project/costs/hooks'
require_dependency 'open_project/costs/hooks/work_package_hook'
require_dependency 'open_project/costs/hooks/project_hook'
require_dependency 'open_project/costs/hooks/work_package_action_menu'
end
config.autoload_paths += Dir["#{config.root}/lib/"]
@ -32,7 +32,7 @@ module OpenProject::Costs
initializer 'costs.register_observers' do |app|
# Observers
ActiveRecord::Base.observers.push :rate_observer, :default_hourly_rate_observer, :costs_issue_observer
ActiveRecord::Base.observers.push :rate_observer, :default_hourly_rate_observer, :costs_work_package_observer
end
config.before_configuration do |app|
@ -66,7 +66,6 @@ module OpenProject::Costs
# Model Patches
require_dependency 'open_project/costs/patches/work_package_patch'
require_dependency 'open_project/costs/patches/issue_patch'
require_dependency 'open_project/costs/patches/project_patch'
require_dependency 'open_project/costs/patches/query_patch'
require_dependency 'open_project/costs/patches/user_patch'
@ -76,17 +75,15 @@ module OpenProject::Costs
# Controller Patchesopen_project/costs/patches/
require_dependency 'open_project/costs/patches/application_controller_patch'
require_dependency 'open_project/costs/patches/issues_controller_patch'
require_dependency 'open_project/costs/patches/work_packages_controller_patch'
require_dependency 'open_project/costs/patches/projects_controller_patch'
# Helper Patches
require_dependency 'open_project/costs/patches/application_helper_patch'
require_dependency 'open_project/costs/patches/users_helper_patch'
require_dependency 'open_project/costs/patches/issues_helper_patch'
require_dependency 'open_project/costs/patches/work_packages_helper_patch'
require_dependency 'open_project/costs/patches/issue_observer'
require_dependency 'open_project/costs/patches/work_package_observer'
# loading the class so that acts_as_journalized gets registered
VariableCostObject
@ -178,8 +175,8 @@ module OpenProject::Costs
end
config.after_initialize do
# We are overwriting issues/_action_menu.html.erb so our view must be found first
IssuesController.view_paths.unshift("#{config.root}/app/views")
# We are overwriting work_packages/_action_menu.html.erb so our view must be found first
WorkPackagesController.view_paths.unshift("#{config.root}/app/views")
end
end

@ -1,4 +1,4 @@
# Hooks to attach to the OpenProject action menu.
class OpenProject::Costs::Hooks::WorkPackageActionMenuHook < Redmine::Hook::ViewListener
render_on :view_work_package_show_action_menu, :partial => 'hooks/view_work_package_show_action_menu'
render_on :view_issues_show_action_menu, :partial => 'hooks/view_work_package_show_action_menu'
end

@ -1,58 +1,58 @@
# Hooks to attach to the Redmine Issues.
class OpenProject::Costs::Hooks::IssueHook < Redmine::Hook::ViewListener
# Hooks to attach to the Redmine WorkPackages.
class OpenProject::Costs::Hooks::WorkPackageHook < Redmine::Hook::ViewListener
# Renders the Cost Object subject and basic costs information
render_on :view_issues_show_details_bottom, :partial => 'hooks/view_issues_show_details_bottom'
# render_on :view_issues_show_details_bottom, :partial => 'hooks/view_work_packages_show_details_bottom'
# Renders a select tag with all the Cost Objects
render_on :view_issues_form_details_bottom, :partial => 'hooks/view_issues_form_details_bottom'
render_on :view_issues_form_details_bottom, :partial => 'hooks/view_work_packages_form_details_bottom'
# Renders a select tag with all the Cost Objects for the bulk edit page
render_on :view_issues_bulk_edit_details_bottom, :partial => 'hooks/view_issues_bulk_edit_details_bottom'
render_on :view_issues_bulk_edit_details_bottom, :partial => 'hooks/view_work_packages_bulk_edit_details_bottom'
render_on :view_issues_move_bottom, :partial => 'hooks/view_issues_move_bottom'
render_on :view_issues_move_bottom, :partial => 'hooks/view_work_packages_move_bottom'
render_on :view_issues_context_menu_end, :partial => 'hooks/view_issues_context_menu_end'
render_on :view_issues_context_menu_end, :partial => 'hooks/view_work_packages_context_menu_end'
# Updates the cost object after a move
#
# Context:
# * params => Request parameters
# * issue => Issue to move
# * work_package => WorkPackage to move
# * target_project => Target of the move
# * copy => true, if the issues are copied rather than moved
def controller_issues_move_before_save(context={})
# FIXME: In case of copy==true, this will break stuff if the original issue is saved
# * copy => true, if the work_packages are copied rather than moved
def controller_work_packages_move_before_save(context={})
# FIXME: In case of copy==true, this will break stuff if the original work_package is saved
cost_object_id = context[:params] && context[:params][:cost_object_id]
case cost_object_id
when "" # a.k.a "(No change)"
# cost objects HAVE to be changed if move is performed across project boundaries
# as the are project specific
context[:issue].cost_object_id = nil unless (context[:issue].project == context[:target_project])
context[:work_package].cost_object_id = nil unless (context[:work_package].project == context[:target_project])
when "none"
context[:issue].cost_object_id = nil
context[:work_package].cost_object_id = nil
else
context[:issue].cost_object_id = cost_object_id
context[:work_package].cost_object_id = cost_object_id
end
end
# Saves the Cost Object assignment to the issue
# Saves the Cost Object assignment to the work_package
#
# Context:
# * :issue => Issue being saved
# * :work_package => WorkPackage being saved
# * :params => HTML parameters
#
def controller_issues_bulk_edit_before_save(context = { })
def controller_work_packages_bulk_edit_before_save(context = { })
case true
when context[:params][:cost_object_id].blank?
# Do nothing
when context[:params][:cost_object_id] == 'none'
# Unassign cost_object
context[:issue].cost_object = nil
context[:work_package].cost_object = nil
else
context[:issue].cost_object = CostObject.find(context[:params][:cost_object_id])
context[:work_package].cost_object = CostObject.find(context[:params][:cost_object_id])
end
return ''
@ -64,7 +64,7 @@ class OpenProject::Costs::Hooks::IssueHook < Redmine::Hook::ViewListener
# Context:
# * :detail => Detail about the journal change
#
def helper_issues_show_detail_after_setting(context = { })
def helper_work_packages_show_detail_after_setting(context = { })
# FIXME: Overwritting the caller is bad juju
if (context[:detail].prop_key == 'cost_object_id')
if context[:detail].value.to_i.to_s == context[:detail].value.to_s

@ -1,21 +0,0 @@
require_dependency 'issue'
class CostsIssueObserver < ActiveRecord::Observer
unloadable
observe :issue
def after_update(issue)
if issue.project_id_changed?
# TODO: This only works with the global cost_rates
CostEntry.update_all({:project_id => issue.project_id}, {:work_package_id => issue.id})
end
end
def before_update(issue)
# FIXME: remove this method once controller_issues_move_before_save is in 0.9-stable
if issue.project_id_changed? && issue.cost_object_id && !issue.project.cost_object_ids.include?(issue.cost_object_id)
issue.cost_object = nil
end
# true
end
end

@ -1,27 +0,0 @@
require 'issue'
module OpenProject::Costs::Patches::IssuePatch
def self.included(base) # :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
# Same as typing in the class
base.class_eval do
unloadable
belongs_to :cost_object, :inverse_of => :work_packages
has_many :cost_entries, :foreign_key => :work_package_id, :dependent => :delete_all
end
end
module ClassMethods
end
module InstanceMethods
end
end
Issue.send(:include, OpenProject::Costs::Patches::IssuePatch)

@ -1,77 +0,0 @@
require_dependency 'issues_controller'
module OpenProject::Costs::Patches::IssuesControllerPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
alias_method_chain :show, :entries
alias_method_chain :destroy, :entries
helper :issues
end
end
module InstanceMethods
# Authorize the user for the requested action
def show_with_entries
@cost_entries = @issue.cost_entries.visible(User.current, @issue.project)
cost_entries_with_rate = @cost_entries.select{|c| c.costs_visible_by?(User.current)}
@material_costs = cost_entries_with_rate.blank? ? nil : cost_entries_with_rate.collect(&:real_costs).sum
@time_entries = @issue.time_entries.visible(User.current, @issue.project)
time_entries_with_rate = @time_entries.select{|c| c.costs_visible_by?(User.current)}
@labor_costs = time_entries_with_rate.blank? ? nil : time_entries_with_rate.collect(&:real_costs).sum
unless @material_costs.nil? && @labor_costs.nil?
@overall_costs = 0
@overall_costs += @material_costs unless @material_costs.nil?
@overall_costs += @labor_costs unless @labor_costs.nil?
else
@overall_costs = nil
end
show_without_entries
end
def destroy_with_entries
@entries = CostEntry.all(:conditions => ['work_package_id IN (?)', @issues])
@hours = TimeEntry.sum(:hours, :conditions => ['work_package_id IN (?)', @issues]).to_f
unless @entries.blank? && @hours == 0
case params[:todo]
when 'destroy'
# nothing to do
when 'nullify'
TimeEntry.update_all('work_package_id = NULL', ['work_package_id IN (?)', @issues])
CostEntry.update_all('work_package_id = NULL', ['work_package_id IN (?)', @issues])
when 'reassign'
reassign_to = @project.work_packages.find_by_id(params[:reassign_to_id])
if reassign_to.nil?
flash.now[:error] = l(:error_issue_not_found_in_project)
return
else
TimeEntry.update_all("work_package_id = #{reassign_to.id}", ['work_package_id IN (?)', @issues])
CostEntry.update_all("work_package_id = #{reassign_to.id}", ['work_package_id IN (?)', @issues])
end
else
# display the destroy form if it's a user request
return unless api_request?
end
end
@issues.each do |issue|
begin
issue.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
# nothing to do, issue was already deleted (eg. by a parent)
end
end
respond_to do |format|
format.html { redirect_to :action => 'index', :project_id => @project }
format.api { head :ok }
end
end
end
end
IssuesController.send(:include, OpenProject::Costs::Patches::IssuesControllerPatch)

@ -1,89 +0,0 @@
require_dependency 'issues_helper'
module OpenProject::Costs::Patches::IssuesHelperPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
# Same as typing in the class
base.class_eval do
def summarized_cost_entries(cost_entries, create_link=true)
last_cost_type = ""
return "-" 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
str_array = []
result.each do |k, v|
txt = pluralize(v[:units], v[:unit], v[:unit_plural])
if create_link
# TODO why does this have project_id, issue_id and cost_type_id params?
str_array << link_to(txt, { :controller => '/costlog',
:action => 'index',
:project_id => @work_package.project,
:work_package_id => @work_package,
:cost_type_id => k },
{ :title => k.name })
else
str_array << "<span title=\"#{h(k.name)}\">#{txt}</span>"
end
end
str_array.join(", ").html_safe
end
def cost_issues_attributes(work_package)
attributes = []
object_value = if work_package.cost_object.nil?
"-"
else
link_to_cost_object(work_package.cost_object)
end
attributes << [CostObject.model_name.human, object_value]
if User.current.allowed_to?(:view_time_entries, @project) ||
User.current.allowed_to?(:view_own_time_entries, @project)
#TODO: put inside controller or model
summed_hours = @time_entries.sum(&:hours)
value = summed_hours > 0 ?
link_to(l_hours(summed_hours), issue_time_entries_path(work_package)) : "-"
attributes << [Issue.human_attribute_name(:spent_hours), value]
end
unless @overall_costs.nil?
attributes << [Issue.human_attribute_name(:overall_costs), number_to_currency(@overall_costs)]
end
if User.current.allowed_to?(:view_cost_entries, @project) ||
User.current.allowed_to?(:view_own_cost_entries, @project)
attributes << [Issue.human_attribute_name(:spent_units), summarized_cost_entries(@cost_entries)]
end
attributes
end
end
end
module InstanceMethods
end
end
IssuesHelper.send(:include, OpenProject::Costs::Patches::IssuesHelperPatch)

@ -13,7 +13,7 @@ module OpenProject::Costs::Patches::ProjectsControllerPatch
module InstanceMethods
def own_total_hours
if User.current.allowed_to?(:view_own_time_entries, @project)
cond = @project.project_condition(Setting.display_subprojects_issues?)
cond = @project.project_condition(Setting.display_subprojects_work_packages?)
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
end
end

@ -6,12 +6,12 @@ module OpenProject::Costs::Patches::QueryPatch
include ActionView::Helpers::NumberHelper
alias :super_value :value
def value(issue)
number_to_currency(issue.send(name))
def value(work_package)
number_to_currency(work_package.send(name))
end
def real_value(issue)
super_value issue
def real_value(work_package)
super_value work_package
end
end

@ -46,9 +46,9 @@ module OpenProject::Costs::Patches::TimeEntryPatch
module ClassMethods
def update_all(updates, conditions = nil, options = {})
# instead of a update_all, perform an individual update during issue#move
# instead of a update_all, perform an individual update during work_package#move
# to trigger the update of the costs based on new rates
if conditions.respond_to?(:keys) && conditions.keys == [:issue_id] && updates =~ /^project_id = ([\d]+)$/
if conditions.respond_to?(:keys) && conditions.keys == [:work_package_id] && updates =~ /^project_id = ([\d]+)$/
project_id = $1
time_entries = TimeEntry.all(:conditions => conditions)
time_entries.each do |entry|

@ -12,7 +12,7 @@ module OpenProject::Costs::Patches::VersionPatch
module InstanceMethods
def spent_hours_with_inheritance
# overwritten method
@spent_hours ||= TimeEntry.visible.sum(:hours, :include => :work_package, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
@spent_hours ||= TimeEntry.visible.sum(:hours, :include => :work_package, :conditions => ["#{WorkPackage.table_name}.fixed_version_id = ?", id]).to_f
end
end
end

@ -0,0 +1,21 @@
require_dependency 'work_package'
class CostsWorkPackageObserver < ActiveRecord::Observer
unloadable
observe :work_package
def after_update(work_package)
if work_package.project_id_changed?
# TODO: This only works with the global cost_rates
CostEntry.update_all({:project_id => work_package.project_id}, {:work_package_id => work_package.id})
end
end
def before_update(work_package)
# FIXME: remove this method once controller_work_packages_move_before_save is in 0.9-stable
if work_package.project_id_changed? && work_package.cost_object_id && !work_package.project.cost_object_ids.include?(work_package.cost_object_id)
work_package.cost_object = nil
end
# true
end
end

@ -41,9 +41,9 @@ module OpenProject::Costs::Patches::WorkPackagePatch
module InstanceMethods
def validate_cost_object
if cost_object_id_changed?
unless (cost_object_id.blank? || project.cost_object_ids.include?(cost_object_id))
errors.add :cost_object_id, :invalid
if cost_object && cost_object.changed?
unless (cost_object.blank? || project.cost_object_ids.include?(cost_object.id))
errors.add :cost_object, :invalid
end
end
end

@ -6,8 +6,9 @@ module OpenProject::Costs::Patches::WorkPackagesControllerPatch
base.class_eval do
alias_method_chain :show, :entries
alias_method_chain :destroy, :entries
helper :issues
helper :work_packages
end
end
@ -33,6 +34,43 @@ module OpenProject::Costs::Patches::WorkPackagesControllerPatch
show_without_entries
end
def destroy_with_entries
@entries = CostEntry.all(:conditions => ['work_package_id IN (?)', @work_packages])
@hours = TimeEntry.sum(:hours, :conditions => ['work_package_id IN (?)', @work_packages]).to_f
unless @entries.blank? && @hours == 0
case params[:todo]
when 'destroy'
# nothing to do
when 'nullify'
TimeEntry.update_all('work_package_id = NULL', ['work_package_id IN (?)', @work_packages])
CostEntry.update_all('work_package_id = NULL', ['work_package_id IN (?)', @work_packages])
when 'reassign'
reassign_to = @project.work_packages.find_by_id(params[:reassign_to_id])
if reassign_to.nil?
flash.now[:error] = l(:error_work_package_not_found_in_project)
return
else
TimeEntry.update_all("work_package_id = #{reassign_to.id}", ['work_package_id IN (?)', @work_packages])
CostEntry.update_all("work_package_id = #{reassign_to.id}", ['work_package_id IN (?)', @work_packages])
end
else
# display the destroy form if it's a user request
return unless api_request?
end
end
@work_packages.each do |work_package|
begin
work_package.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if work_package no longer exists
# nothing to do, work_package was already deleted (eg. by a parent)
end
end
respond_to do |format|
format.html { redirect_to :action => 'index', :project_id => @project }
format.api { head :ok }
end
end
end
end

@ -7,44 +7,74 @@ module OpenProject::Costs::Patches::WorkPackagesHelperPatch
attributes = []
attributes << work_package_show_table_row(:cost_object) do
work_package.cost_object ?
link_to_cost_object(work_package.cost_object) :
"-"
end
work_package.cost_object ?
link_to_cost_object(work_package.cost_object) :
"-"
end
if User.current.allowed_to?(:view_time_entries, @project) ||
User.current.allowed_to?(:view_own_time_entries, @project)
User.current.allowed_to?(:view_own_time_entries, @project)
attributes << work_package_show_table_row(:spent_hours) do
#TODO: put inside controller or model
summed_hours = @time_entries.sum(&:hours)
# TODO: put inside controller or model
summed_hours = @time_entries.sum(&:hours)
summed_hours > 0 ?
link_to(l_hours(summed_hours), issue_time_entries_path(work_package)) :
"-"
end
summed_hours > 0 ?
link_to(l_hours(summed_hours), work_package_time_entries_path(work_package)) :
"-"
end
end
attributes << work_package_show_table_row(:overall_costs) do
@overall_costs.nil? ?
"-" :
number_to_currency(@overall_costs)
end
@overall_costs.nil? ?
"-" :
number_to_currency(@overall_costs)
end
if User.current.allowed_to?(:view_cost_entries, @project) ||
User.current.allowed_to?(:view_own_cost_entries, @project)
User.current.allowed_to?(:view_own_cost_entries, @project)
attributes << work_package_show_table_row(:spent_units) do
summarized_cost_entries(@cost_entries, work_package)
end
end
summed_costs = summarized_cost_entries(@cost_entries)
attributes
end
#summed_costs > 0 ?
summed_costs# :
# "-"
end
def summarized_cost_entries(cost_entries, work_package, create_link=true)
last_cost_type = ""
return "-" 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
attributes
str_array = []
result.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?
str_array << link_to(txt, { :controller => '/costlog',
:action => 'index',
:project_id => work_package.project,
:work_package_id => work_package,
:cost_type_id => k },
{ :title => k.name })
else
str_array << "<span title=\"#{h(k.name)}\">#{txt}</span>"
end
end
str_array.join(", ").html_safe
end
def work_package_show_attributes_with_costs(work_package)

@ -11,9 +11,9 @@ Gem::Specification.new do |s|
s.email = "info@finn.de"
s.homepage = "http://www.finn.de"
s.summary = "A OpenProject plugin to manage costs"
s.description = "This plugin allows to track labor and units cost associated with issues."
s.description = "This plugin allows to track labor and units cost associated with work packages."
s.files = Dir["{app,config,db,lib}/**/*", "Rakefile", "README.rdoc"]
s.files = Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "README.md"]
s.test_files = Dir["spec/**/*"]
s.add_dependency "rails", "~> 3.2.9"

@ -2,22 +2,22 @@ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper.rb")
describe CostlogController do
include Cost::PluginSpecHelper
let (:project) { FactoryGirl.create(:project_with_trackers) }
let (:issue) { FactoryGirl.create(:issue, :project => project,
let (:project) { FactoryGirl.create(:project_with_types) }
let (:work_package) { FactoryGirl.create(:work_package, :project => project,
:author => user,
:tracker => project.trackers.first) }
:type => project.types.first) }
let (:user) { FactoryGirl.create(:user) }
let (:user2) { FactoryGirl.create(:user) }
let (:controller) { FactoryGirl.build(:role, :permissions => [:log_costs, :edit_cost_entries]) }
let (:cost_type) { FactoryGirl.build(:cost_type) }
let (:cost_entry) { FactoryGirl.build(:cost_entry, :work_package => issue,
let (:cost_entry) { FactoryGirl.build(:cost_entry, :work_package => work_package,
:project => project,
:spent_on => Date.today,
:overridden_costs => 400,
:units => 100,
:user => user,
:comments => "") }
let(:issue_status) { FactoryGirl.create(:issue_status, :is_default => true) }
let(:work_package_status) { FactoryGirl.create(:work_package_status, :is_default => true) }
def grant_current_user_permissions user, permissions
member = FactoryGirl.build(:member, :project => project,
@ -35,7 +35,7 @@ describe CostlogController do
shared_examples_for "assigns" do
it { assigns(:cost_entry).project.should == expected_project }
it { assigns(:cost_entry).work_package.should == expected_issue }
it { assigns(:cost_entry).work_package.should == expected_work_package }
it { assigns(:cost_entry).user.should == expected_user }
it { assigns(:cost_entry).spent_on.should == expected_spent_on }
it { assigns(:cost_entry).cost_type.should == expected_cost_type }
@ -53,10 +53,10 @@ describe CostlogController do
end
describe "GET new" do
let(:params) { { "work_package_id" => issue.id.to_s } }
let(:params) { { "work_package_id" => work_package.id.to_s } }
let(:expected_project) { project }
let(:expected_issue) { issue }
let(:expected_work_package) { work_package }
let(:expected_user) { user }
let(:expected_spent_on) { Date.today }
let(:expected_cost_type) { nil }
@ -199,9 +199,9 @@ describe CostlogController do
before do
grant_current_user_permissions user, [:edit_cost_entries]
cost_entry.project = FactoryGirl.create(:project_with_trackers)
cost_entry.work_package = FactoryGirl.create(:issue, :project => cost_entry.project,
:tracker => cost_entry.project.trackers.first,
cost_entry.project = FactoryGirl.create(:project_with_types)
cost_entry.work_package = FactoryGirl.create(:work_package, :project => cost_entry.project,
:type => cost_entry.project.types.first,
:author => user)
cost_entry.save!
end
@ -226,14 +226,14 @@ describe CostlogController do
describe "POST create" do
let (:params) { { "project_id" => project.id.to_s,
"cost_entry" => { "user_id" => user.id.to_s,
"work_package_id" => (issue.present? ? issue.id.to_s : "") ,
"work_package_id" => (work_package.present? ? work_package.id.to_s : "") ,
"units" => units.to_s,
"cost_type_id" => (cost_type.present? ? cost_type.id.to_s : "" ),
"comments" => "lorem",
"spent_on" => date.to_s,
"overridden_costs" => overridden_costs.to_s } } }
let(:expected_project) { project }
let(:expected_issue) { issue }
let(:expected_work_package) { work_package }
let(:expected_user) { user }
let(:expected_overridden_costs) { overridden_costs }
let(:expected_spent_on) { date }
@ -254,7 +254,7 @@ describe CostlogController do
post :create, params
end
# is this really usefull, shouldn't it redirect to the creating issue by default?
# is this really usefull, shouldn't it redirect to the creating work_package by default?
it { response.should redirect_to(:controller => "costlog", :action => "index", :project_id => project) }
it { assigns(:cost_entry).should_not be_new_record }
it_should_behave_like "assigns"
@ -409,27 +409,27 @@ describe CostlogController do
end
describe "WHEN the user is allowed to create cost_entries
WHEN the id of an issue not included in the provided project is provided" do
WHEN the id of an work_package not included in the provided project is provided" do
let(:project2) { FactoryGirl.create(:project_with_trackers) }
let(:issue2) { FactoryGirl.create(:issue, :project => project2,
:tracker => project2.trackers.first,
let(:project2) { FactoryGirl.create(:project_with_types) }
let(:work_package2) { FactoryGirl.create(:work_package, :project => project2,
:type => project2.types.first,
:author => user) }
let(:expected_issue) { issue2 }
let(:expected_work_package) { work_package2 }
before do
grant_current_user_permissions user, [:log_costs]
params["cost_entry"]["work_package_id"] = issue2.id
params["cost_entry"]["work_package_id"] = work_package2.id
end
it_should_behave_like "invalid create"
end
describe "WHEN the user is allowed to create cost_entries
WHEN no issue_id is provided" do
WHEN no work_package_id is provided" do
let(:expected_issue) { nil }
let(:expected_work_package) { nil }
before do
grant_current_user_permissions user, [:log_costs]
@ -476,7 +476,7 @@ describe CostlogController do
cost_entry.save(:validate => false)
end
let(:expected_issue) { cost_entry.work_package }
let(:expected_work_package) { cost_entry.work_package }
let(:expected_user) { cost_entry.user }
let(:expected_project) { cost_entry.project }
let(:expected_cost_type) { cost_entry.cost_type }
@ -514,15 +514,15 @@ describe CostlogController do
describe "WHEN the user is allowed to update cost_entries
WHEN updating:
issue_id
work_package_id
user_id
units
cost_type
overridden_costs
spent_on" do
let(:expected_issue) { FactoryGirl.create(:issue, :project => project,
:tracker => project.trackers.first,
let(:expected_work_package) { FactoryGirl.create(:work_package, :project => project,
:type => project.types.first,
:author => user) }
let(:expected_user) { FactoryGirl.create(:user) }
let(:expected_spent_on) { cost_entry.spent_on + 4.days }
@ -534,7 +534,7 @@ describe CostlogController do
grant_current_user_permissions expected_user, []
grant_current_user_permissions user, [:edit_cost_entries]
params["cost_entry"]["work_package_id"] = expected_issue.id.to_s
params["cost_entry"]["work_package_id"] = expected_work_package.id.to_s
params["cost_entry"]["user_id"] = expected_user.id.to_s
params["cost_entry"]["spent_on"] = expected_spent_on.to_s
params["cost_entry"]["units"] = expected_units.to_s
@ -585,33 +585,33 @@ describe CostlogController do
end
describe "WHEN the user is allowed to update cost_entries
WHEN updating the issue
WHEN the new issue isn't an issue of the current project" do
WHEN updating the work_package
WHEN the new work_package isn't an work_package of the current project" do
let(:project2) { FactoryGirl.create(:project_with_trackers) }
let(:issue2) { FactoryGirl.create(:issue, :project => project2,
:tracker => project2.trackers.first) }
let(:expected_issue) { issue2 }
let(:project2) { FactoryGirl.create(:project_with_types) }
let(:work_package2) { FactoryGirl.create(:work_package, :project => project2,
:type => project2.types.first) }
let(:expected_work_package) { work_package2 }
before do
grant_current_user_permissions user, [:edit_cost_entries]
params["cost_entry"]["work_package_id"] = issue2.id.to_s
params["cost_entry"]["work_package_id"] = work_package2.id.to_s
end
it_should_behave_like "invalid update"
end
describe "WHEN the user is allowed to update cost_entries
WHEN updating the issue
WHEN the new issue_id isn't existing" do
WHEN updating the work_package
WHEN the new work_package_id isn't existing" do
let(:expected_issue) { nil }
let(:expected_work_package) { nil }
before do
grant_current_user_permissions user, [:edit_cost_entries]
params["cost_entry"]["work_package_id"] = (issue.id + 1).to_s
params["cost_entry"]["work_package_id"] = (work_package.id + 1).to_s
end
it_should_behave_like "invalid update"

@ -2,7 +2,7 @@ FactoryGirl.define do
factory :cost_entry do
project
user { FactoryGirl.create(:user, :member_in_project => project)}
work_package { FactoryGirl.create(:issue, :project => project) }
work_package { FactoryGirl.create(:work_package, :project => project) }
cost_type
spent_on Date.today
units 1

@ -3,13 +3,13 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe CostEntry do
include Cost::PluginSpecHelper
let(:project) { FactoryGirl.create(:project_with_trackers) }
let(:project2) { FactoryGirl.create(:project_with_trackers) }
let(:issue) { FactoryGirl.create(:issue, :project => project,
:tracker => project.trackers.first,
let(:project) { FactoryGirl.create(:project_with_types) }
let(:project2) { FactoryGirl.create(:project_with_types) }
let(:work_package) { FactoryGirl.create(:work_package, :project => project,
:type => project.types.first,
:author => user) }
let(:issue2) { FactoryGirl.create(:issue, :project => project2,
:tracker => project2.trackers.first,
let(:work_package2) { FactoryGirl.create(:work_package, :project => project2,
:type => project2.types.first,
:author => user) }
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
@ -18,7 +18,7 @@ describe CostEntry do
member
FactoryGirl.build(:cost_entry, :cost_type => cost_type,
:project => project,
:work_package => issue,
:work_package => work_package,
:spent_on => date,
:units => units,
:user => user,
@ -28,7 +28,7 @@ describe CostEntry do
let(:cost_entry2) do
FactoryGirl.build(:cost_entry, :cost_type => cost_type,
:project => project,
:work_package => issue,
:work_package => work_package,
:spent_on => date,
:units => units,
:user => user,
@ -237,7 +237,7 @@ describe CostEntry do
describe "WHEN no project is provided" do
before do
cost_entry.project = nil
# unfortunately the project get's set to the issue's project if no project is provided
# unfortunately the project get's set to the work_package's project if no project is provided
# TODO: check if that is necessary
cost_entry.work_package = nil
end
@ -245,14 +245,14 @@ describe CostEntry do
it { cost_entry.should_not be_valid }
end
describe "WHEN no issue is provided" do
describe "WHEN no work_package is provided" do
before { cost_entry.work_package = nil }
it { cost_entry.should_not be_valid }
end
describe "WHEN the issue is not in the project" do
before { cost_entry.work_package = issue2 }
describe "WHEN the work_package is not in the project" do
before { cost_entry.work_package = work_package2 }
it { cost_entry.should_not be_valid }
end

@ -1,37 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Issue do
let(:user) { FactoryGirl.create(:admin)}
let(:role) { FactoryGirl.create(:role) }
let(:project) do
project = FactoryGirl.create(:project_with_trackers)
project.add_member!(user, role)
project
end
let(:project2) { FactoryGirl.create(:project_with_trackers) }
let(:issue) { FactoryGirl.create(:issue, :project => project,
:tracker => project.trackers.first,
:author => user) }
let!(:cost_entry) { FactoryGirl.create(:cost_entry, work_package: issue, project: project, units: 3, spent_on: Date.today, user: user, comments: "test entry") }
let!(:cost_object) { FactoryGirl.create(:cost_object, project: project) }
before(:each) do
User.stub!(:current).and_return(user)
end
it "should update cost entries on move" do
issue.project_id.should eql project.id
issue.move_to_project(project2).should_not be_false
cost_entry.reload.project_id.should eql project2.id
end
it "should allow to set cost_object to nil" do
issue.cost_object = cost_object
issue.save!
issue.cost_object.should eql cost_object
issue.cost_object = nil
lambda { issue.save! }.should_not raise_error(ActiveRecord::RecordInvalid)
end
end

@ -2,13 +2,13 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe TimeEntry do
include Cost::PluginSpecHelper
let(:project) { FactoryGirl.create(:project_with_trackers, is_public: false) }
let(:project2) { FactoryGirl.create(:project_with_trackers, is_public: false) }
let(:issue) { FactoryGirl.create(:issue, :project => project,
:tracker => project.trackers.first,
let(:project) { FactoryGirl.create(:project_with_types, is_public: false) }
let(:project2) { FactoryGirl.create(:project_with_types, is_public: false) }
let(:work_package) { FactoryGirl.create(:work_package, :project => project,
:type => project.types.first,
:author => user) }
let(:issue2) { FactoryGirl.create(:issue, :project => project2,
:tracker => project2.trackers.first,
let(:work_package2) { FactoryGirl.create(:work_package, :project => project2,
:type => project2.types.first,
:author => user2) }
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
@ -23,7 +23,7 @@ describe TimeEntry do
let(:hours) { 5.0 }
let(:time_entry) do
FactoryGirl.create(:time_entry, :project => project,
:work_package => issue,
:work_package => work_package,
:spent_on => date,
:hours => hours,
:user => user,
@ -33,7 +33,7 @@ describe TimeEntry do
let(:time_entry2) do
FactoryGirl.create(:time_entry, :project => project,
:work_package => issue,
:work_package => work_package,
:spent_on => date,
:hours => hours,
:user => user,

@ -11,6 +11,10 @@ describe User, "#destroy" do
user2
end
after do
User.current = nil
end
shared_examples_for "costs updated journalized associated object" do
before do
User.current = user2
@ -39,13 +43,13 @@ describe User, "#destroy" do
it { associated_instance.journals.first.user.should == user2 }
it "should update first journal changed_data" do
associations.each do |association|
associated_instance.journals.first.changed_data[association.to_s + "_id"].last.should == user2.id
associated_instance.journals.first.changed_data["#{association}_id".to_sym].last.should == user2.id
end
end
it { associated_instance.journals.last.user.should == substitute_user }
it "should update second journal changed_data" do
associations.each do |association|
associated_instance.journals.last.changed_data[association.to_s + "_id"].last.should == substitute_user.id
associated_instance.journals.last.changed_data["#{association}_id".to_sym].last.should == substitute_user.id
end
end
end
@ -78,14 +82,14 @@ describe User, "#destroy" do
it { associated_instance.journals.first.user.should == substitute_user }
it "should update the first journal" do
associations.each do |association|
associated_instance.journals.first.changed_data[association.to_s + "_id"].last.should == substitute_user.id
associated_instance.journals.first.changed_data["#{association}_id".to_sym].last.should == substitute_user.id
end
end
it { associated_instance.journals.last.user.should == user2 }
it "should update the last journal" do
associations.each do |association|
associated_instance.journals.last.changed_data[association.to_s + "_id"].first.should == substitute_user.id
associated_instance.journals.last.changed_data[association.to_s + "_id"].last.should == user2.id
associated_instance.journals.last.changed_data["#{association}_id".to_sym].first.should == substitute_user.id
associated_instance.journals.last.changed_data["#{association}_id".to_sym].last.should == user2.id
end
end
end
@ -120,16 +124,16 @@ describe User, "#destroy" do
end
describe "WHEN the user has a cost entry" do
let(:issue) { FactoryGirl.create(:issue) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:entry) { FactoryGirl.build(:cost_entry, :user => user,
:project => issue.project,
:project => work_package.project,
:units => 100.0,
:spent_on => Date.today,
:work_package => issue,
:work_package => work_package,
:comments => "") }
before do
FactoryGirl.create(:member, :project => issue.project,
FactoryGirl.create(:member, :project => work_package.project,
:user => user,
:roles => [FactoryGirl.build(:role)])
entry.save!

@ -2,8 +2,8 @@ require File.dirname(__FILE__) + '/../spec_helper'
describe VariableCostObject do
let(:cost_object) { FactoryGirl.build(:variable_cost_object) }
let(:tracker) { FactoryGirl.create(:tracker_feature) }
let(:project) { FactoryGirl.create(:project_with_trackers) }
let(:type) { FactoryGirl.create(:type_feature) }
let(:project) { FactoryGirl.create(:project_with_types) }
let(:user) { FactoryGirl.create(:user) }
describe 'recreate initial journal' do
@ -31,18 +31,18 @@ describe VariableCostObject do
end
describe "destroy" do
let(:issue) { FactoryGirl.create(:issue) }
let(:work_package) { FactoryGirl.create(:work_package) }
before do
cost_object.author = user
cost_object.work_packages = [issue]
cost_object.work_packages = [work_package]
cost_object.save!
cost_object.destroy
end
it { VariableCostObject.find_by_id(cost_object.id).should be_nil }
it { Issue.find_by_id(issue.id).should == issue }
it { issue.reload.cost_object.should be_nil }
it { WorkPackage.find_by_id(work_package.id).should == work_package }
it { work_package.reload.cost_object.should be_nil }
end
end

@ -0,0 +1,37 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe WorkPackage do
let(:user) { FactoryGirl.create(:admin) }
let(:role) { FactoryGirl.create(:role) }
let(:project) do
project = FactoryGirl.create(:project_with_types)
project.add_member!(user, role)
project
end
let(:project2) { FactoryGirl.create(:project_with_types, types: project.types) }
let(:work_package) { FactoryGirl.create(:work_package, :project => project,
:type => project.types.first,
:author => user) }
let!(:cost_entry) { FactoryGirl.create(:cost_entry, work_package: work_package, project: project, units: 3, spent_on: Date.today, user: user, comments: "test entry") }
let!(:cost_object) { FactoryGirl.create(:cost_object, project: project) }
before(:each) do
User.stub!(:current).and_return(user)
end
it "should update cost entries on move" do
work_package.project_id.should eql project.id
work_package.move_to_project(project2).should_not be_false
cost_entry.reload.project_id.should eql project2.id
end
it "should allow to set cost_object to nil" do
work_package.cost_object = cost_object
work_package.save!
work_package.cost_object.should eql cost_object
work_package.cost_object = nil
lambda { work_package.save! }.should_not raise_error(ActiveRecord::RecordInvalid)
end
end
Loading…
Cancel
Save