Conflicts:
	app/views/hooks/_view_projects_settings_members_table_row.rhtml
pull/6827/head
jwollert 13 years ago
commit 6def2e627e
  1. 5
      app/controllers/cost_objects_controller.rb
  2. 6
      app/controllers/cost_types_controller.rb
  3. 73
      app/models/cost_object.rb
  4. 1
      app/models/cost_rate.rb
  5. 10
      app/models/cost_type.rb
  6. 11
      app/models/rate.rb
  7. 18
      app/models/variable_cost_object.rb
  8. 2
      app/views/cost_objects/_form.rhtml
  9. 17
      app/views/cost_objects/_labor_budget_item.rhtml
  10. 6
      app/views/cost_objects/_list.rhtml
  11. 21
      app/views/cost_objects/_material_budget_item.rhtml
  12. 4
      app/views/cost_objects/_sidebar.rhtml
  13. 12
      app/views/cost_objects/index.rhtml
  14. 8
      app/views/cost_objects/new.rhtml
  15. 6
      app/views/cost_objects/show.rhtml
  16. 11
      app/views/cost_types/_list.rhtml
  17. 6
      app/views/cost_types/_list_deleted.rhtml
  18. 22
      app/views/cost_types/_rate.rhtml
  19. 13
      app/views/cost_types/edit.rhtml
  20. 8
      app/views/cost_types/index.rhtml
  21. 2
      app/views/costlog/_date_range.rhtml
  22. 12
      app/views/costlog/edit.rhtml
  23. 10
      app/views/groups/_users_unused.rhtml
  24. 9
      app/views/hooks/_view_projects_settings_members_table_row.rhtml
  25. 2
      app/views/hourly_rates/_list_default.rhtml
  26. 6
      app/views/hourly_rates/_list_project.rhtml
  27. 22
      app/views/hourly_rates/_rate.rhtml
  28. 1
      app/views/hourly_rates/edit.rhtml
  29. 29
      app/views/issues/_action_menu.rhtml
  30. 18
      config/locales/de.yml
  31. 30
      config/locales/en.yml
  32. 34
      db/migrate/20120313152442_create_initial_variable_cost_object_journals.rb
  33. 29
      features/activity.feature
  34. 24
      features/step_definitions/cost_steps.rb
  35. 8
      init.rb
  36. 26
      lib/costs/principal_allowance_evaluator/costs.rb
  37. 10
      lib/costs_issue_patch.rb
  38. 92
      lib/costs_user_patch.rb
  39. 6
      spec/factories/varibale_cost_object_factory.rb
  40. 112
      spec/lib/costs/principal_allowance_evaluator/costs_spec.rb
  41. 386
      spec/models/user_allowed_to_spec.rb
  42. 9
      spec/models/user_spec.rb
  43. 20
      spec/models/variable_cost_object_spec.rb
  44. 25
      spec/spec_helper.rb

@ -33,6 +33,9 @@ class CostObjectsController < ApplicationController
include CostObjectsHelper
include Redmine::Export::PDF
menu_item :new_budget, :only => [:new]
menu_item :show_all, :only => [:index]
def index
limit = per_page_option
respond_to do |format|
@ -234,4 +237,4 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

@ -92,4 +92,8 @@ private
@cost_type = CostType.find(params[:id])
end
end
end
def default_breadcrumb
l(:caption_cost_type_plural)
end
end

@ -6,77 +6,74 @@ class CostObject < ActiveRecord::Base
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :project
has_many :issues, :dependent => :nullify
has_many :cost_entries, :through => :issues
has_many :time_entries, :through => :issues
attr_protected :author
acts_as_attachable :after_remove => :attachment_removed
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}}
if respond_to? :acts_as_journalized
acts_as_journalized :activity_find_options => {:include => [:project, :author]},
:activity_timestamp => "#{table_name}.updated_on",
:activity_author_key => :author_id
else
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
validates_presence_of :subject, :project, :author, :kind
validates_length_of :subject, :maximum => 255
validates_length_of :subject, :minimum => 1
def before_validation
self.author_id = User.current.id if self.new_record?
end
def before_destroy
issues.all.each do |i|
result = i.update_attributes({:cost_object => nil})
return false unless result
end
end
def attributes=(attrs)
# Remove any attributes which can not be assigned.
# This is to protect from exceptions during change of cost object type
attrs.delete_if{|k, v| !self.respond_to?("#{k}=")} if attrs.is_a?(Hash)
super(attrs)
end
def copy_from(arg)
cost_object = arg.is_a?(CostObject) ? arg : CostObject.find(arg)
self.attributes = cost_object.attributes.dup
end
# Wrap type column to make it usable in views (especially in a select tag)
def kind
self[:type]
end
def kind=(type)
self[:type] = type
end
# Assign all the issues with +version_id+ to this Cost Object
def assign_issues_by_version(version_id)
version = Version.find_by_id(version_id)
return 0 if version.nil? || version.fixed_issues.blank?
version.fixed_issues.each do |issue|
issue.update_attribute(:cost_object_id, self.id)
end
return version.fixed_issues.size
end
# Change the Cost Object type to another type. Valid types are
#
# * FixedCostObject
@ -90,46 +87,46 @@ class CostObject < ActiveRecord::Base
return self
end
end
# Amount spent. Virtual accessor that is overriden by subclasses.
def spent
0
end
def spent_for_display
# FIXME: Remove this function
spent
end
# Budget of labor. Virtual accessor that is overriden by subclasses.
def labor_budget
0.0
end
def labor_budget_for_display
# FIXME: Remove this function
labor_budget
end
# Budget of material, i.e. all costs besides labor costs. Virtual accessor that is overriden by subclasses.
def material_budget
0.0
end
def material_budget_for_display
# FIXME: Remove this function
material_budget
end
def budget
material_budget + labor_budget
end
def budget_for_display
# FIXME: Remove this function
budget
end
def status
# this just returns the symbol for I18N
if project_manager_signoff
@ -138,20 +135,20 @@ class CostObject < ActiveRecord::Base
client_signoff ? :label_status_awaiting_client : :label_status_in_progress
end
end
# Label of the current type for display in GUI. Virtual accessor that is overriden by subclasses.
def type_label
return l(:label_cost_object)
end
# Amount of the budget spent. Expressed as as a percentage whole number
def budget_ratio
return 0.0 if self.budget.nil? || self.budget == 0.0
return ((self.spent / self.budget) * 100).round
end
def css_classes
return "issue cost_object"
end
end

@ -5,6 +5,7 @@ class CostRate < Rate
def validate
# Only allow change of project and user on first creation
super
return if self.new_record?
errors.add :cost_type_id, :activerecord_error_invalid if cost_type_id_changed?

@ -63,12 +63,12 @@ class CostType < ActiveRecord::Base
end
end
end
def save_rates
rates.each do |rate|
rate.save(false)
rate.save!
end
end
end
end

@ -1,5 +1,8 @@
class Rate < ActiveRecord::Base
validates_numericality_of :rate, :allow_nil => false, :message => :activerecord_error_invalid
validates_presence_of :valid_from
validates_presence_of :rate
validates_numericality_of :rate, :allow_nil => false
belongs_to :user
belongs_to :project
@ -12,15 +15,15 @@ class Rate < ActiveRecord::Base
value
end
end
def validate
valid_from.to_date
rescue Exception
errors.add :valid_from, :activerecord_error_invalid
errors.add :valid_from, :not_a_date
end
def before_save
self.valid_from &&= valid_from.to_date
end
end
end

@ -10,6 +10,22 @@ 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
self.class.superclass.plural_name
end
def copy_from(arg)
cost_object = arg.is_a?(VariableCostObject) ? arg : VariableCostObject.find(arg)
self.attributes = cost_object.attributes.dup
@ -111,4 +127,4 @@ class VariableCostObject < CostObject
labor_budget_item.save(false)
end
end
end
end

@ -91,5 +91,5 @@
<%= javascript_include_tag 'subform', :plugin => 'redmine_costs' %>
<%= javascript_include_tag 'editinplace', :plugin => 'redmine_costs' %>
<%= javascript_tag "initialize_editinplace('src=\"#{image_path('cancel.png')}\" value=\"#{l(:label_cancel)}\"' )" %>
<%= javascript_tag "initialize_editinplace('src=\"#{image_path('cancel.png')}\" alt=\"#{l(:button_cancel_edit_budget)}\" title=\"#{l(:button_cancel_edit_budget)}\"' )" %>
<% end %>

@ -1,7 +1,7 @@
<%-
index ||= "INDEX"
<%-
index ||= "INDEX"
new_or_existing = labor_budget_item.new_record? ? 'new' : 'existing'
id_or_index = labor_budget_item.new_record? ? index : labor_budget_item.id
id_or_index = labor_budget_item.new_record? ? index : labor_budget_item.id
prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][]"
id_prefix = "cost_object_#{new_or_existing}_labor_budget_item_attributes_#{id_or_index}"
name_prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][#{id_or_index}]"
@ -9,24 +9,27 @@
@labor_budget_item = labor_budget_item
error_messages = error_messages_for 'labor_budget_item'
-%>
-%>
<% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, labor_budget_item do |cost_form| %>
<tr class="cost_entry <%= classes %>" id="<%= id_prefix %>">
<td class="units">
<label class="hidden-for-sighted" for="<%= id_prefix %>_units"><%= l(:field_hours) %></label>
<%= cost_form.text_field :hours, :index => id_or_index, :size => 3 %>
</td>
<td class="user">
<label class="hidden-for-sighted" for="<%= id_prefix %>_user_id"><%= l(:label_user) %></label>
<%= cost_form.select :user_id, @project.assignable_users.sort.collect{|u| [u.name, u.id]}, {:prompt => true}, {:index => id_or_index} %>
</td>
<td class="comment">
<label class="hidden-for-sighted" for="<%= id_prefix %>_comments"><%= l(:field_comments) %></label>
<%= cost_form.text_field :comments, :index => id_or_index, :size => 40 %>
</td>
<td class="currency">
<span id="<%= "#{id_prefix}_costs" %>" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<a href="javascript:;" id="<%= "#{id_prefix}_costs" %>" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<%= number_to_currency(labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id)) if labor_budget_item.can_view_costs?(User.current, @project) %>
</span>
</a>
<%= update_page_tag do |page|
page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');"
page << "edit($('#{id_prefix}_costs'), '#{name_prefix}[budget]', '#{number_to_currency(labor_budget_item.budget)}');" if labor_budget_item.budget
@ -35,7 +38,7 @@
<%= observe_field( "#{id_prefix}_hours", :frequency => 1, :url => {:action => :update_labor_budget_item, :project_id => @project.id}, :with => "'user_id=' + encodeURIComponent(document.getElementById('#{id_prefix}_user_id').value) + '&hours=' + encodeURIComponent(value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
</td>
<td class="delete">
<%= link_to_function image_tag('delete.png'), "deleteLaborBudgetItem('#{id_prefix}')" %>
<%= link_to_function image_tag('delete.png', :alt => l(:button_delete), :title => l(:button_delete)), "deleteLaborBudgetItem('#{id_prefix}')" %>
</td>
</tr>
<% end %>

@ -24,7 +24,7 @@
<%= content_tag(:td, number_to_currency(cost_object.budget, :precision => 0), :class => 'currency') %>
<%= content_tag(:td, number_to_currency(cost_object.spent, :precision => 0), :class => 'currency') %>
<%= content_tag(:td, number_to_currency(cost_object.budget - cost_object.spent, :precision => 0), :class => 'currency') %>
<%= content_tag(:td, extended_progress_bar(cost_object.budget_ratio, :width => '100%')) %>
<%= content_tag(:td, extended_progress_bar(cost_object.budget_ratio, :width => '100%', :legend => "#{cost_object.budget_ratio}%")) %>
<%-
total_budget += cost_object.budget
labor_budget += cost_object.labor_budget
@ -42,7 +42,7 @@
<td />
<tr>
<% end %>
</tbody>
</table>
<% end -%>
<% end -%>

@ -1,45 +1,48 @@
<%-
index ||= "INDEX"
<%-
index ||= "INDEX"
new_or_existing = material_budget_item.new_record? ? 'new' : 'existing'
id_or_index = material_budget_item.new_record? ? index : material_budget_item.id
id_or_index = material_budget_item.new_record? ? index : material_budget_item.id
prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][]"
id_prefix = "cost_object_#{new_or_existing}_material_budget_item_attributes_#{id_or_index}"
name_prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][#{id_or_index}]"
classes ||= ""
@material_budget_item = material_budget_item
error_messages = error_messages_for 'material_budget_item'
-%>
-%>
<% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, material_budget_item do |cost_form| %>
<tr class="cost_entry <%= classes %>" id="<%= id_prefix %>">
<td class="units">
<label class="hidden-for-sighted" for="<%= id_prefix %>_units"><%= l(:field_units) %></label>
<%= cost_form.text_field :units, :index => id_or_index, :size => 3 %>
<span id="<%= "#{id_prefix}_unit_name" %>">
<%=h material_budget_item.cost_type.unit_plural if material_budget_item.cost_type %>
</span>
</td>
<td class="cost_type">
<label class="hidden-for-sighted" for="<%= id_prefix %>_cost_type_id"><%= l(:field_cost_type) %></label>
<%= cost_form.select :cost_type_id, cost_types_collection_for_select_options(material_budget_item.cost_type), {}, {:index => id_or_index} %>
<%= observe_field( "#{id_prefix}_cost_type_id", :url => {:action => :update_material_budget_item, :project_id => @project.id}, :with => "'cost_type_id=' + encodeURIComponent(value) + '&units=' + encodeURIComponent(document.getElementById('#{id_prefix}_units').value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
<%= observe_field( "#{id_prefix}_units", :frequency => 1, :url => {:action => :update_material_budget_item, :project_id => @project.id}, :with => "'cost_type_id=' + encodeURIComponent(document.getElementById('#{id_prefix}_cost_type_id').value) + '&units=' + encodeURIComponent(value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
</td>
<td class="comment">
<label class="hidden-for-sighted" for="<%= id_prefix %>_comments"><%= l(:field_comments) %></label>
<%= cost_form.text_field :comments, :index => id_or_index, :size => 40 %>
</td>
<td class="currency">
<span id="<%= "#{id_prefix}_costs" %>" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<a href="javascript:;" id="<%= "#{id_prefix}_costs" %>" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<%= number_to_currency(material_budget_item.calculated_costs(@cost_object.fixed_date)) %>
</span>
</a>
<%= update_page_tag do |page|
page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');"
page << "edit($('#{id_prefix}_costs'), '#{name_prefix}[budget]', '#{number_to_currency(material_budget_item.budget)}');" if material_budget_item.budget
end %>
</td>
<td class="delete">
<%= link_to_function image_tag('delete.png'), "deleteMaterialBudgetItem('#{id_prefix}')" %>
<%= link_to_function image_tag('delete.png', :alt => l(:button_delete), :title => l(:button_delete)), "deleteMaterialBudgetItem('#{id_prefix}')" %>
</td>
</tr>
<% end %>

@ -1,4 +0,0 @@
<h3><%= l(:label_cost_object_plural) %></h3>
<%= link_to_if_authorized l(:label_cost_object_new), {:action => 'new', :project_id => @project } %><br />
<%= link_to l(:label_view_all_cost_objects), {:action => 'index'} %><br />

@ -1,10 +1,6 @@
<div class="contextual">
<%= link_to_if_authorized l(:button_add_cost_object), {:controller => 'cost_objects', :action => 'new', :project_id => @project }, :class => 'icon icon-add' %>
</div>
<h2><%=l(:label_cost_object_plural)%></h2>
<% html_title(l(:label_cost_object_plural)) %>
<% if @cost_objects.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
@ -23,10 +19,6 @@
-->
</p>
<% content_for :sidebar do %>
<%= render :partial => 'cost_objects/sidebar' %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag 'context_menu' %>
<%= stylesheet_link_tag 'context_menu' %>
@ -37,4 +29,4 @@
<% html_title l(:label_cost_object_plural) %>
<div id="context-menu" style="display: none;"></div>
<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'cost_objects', :action => 'context_menu')}')" %>
<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'cost_objects', :action => 'context_menu')}')" %>

@ -8,23 +8,19 @@
</div>
<%= submit_tag l(:button_create) %>
<%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
<%= link_to_remote l(:label_preview),
<%= link_to_remote l(:label_preview),
{ :url => { :controller => 'cost_objects', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('cost_object_form')",
:complete => "Element.scrollTo('preview')"
}, :accesskey => accesskey(:preview) %>
<%= javascript_tag "Form.Element.focus('cost_object_subject');" %>
<% end %>
<div id="preview" class="wiki"></div>
<% content_for :sidebar do %>
<%= render :partial => 'cost_objects/sidebar' %>
<% end %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>

@ -52,12 +52,8 @@
<% html_title "#{l(:label_cost_object)} ##{@cost_object.id}: #{@cost_object.subject}" %>
<% content_for :sidebar do %>
<%= render :partial => 'cost_objects/sidebar' %>
<% end %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<%= javascript_include_tag 'cost_objects', :plugin => 'redmine_costs' %>
<% end %>
<% end %>

@ -24,14 +24,15 @@
<%= content_tag :td, number_to_currency( rate ? rate.rate : 0.0), :class => "currency", :id => "cost_type_#{cost_type.id}_rate"%>
<td>
<% form_for :cost_type, cost_type, :url => { :controller => 'cost_types', :action => 'set_rate', :id => cost_type } do |f| %>
<%= f.text_field :rate, :value => "", :name => :rate, :size => 7 %> <%= Setting.plugin_redmine_costs['costs_currency'] %>
<%= image_submit_tag "save.png" %>
<label class="hidden-for-sighted" for="<%= "rate_field_#{cost_type.id}" %>"><%= l(:caption_set_rate) %></label>
<%= f.text_field :rate, :value => "", :name => :rate, :size => 7, :id => "rate_field_#{cost_type.id}" %> <%= Setting.plugin_redmine_costs['costs_currency'] %>
<%= image_submit_tag "save.png", :alt => l(:caption_save_rate), :title => l(:caption_save_rate) %>
<% end %>
</td>
<%= content_tag :td, cost_type.is_default? ? image_tag('true.png') : "" %>
<%= content_tag :td, cost_type.is_default? ? image_tag('true.png', :alt => l(:general_text_Yes)) : "" %>
<td>
<% form_for :cost_type, cost_type, :url => { :controller => 'cost_types', :action => 'toggle_delete', :id => cost_type } do |f| %>
<%= image_submit_tag "locked.png", :onclick => "return #{confirm_javascript_function(l(:text_are_you_sure))}" %>
<%= image_submit_tag "locked.png", :alt => l(:button_lock), :title => l(:button_lock), :onclick => "return #{confirm_javascript_function(l(:text_are_you_sure))}" %>
<% end %>
</td>
</tr>
@ -40,4 +41,4 @@
<% end %>
</tbody>
</table>
<% end %>
<% end %>

@ -9,7 +9,7 @@
<%= sort_header_tag "unit", :caption => l(:caption_cost_type_unit_name) %>
<%= sort_header_tag "unit_plural", :caption => l(:caption_cost_type_unit_name_plural) %>
<th><%= l(:caption_current_rate) %></th>
<th><%= l(:caption_deleted_at) %></th>
<th><%= l(:caption_locked_on) %></th>
<th></th>
</tr></thead>
<tbody>
@ -23,7 +23,7 @@
<%= content_tag :td, cost_type.deleted_at.to_date %>
<td>
<% form_for :cost_type, cost_type, :url => { :controller => 'cost_types', :action => 'toggle_delete', :id => cost_type } do |f| %>
<%= image_submit_tag "unlock.png" %>
<%= image_submit_tag "unlock.png" , :alt => l(:button_unlock), :title => l(:button_unlock) %>
<% end %>
</td>
</tr>
@ -32,4 +32,4 @@
<% end %>
</tbody>
</table>
<% end %>
<% end %>

@ -1,7 +1,7 @@
<%-
index ||= "INDEX"
<%-
index ||= "INDEX"
new_or_existing = rate.new_record? ? 'new' : 'existing'
id_or_index = rate.new_record? ? index : rate.id
id_or_index = rate.new_record? ? index : rate.id
prefix = "cost_type[#{new_or_existing}_rate_attributes][]"
id_prefix = "cost_type_#{new_or_existing}_rate_attributes_#{id_or_index}"
name_prefix = "cost_type[#{new_or_existing}_rate_attributes][#{id_or_index}]"
@ -9,14 +9,20 @@
@rate = rate
error_messages = error_messages_for 'rate'
-%>
-%>
<% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, rate do |rate_form| %>
<tr class="<%= classes %>" id="<%= id_prefix %>">
<td><%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %></td>
<td class="currency"><%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %> <%= Setting.plugin_redmine_costs['costs_currency'] %></td>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)"%></td>
<td>
<label class="hidden-for-sighted", for="<%= "#{id_prefix}_valid_from" %>"><%= l(:caption_valid_from) %></label>
<%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %>
</td>
<td class="currency">
<label class="hidden-for-sighted", for="<%= "#{id_prefix}_rate" %>"><%= l(:caption_rate) %></label>
<%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %> <%= Setting.plugin_redmine_costs['costs_currency'] %>
</td>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)", {:alt => l(:button_delete), :title => l(:button_delete)}%></td>
</tr>
<% end %>
<% end %>

@ -10,7 +10,7 @@
<p><%= f.text_field :unit_plural, :size => 20 %></p>
<p><%= f.check_box :default %></p>
</div>
<h3><%= l :caption_rate_history %></h3>
<% javascript_tag do -%>
RatesForm = new Subform('<%= escape_javascript(render(:partial => "rate", :object => CostRate.new )) %>',<%= @cost_type.rates.length %>,'rates_body');
@ -22,13 +22,22 @@
<th></th>
</tr></thead>
<tbody id="rates_body">
<%- @cost_type.rates.sort { |a,b| b.valid_from <=> a.valid_from }.each_with_index do |rate, index| -%>
<%- @cost_type.rates.sort { |a,b|
case
when !a.valid? && !b.valid?: 0
when !a.valid?: -1
when !b.valid?: 1
else
b.valid_from <=> a.valid_from
end
}.each_with_index do |rate, index| -%>
<%= render :partial => 'rate', :object => rate, :locals => {:index => index, :classes => cycle('odd', 'even')} %>
<%- end -%>
</tbody>
</table>
<div>
<div>
<label class="hidden-for-sighted", for="add_rate_date" %>"><%= l(:description_date_for_new_rate) %></label>
<%= text_field_tag :add_rate_date, "", :size => 10, :value => Date.today %><%= calendar_for("add_rate_date") %>
<%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {:class => "icon icon-add"} %>
</div>

@ -12,16 +12,16 @@
</p>
<p>
<%= check_box_tag :include_deleted, "1", @include_deleted, :autocomplete => "off" %>
<label for="include_deleted"><%= l(:label_include_deleted) %></label>
<label for="include_deleted"><%= l(:caption_show_locked) %></label>
</p>
<p class="buttons">
<%= link_to_remote l(:button_apply),
<%= link_to_remote l(:button_apply),
{ :update => "content",
:with => "Form.serialize('query_form')"
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :clear_filter => true },
{ :url => { :clear_filter => true },
:update => "content",
}, :class => 'icon icon-reload' %>
</p>

@ -9,8 +9,10 @@
<%= radio_button_tag 'period_type', '2', @free_period %>
<span onclick="$('period_type_2').checked = true;">
<%= l(:label_date_from) %>
<%= label_tag 'from', l(:description_date_range_list) %>
<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %>
<%= l(:label_date_to) %>
<%= label_tag 'to', l(:description_date_range_interval) %>
<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %>
</span>
<%= submit_tag l(:button_apply), :name => nil %>

@ -19,11 +19,11 @@
<%= observe_field( "cost_entry_spent_on", :frequency => 1, :url => {:controller => :cost_objects, :action => :update_material_budget_item, :project_id => @cost_entry.project}, :with => "'cost_type_id=' + encodeURIComponent(document.getElementById('cost_entry_cost_type_id').value) + '&units=' + encodeURIComponent(document.getElementById('cost_entry_units').value) + '&fixed_date=' + encodeURIComponent(value) + '&element_id=cost_entry'") %>
</p>
<p>
<label><%= l(:field_costs) %></label>
<label for="cost_entry_costs_edit"><%= l(:field_costs) %></label>
<% if User.current.allowed_to? :view_cost_rates, @cost_entry.project %>
<span id="cost_entry_costs" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<a href="javascript:;" id="cost_entry_costs" class="icon icon-edit" title="<%= l(:help_click_to_edit) %>">
<%= number_to_currency(@cost_entry.calculated_costs) %>
</span>
</a>
<%= update_page_tag do |page|
page << "makeEditable('cost_entry_costs', 'cost_entry[overridden_costs]');"
page << "edit($('cost_entry_costs'), 'cost_entry[overridden_costs]', '#{number_to_currency(@cost_entry.overridden_costs)}');" if @cost_entry.overridden_costs
@ -34,7 +34,7 @@
</span>
<br /><em><%= l(:help_override_rate) %></em>
<% end %>
</p>
<p><%= f.text_field :comments, :size => 100 %></p>
</div>
@ -44,7 +44,7 @@
<% content_for :header_tags do %>
<%= javascript_include_tag 'editinplace', :plugin => 'redmine_costs' %>
<%= javascript_tag "initialize_editinplace('src=\"#{image_path('cancel.png')}\" value=\"#{l(:label_cancel)}\"' )" %>
<%= javascript_tag "initialize_editinplace('src=\"#{image_path('cancel.png')}\" value=\"#{l(:label_cancel)}\" alt=\"#{l(:button_cancel_edit_costs)}\" title=\"#{l(:button_cancel_edit_costs)}\"' )" %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<% end %>

@ -14,7 +14,7 @@
<td align="center" style="white-space: nowrap;">
<% remote_form_for :group_user, group_user, :url => { :controller => 'groups', :action => 'set_membership_type', :id => @group, :user_id => user}, :method => :post do |f| %>
<%= f.select :membership_type, GroupUser::MEMBERSHIP_TYPES.collect{|t| [l(t), t]} %>
<%= image_submit_tag "save.png" %>
<%= image_submit_tag "save.png", :alt => l(:button_save) %>
<% end %>
</td>
<td class="buttons">
@ -36,24 +36,24 @@
<% if users.any? %>
<% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_user_new)%></legend>
<p><%= text_field_tag 'user_search', nil, :size => "40" %></p>
<p>
<%= label_tag :membership_type, l(:label_membership_type) %>
<%= select_tag :membership_type, options_for_select(GroupUser::MEMBERSHIP_TYPES.collect{|t| [l(t), t]}, GroupUser::DEFAULT_MEMBERSHIP_TYPE) %>
</p>
<%= observe_field(:user_search,
:frequency => 0.5,
:update => :users,
:url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
:with => 'q')
%>
<div id="users">
<%= principals_check_box_tags 'user_ids[]', users %>
</div>
<p><%= submit_tag l(:button_add) %></p>
</fieldset>
<% end %>

@ -7,7 +7,7 @@
allow_view = User.current.allowed_to?(:view_hourly_rates, project, :for => member.user)
allow_edit = User.current.allowed_to?(:edit_hourly_rates, project, :for => member.user)
-%>
<%- if allow_view -%>
<% rate = member.user.current_rate(project) -%>
<td class="currency" id="rate_for_<%= member.user.id %>">
@ -20,9 +20,8 @@
<% if allow_edit %>
<td align="center" style="white-space: nowrap;">
<% remote_form_for :rate, :url => { :controller => 'hourly_rates', :action => 'set_rate', :id => member.user, :project_id => project}, :method => :posts do |f| %>
<label class="hidden-for-sighted"><%= l(:caption_set_rate) %>
<%= f.text_field :rate, :value => "", :name => :rate, :size => 7 %> <%= Setting.plugin_redmine_costs['costs_currency'] %>
</label>
<label class="hidden-for-sighted", for="rate_text_field_for_<%= member.user.id %>"><%= l(:caption_set_rate) %></label>
<%= f.text_field :rate, :value => "", :name => :rate, :size => 7, :id => "rate_text_field_for_#{member.user.id}"%> <%= Setting.plugin_redmine_costs['costs_currency'] %>
<%= image_submit_tag "save.png", :alt => l(:button_save) %>
<% end %>
</td>
@ -34,4 +33,4 @@
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<% end %>
<% end %>
<% end %>
<% end %>

@ -17,7 +17,7 @@
<tr class="<%= cycle('odd', 'even') %>">
<td style="padding-right: 1em;"><%= rate.valid_from %></td>
<td class="currency"><%= number_to_currency(rate.rate) %></td>
<td><%= rate == current_rate ? image_tag('true.png') : "" %></td>
<td><%= rate == current_rate ? image_tag('true.png', :alt => l(:general_text_Yes)) : "" %></td>
</tr>
<%- end -%>
</tbody>

@ -1,7 +1,7 @@
<%
rates = @rates unless rates
project = @project unless project
current_rate = @user.current_rate(project)
%>
@ -26,9 +26,9 @@
<tr class="<%= cycle('odd', 'even') %>">
<td style="padding-right: 1em;"><%= rate.valid_from %></td>
<td class="currency"><%= number_to_currency(rate.rate) %></td>
<td><%= rate == current_rate ? image_tag('true.png') : "" %></td>
<td><%= rate == current_rate ? image_tag('true.png', :alt => l(:general_text_Yes)) : "" %></td>
</tr>
<%- end -%>
</tbody>
</table>
<% end %>
<% end %>

@ -1,7 +1,7 @@
<%-
index ||= "INDEX"
<%-
index ||= "INDEX"
new_or_existing = rate.new_record? ? 'new' : 'existing'
id_or_index = rate.new_record? ? index : rate.id
id_or_index = rate.new_record? ? index : rate.id
prefix = "user[#{new_or_existing}_rate_attributes][]"
id_prefix = "user_#{new_or_existing}_rate_attributes_#{id_or_index}"
name_prefix = "user[#{new_or_existing}_rate_attributes][#{id_or_index}]"
@ -9,14 +9,20 @@
@rate = rate
error_messages = error_messages_for 'rate'
-%>
-%>
<% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, rate do |rate_form| %>
<tr class="<%= classes %>" id="<%= id_prefix %>">
<td><%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %></td>
<td class="currency"><%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %> <%= Setting.plugin_redmine_costs['costs_currency'] %></td>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)"%></td>
<td>
<label class="hidden-for-sighted", for="<%= "#{id_prefix}_valid_from" %>"><%= l(:caption_valid_from) %></label>
<%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %>
</td>
<td class="currency">
<label class="hidden-for-sighted", for="<%= "#{id_prefix}_rate" %>"><%= l(:caption_rate) %></label>
<%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %> <%= Setting.plugin_redmine_costs['costs_currency'] %>
</td>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)", :alt => l(:button_delete) %></td>
</tr>
<% end %>
<% end %>

@ -26,6 +26,7 @@
</table>
<div>
<div>
<label class="hidden-for-sighted", for="add_rate_date" %>"><%= l(:description_date_for_new_rate) %></label>
<%= text_field_tag :add_rate_date, "", :size => 10, :value => Date.today %><%= calendar_for("add_rate_date") %>
<%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {:class => "icon icon-add"} %>
</div>

@ -1,10 +1,19 @@
<div class="contextual">
<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %>
<%= link_to_if_authorized l(:button_log_costs), {:controller => 'costlog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-pieces' %>
<%= watcher_link(@issue, User.current, {:class => 'watcher_link', :replace => ['#watchers', '.watcher_link']}) %>
<%= link_to_if_authorized l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-duplicate' %>
<%= link_to_if_authorized l(:button_copy), {:controller => 'issue_moves', :action => 'new', :id => @issue, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy' %>
<%= link_to_if_authorized l(:button_move), {:controller => 'issue_moves', :action => 'new', :id => @issue}, :class => 'icon icon-move' %>
<%= 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' %>
</div>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<% end %>
<% content_for :action_menu_main do %>
<%= li_unless_nil(link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit))) %>
<%= li_unless_nil(watcher_link(@issue,
User.current,
{ :class => 'watcher_link',
:replace => User.current.allowed_to?(:view_issue_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 => 'edit', :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') %>
<% end %>

@ -1,13 +1,13 @@
---
de:
---
de:
cost_objects_title: Budgets
cost_types_title: Kostenarten
cost_reports_title: Reports
project_module_costs_module: Controlling
currency_separator: ","
currency_delimiter: .
x_entries:
one: "1 Eintrag"
other: "%{count} Einträge"
@ -35,9 +35,10 @@ de:
caption_costs: Kosten
caption_current_rate: Aktueller Satz
caption_default: Standard
field_rate: Satz
field_rates: Sätze
caption_default_rate_history_for: "Standardsatz-Historie f\xC3\xBCr %{user}"
caption_default_rates: "Standards\xC3\xA4tze"
caption_deleted_at: "Gel\xC3\xB6scht am"
caption_fixed_date: Referenzdatum
caption_issue: Ticket
caption_labor: Personal
@ -57,6 +58,7 @@ de:
caption_status: Status
caption_subject: Titel
caption_valid_from: "G\xC3\xBCltig ab"
field_valid_from: "G\xC3\xBCltig ab"
error_generic: "Bei der Anfrage ist ein Fehler aufgetreten. Sie wurde zurückgesetzt. Bitte melden Sie das Problem Ihrem Redmine Administrator"
@ -159,3 +161,9 @@ de:
text_destroy_time_and_cost_entries_question: Es wurden bereits %{hours} Stunden sowie %{cost_entries} auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?
text_reassign_time_and_cost_entries: 'Gebuchte Aufwände diesem Ticket zuweisen:'
text_warning_hidden_elements: "Es wurden möglicherweise nicht alle Einträge berücksichtigt."
caption_save_rate: "Satz speichern"
caption_locked_on: "Gesperrt am"
caption_show_locked: "Gesperrte Typen anzeigen"
button_cancel_edit_budget: "Budget bearbeiten abbrechen"
button_cancel_edit_costs: "Kosten bearbeiten abbrechen"
description_date_for_new_rate: "Datum für neuen Satz"

@ -1,5 +1,5 @@
---
en:
---
en:
cost_objects_title: Budgets
cost_types_title: Cost Types
cost_reports_title: Cost Reports
@ -7,7 +7,7 @@ en:
currency_separator: .
currency_delimiter: ","
x_entries:
one: "1 Entry"
other: "%{count} Entries"
@ -34,9 +34,10 @@ en:
caption_costs: Costs
caption_current_rate: Current Rate
caption_default: Default
field_rate: Rate
field_rates: Rates
caption_default_rate_history_for: Default Rate History for %{user}
caption_default_rates: Default Rates
caption_deleted_at: Deleted on
caption_fixed_date: Fixed Date
caption_issue: Issue
caption_labor: Labor
@ -56,9 +57,10 @@ en:
caption_status: Status
caption_subject: Title
caption_valid_from: Valid from
field_valid_from: Valid from
error_generic: "An error occurred while executing the query. It has been resetted. Please report this error to your Redmine administrator."
field_budget_ratio: Spent Budget
field_client_signoff: Client signoff
field_cost_object: Budget
@ -81,11 +83,11 @@ en:
field_unit_plural: Pluralized unit name
field_unit_price: Unit price
field_units: Units
help_click_to_edit: Click here to edit.
help_currency_format: Format of displayed currency values. %%n is replaced with the currency value, %%u ist replaced with the currency unit.
help_override_rate: Enter a value here to override the default rate.
label_awaiting_client: Awaiting client response
label_status_awaiting_client: Awaiting client response
label_awaiting_manager: Awaiting manager response
@ -131,12 +133,12 @@ en:
label_variable_cost_object: Variable rate based budget
label_view_all_cost_objects: View all Budgets
label_yes: "Yes"
notice_cost_object_conflict: "Issues 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.
permission_block_tickets: Block tickets
permission_edit_cost_entries: Edit booked unit costs
permission_edit_cost_objects: Edit Budgets
@ -149,7 +151,7 @@ en:
permission_view_cost_rates: View cost rates
permission_view_hourly_rates: View all hourly rates
permission_view_own_hourly_rate: View own hourly rate
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 ?"
@ -157,3 +159,9 @@ en:
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_warning_hidden_elements: "Some entries may have been excluded from the aggregation."
caption_save_rate: "Save rate"
caption_locked_on: "Locked on"
caption_show_locked: "Show locked types"
button_cancel_edit_budget: "Cancel editing budget"
button_cancel_edit_costs: "Cancel editing costs"
description_date_for_new_rate: "Date for new rate"

@ -0,0 +1,34 @@
class CreateInitialVariableCostObjectJournals < ActiveRecord::Migration
def self.up
[VariableCostObject].each do |p|
say_with_time("Building initial journals for #{p.class_name}") do
# avoid touching the journaled object on journal creation
p.journal_class.class_exec {
def touch_journaled_after_creation
end
}
# Create initial journals
p.find(:all).each do |o|
# Using rescue and save! here because either the Journal or the
# touched record could fail. This will catch either error and continue
begin
new_journal = o.recreate_initial_journal!
rescue ActiveRecord::RecordInvalid => ex
if new_journal.errors.count == 1 && new_journal.errors.first[0] == "version"
# Skip, only error was from creating the initial journal for a record that already had one.
else
puts "ERROR: errors creating the initial journal for #{o.class.to_s}##{o.id.to_s}:"
puts " #{ex.message}"
end
end
end
end
end
end
def self.down
VariableCostObjectJournal.destroy_all
end
end

@ -0,0 +1,29 @@
Feature: Cost Object activities
Background:
Given there is a standard cost control project named "project1"
And I am admin
Scenario: cost object is a selectable activity type
When I go to the activity page of the project "project1"
Then I should see "Budgets" within "#sidebar"
Scenario: Generating a cost object creates an activity
Given there is a variable cost object with the following:
| project | project1 |
| subject | Cost Object Subject |
| created_on | Time.now - 1.day |
When I go to the activity page of the project "project1"
Then I should see "Cost Object Subject"
Scenario: Updating a cost object creates an activity
Given there is a variable cost object with the following:
| project | project1 |
| subject | cost_object1 |
| created_on | Time.now - 40.days |
And I update the variable cost object "cost_object1" with the following:
| subject | cost_object1_new_title |
When I go to the activity page of the project "project1"
Then I should see "cost_object1_new_title"

@ -166,3 +166,27 @@ Given /^users have times and the cost type "([^\"]*)" logged on the issue "([^\"
end
end
Given /^there is a variable cost object with the following:$/ do |table|
cost_object = Factory.build(:variable_cost_object)
table_hash = table.rows_hash
cost_object.created_on = table_hash.has_key?("created_on") ?
eval(table_hash["created_on"]) :
Time.now
cost_object.fixed_date = cost_object.created_on.to_date
cost_object.project = (Project.find_by_identifier(table_hash["project"]) || Project.find_by_name(table_hash ["project"])) if table_hash.has_key? "project"
cost_object.author = User.current
cost_object.subject = table_hash["subject"] if table_hash.has_key? "subject"
cost_object.save!
cost_object.journals.first.update_attribute(:created_at, eval(table_hash["created_on"])) if table_hash.has_key?("created_on")
end
Given /^I update the variable cost object "([^"]*)" with the following:$/ do |subject, table|
cost_object = VariableCostObject.find_by_subject(subject)
cost_object.subject = table.rows_hash["subject"]
cost_object.save!
end

@ -48,6 +48,8 @@ Dispatcher.to_prepare do
require_dependency 'costs_issue_observer'
# loading the class so that acts_as_journalized gets registered
VariableCostObject
end
# Hooks
@ -60,7 +62,7 @@ Redmine::Plugin.register :redmine_costs do
author 'Holger Just @ finnlabs'
author_url 'http://finn.de/team#h.just'
description 'The costs plugin provides basic cost management functionality for Redmine.'
version '2.0.2'
version '2.1.0'
requires_redmine :version_or_higher => '0.9'
@ -129,8 +131,8 @@ Redmine::Plugin.register :redmine_costs do
menu :project_menu, :cost_objects, {:controller => 'cost_objects', :action => 'index'},
:param => :project_id, :before => :settings, :caption => :cost_objects_title
# Activities
activity_provider :cost_objects
menu :project_menu, :new_budget, {:action => 'new', :controller => 'cost_objects' }, :param => :project_id, :caption => :label_cost_object_new, :parent => :cost_objects
menu :project_menu, :show_all, {:action => 'index', :controller => 'cost_objects' }, :param => :project_id, :caption => :label_view_all_cost_objects, :parent => :cost_objects
end
# Observers

@ -0,0 +1,26 @@
class Costs::PrincipalAllowanceEvaluator::Costs < ChiliProject::PrincipalAllowanceEvaluator::Base
def granted_for_global? membership, action, options
granted = super
allowed_for_role = Proc.new do |role|
@user.allowed_for_role(action, nil, role, [@user], options.merge({:for => @user, :global => true}))
end
granted ||= if membership.is_a?(Member)
membership.roles.any?(&allowed_for_role)
elsif membership.is_a?(Role)
allowed_for_role.call(membership)
end
end
def granted_for_project? role, action, project, options
(project.is_public? || role.member?) &&
@user.allowed_for_role(action, project, role, [@user], options.merge({:for => @user}))
end
def denied_for_project? role, action, project, options
action.is_a?(Symbol) &&
options[:for] && options[:for] != @user &&
Redmine::AccessControl.permission(action).granular_for
end
end

@ -16,6 +16,16 @@ module CostsIssuePatch
# disabled for now, implements part of ticket blocking
alias_method_chain :validate, :cost_object
register_journal_formatter(:cost_association) do |value, journaled, field|
association = journaled.class.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by_id(value.to_i)
record.subject if record
end
end
register_on_journal_formatter(:cost_association, 'cost_object_id')
def spent_hours
# overwritten method
@spent_hours ||= self.time_entries.visible(User.current).sum(:hours) || 0

@ -24,7 +24,7 @@ module CostsUserPatch
before_save :save_rates
alias_method_chain :allowed_to?, :inheritance
register_allowance_evaluator Costs::PrincipalAllowanceEvaluator::Costs
end
end
@ -37,8 +37,13 @@ module CostsUserPatch
perm = Redmine::AccessControl.permission(action)
if perm.granular_for
allowed && users.include?(options[:for] || self)
elsif !allowed && options[:for] && granulars = Redmine::AccessControl.permissions.select{|p| p.granular_for == perm}
granulars.detect{|p| self.allowed_to? p.name, project, options}
elsif !allowed &&
options[:for] &&
granulars = Redmine::AccessControl.permissions.select{|p| p.granular_for == perm}
granulars.any?{|p| self.allowed_to? p.name, project, options} ?
role :
false
else
allowed
end
@ -68,73 +73,6 @@ module CostsUserPatch
roles
end
# Return true if the user is allowed to do the specified action on project
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allowed_to_with_inheritance?(action, context, options={})
allowed_for_role = Proc.new do |role, users|
self.allowed_for_role(action, context, role, users, options)
end
options[:for] = self unless options.has_key?(:for)
if context && context.is_a?(Project)
# No action allowed on archived projects
return false unless context.active?
# No action allowed on disabled modules
return false unless context.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
roles = granular_roles_for_project(context)
return false unless roles
allowing_role_pair = roles.detect do |role, users|
if (context.is_public? || role.member?)
allowed_for_role.call(role, users)
else
false
end
end
allowing_role_pair ? allowing_role_pair[0] : allowing_role_pair #the role
elsif context && context.is_a?(Array)
# Authorize if user is authorized on every element of the array
context.map do |project|
allowed_to?(action, project, options)
end.inject do |memo,allowed|
memo && allowed
end
elsif options[:global]
# Admin users are always authorized
return true if admin?
# authorize if user has at least one role that has this permission
roles = memberships.inject({}) do |roles, m|
granular_roles(m.member_roles).each_pair do |role, users|
if roles[role]
roles[role] |= users unless users.nil?
else
roles[role] = users
end
roles
end
roles
end
allowing_role = roles.detect(&allowed_for_role)
if allowing_role
allowing_role[0]
else
self.logged? ? allowed_for_role.call(Role.non_member, [self]) : allowed_for_role.call(Role.anonymous, [self])
end
else
false
end
end
def allowed_for(permission, projects = nil)
unless projects.nil? or projects.blank?
projects = [projects] unless projects.is_a? Array
@ -248,20 +186,6 @@ module CostsUserPatch
roles = {}
member_roles.each do |r|
roles[r.role] = [self]
if r.inherited_from
# the role was inherited from a group
case r.membership_type
when :controller
inherited = MemberRole.find_by_id(r.inherited_from)
users = [self]
users += inherited.member.users# if inherited.member.principal.is_a? Group
roles[r.role] = users
else # :default
#nothing
end
end
end
roles
end

@ -0,0 +1,6 @@
Factory.define :variable_cost_object do |m|
m.association :project, :factory => :project
m.sequence(:subject) { |n| "Cost Object No. #{n}" }
m.sequence(:description) { |n| "I am a Cost Object No. #{n}" }
m.fixed_date Time.now
end

@ -0,0 +1,112 @@
require File.dirname(__FILE__) + '/../../../spec_helper'
describe Costs::PrincipalAllowanceEvaluator::Costs do
let(:klass) { Costs::PrincipalAllowanceEvaluator::Costs }
let(:user) { Factory.build :user }
let(:filter) { klass.new user }
let(:member) { Factory.build :member }
let(:project) { Factory.build :project }
let(:role) { Factory.build :role }
let(:role2) { Factory.build :role }
let(:permission) { Redmine::AccessControl::Permission.new(:action, {}, {}) }
let(:permission2) { Redmine::AccessControl::Permission.new(:action2, {}, {}) }
before do
Redmine::AccessControl.permissions.clear
Redmine::AccessControl.permissions << permission
Redmine::AccessControl.permissions << permission2
end
describe :granted_for_project? do
describe "WHEN the role is allowing the action" do
before do
role.permissions << permission.name
end
it { filter.granted_for_project?(role, permission.name, project, {}).should be_true }
end
describe "WHEN the role is not allowing the action" do
it { filter.granted_for_project?(role, permission.name, project, {}).should be_false }
end
end
describe :granted_for_global? do
describe "WHEN the membership has a role allowing the action" do
before do
member.roles = [role]
role.permissions << permission.name
end
it { filter.granted_for_global?(member, permission.name, {}).should be_true }
end
describe "WHEN the membership has two roles
WHEN the first role is not allowing the action
WHEN the second role is not allowing the action
WHEN the action is a granular_for an action the second role allows" do
before do
permission2.instance_variable_set("@granular_for", permission.name)
member.user = user
member.project = project
member.roles = [role, role2]
member.save!
role2.permissions << permission2.name
role2.save!
end
it { filter.granted_for_global?(member, permission.name, :for => user).should be_true }
end
describe "WHEN the membership has two roles
WHEN the first role is not allowing the action
WHEN the second role is not allowing the action
WHEN the action is a granular_for an action the second role does not allow" do
before do
member.user = user
member.project = project
member.roles = [role, role2]
member.save!
role2.permissions << :action_non
role2.save!
end
it { filter.granted_for_global?(member, permission.name, :for => user).should be_false }
end
describe "WHEN the membership has one role
WHEN the role is allowing the action
WHEN the action is a granular_for another action" do
before do
permission.instance_variable_set("@granular_for", :action_lorem)
member.user = user
member.project = project
member.roles = [role]
member.save!
role.permissions << :action
role.save!
end
it { filter.granted_for_global?(member, permission.name, {}).should be_true }
end
describe "WHEN inserting something other than a Member" do
it { filter.granted_for_global?(1, :action, {}).should be_false }
end
end
end

@ -0,0 +1,386 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe User, "#allowed_to?" do
let(:controller_member_role) { Factory.build(:member_role, :membership_type => :controller) }
let(:member) { Factory.build(:member) }
let(:member2) { Factory.build(:member) }
let(:group_member) { Factory.build(:member) }
let(:role) { Factory.build(:role) }
let(:role2) { Factory.build(:role) }
let(:user) { Factory.build(:user) }
let(:non_member) { Factory.build(:non_member) }
let(:anonymous_role) { Factory.build(:anonymous_role) }
let(:permission) { Redmine::AccessControl::Permission.new(:action, {}, {}) }
let(:permission2) { Redmine::AccessControl::Permission.new(:action2, {}, {}) }
let(:project) { Factory.build(:project) }
let(:project2) { Factory.build(:project) }
let(:group) { Group.new :lastname => "group" }
def create_member_with_roles roles
member.user = user
member.project = project
member.roles = roles
member.save!
member
end
before do
project.save!
Redmine::AccessControl.permissions.clear
Redmine::AccessControl.permissions << permission
Redmine::AccessControl.permissions << permission2
non_member.save!
anonymous_role.save!
User.anonymous
user.save!
role.save!
end
after do
User.destroy_all
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has a role allowing the action" do
before do
create_member_with_roles [role]
role.permissions << permission.name
role.save
end
it { user.allowed_to?(permission.name, project, {}).should be_true }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has a role not allowing the action" do
before do
create_member_with_roles [role]
role.permissions << :action_non
role.save
end
it { user.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has a role not allowing the action
WHEN the membership has a second role allowing the action" do
before do
create_member_with_roles [role, role2]
role.permissions << :action_non
role.save!
role2.permissions << permission.name
role2.save!
end
it { user.allowed_to?(permission.name, project, {}).should be_true }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has two roles
WHEN the first role is not allowing the action
WHEN the second role is not allowing the action
WHEN the action is a granular_for an action the second role allows" do
before do
permission2.instance_variable_set("@granular_for_obj", permission)
create_member_with_roles [role, role2]
role2.permissions << permission2.name
role2.save!
end
it { user.allowed_to?(permission.name, project).should be_true }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has two roles
WHEN the first role is not allowing the action
WHEN the second role is not allowing the action
WHEN the action is a granular_for an action the second role does not allow" do
before do
permission2.instance_variable_set("@granular_for_obj", permission)
create_member_with_roles [role, role2]
role2.permissions << :non
role2.save!
end
it { user.allowed_to?(permission.name, project).should be_false }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has one role
WHEN the first role is allowing the action
WHEN the action is a granular_for another action
WHEN the request is issued for the user" do
before do
permission.instance_variable_set("@granular_for_obj", permission2)
create_member_with_roles [role]
role.permissions << :action
role.save!
end
it { user.allowed_to?(permission.name, project, :for => user).should be_true }
end
describe "WHEN requesting a project permission
WHEN the user has a membership in the project
WHEN the membership has one role
WHEN the first role is allowing the action
WHEN the action is a granular_for another action
WHEN the request is issued for somebody else" do
before do
permission.instance_variable_set("@granular_for_obj", permission2)
create_member_with_roles [role]
role.permissions << :action
role.save!
end
it { user.allowed_to?(permission.name, project, :for => Factory.build(:user)).should be_false }
end
describe "WHEN requesting a project permission
WHEN the user has no membership in this project" do
before do
member.user = user
member.project = Factory.build(:project)
member.roles = [role]
member.save!
role.permissions << permission.name
role.save!
end
it { user.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a project permission
WHEN the user is admin" do
before do
user.admin = true
end
it { user.allowed_to?(permission.name, project, {}).should be_true }
end
describe "WHEN requesting a project permission
WHEN the project is public
WHEN the action is allowed for non members" do
before do
project.is_public = true
non_member.permissions << permission.name
non_member.save!
end
it { user.allowed_to?(permission.name, project, {}).should be_true }
end
describe "WHEN requesting a project permission
WHEN the project is public
WHEN the action is not allowed for non members" do
before do
project.is_public = true
end
it { user.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a project permission as anonymous
WHEN the project is public
WHEN the action is allowed for anonymous" do
before do
project.is_public = true
anonymous_role.permissions << permission.name
anonymous_role.save!
end
it { User.anonymous.allowed_to?(permission.name, project, {}).should be_true }
end
describe "WHEN requesting a project permission as anonymous
WHEN the project is public
WHEN the action is not allowed for anonymous" do
before do
project.is_public = true
end
it { User.anonymous.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a project permission
WHEN the project is inactive" do
before do
create_member_with_roles [role]
role.permissions << permission.name
project.archive
end
it { user.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a project permission
WHEN the project is not allowing the action" do
before do
create_member_with_roles [role]
project.instance_variable_set("@allowed_permissions", [])
role.permissions << permission.name
role.save!
end
it { user.allowed_to?(permission.name, project, {}).should be_false }
end
describe "WHEN requesting a permission on two projects
WHEN the permission is granted on both projects" do
before do
project2.save!
create_member_with_roles [role]
member2.project = project2
member2.user = user
member2.roles = [role2]
member2.save!
role2.permissions << permission.name
role2.save!
role.permissions << permission.name
role.save!
end
it { user.allowed_to?(permission.name, [project, project2], {}).should be_true }
end
describe "WHEN requesting a permission on two projects
WHEN the permission is granted on one project" do
before do
project2.save!
create_member_with_roles [role]
role.permissions << permission.name
role.save!
end
it { user.allowed_to?(permission.name, [project, project2], {}).should be_false }
end
describe "WHEN requesting a permission on two projects
WHEN the permission is granted on none of the project" do
it { user.allowed_to?(permission.name, [project, project2], {}).should be_false }
end
describe "WHEN requesting a permission on no project" do
it { user.allowed_to?(permission.name, [], {}).should be_false }
end
describe "WHEN requesting a global permission
WHEN the user is admin" do
before do
user.admin = true
end
it { user.allowed_to?(permission.name, nil, :global => true).should be_true }
end
describe "WHEN requesting a global permission as anonymous
WHEN anonymous is allowed the action" do
before do
anonymous_role.permissions << :action
anonymous_role.save!
end
it { User.anonymous.allowed_to?(:action, nil, :global => true, :for => user).should be_true }
end
describe "WHEN requesting a global permission as anonymous
WHEN anonymous is not allowed the action" do
it { User.anonymous.allowed_to?(:action, nil, :global => true, :for => user).should be_false }
end
describe "WHEN requesting a global permission as anonymous
WHEN anonymous is not allowed the action
WHEN anonymous has a permission for an action that is a granular_for the requested action" do
before do
permission2.instance_variable_set("@granular_for_obj", permission)
anonymous_role.permissions << :action2
anonymous_role.save!
end
it { User.anonymous.allowed_to?(:action, nil, :global => true).should be_true }
end
describe "WHEN requesting a global permission
WHEN non_members are allowed the action" do
before do
non_member.permissions << :action
non_member.save!
end
it { user.allowed_to?(:action, nil, :global => true, :for => user).should be_true }
end
describe "WHEN requesting a global permission
WHEN non_members are not allowed the action" do
it { user.allowed_to?(:action, nil, :global => true, :for => user).should be_false }
end
describe "WHEN requesting a global permission
WHEN non_members are not allowed the action
WHEN non_member has a permission for an action that is a granular_for the requested action" do
before do
permission2.instance_variable_set("@granular_for_obj", permission)
non_member.permissions << :action2
non_member.save!
end
it { user.allowed_to?(:action, nil, :global => true).should be_true }
end
end

@ -0,0 +1,9 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe User do
let(:klass) { User }
describe :registered_allowance_evaluators do
it { klass.registered_allowance_evaluators.include?(Costs::PrincipalAllowanceEvaluator::Costs).should be_true }
end
end

@ -0,0 +1,20 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe VariableCostObject do
before(:each) do
@tracker ||= Factory.create(:tracker_feature)
@project ||= Factory.create(:project_with_trackers)
@current = Factory.create(:user, :login => "user1", :mail => "user1@users.com")
User.stub!(:current).and_return(@current)
end
it 'should work with recreate initial journal' do
@variable_cost_object ||= Factory.create(:variable_cost_object , :project => @project, :author => @current)
initial_journal = @variable_cost_object.journals.first
recreated_journal = @variable_cost_object.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
end

@ -1,26 +1,7 @@
RAILS_ENV = "test" unless defined? RAILS_ENV
# prevent case where we are using rubygems and test-unit 2.x is installed
begin
require 'rubygems'
gem "test-unit", "~> 1.2.3"
rescue LoadError
end
begin
#require "config/environment" unless defined? RAILS_ROOT
require 'spec/spec_helper'
rescue LoadError => error
puts <<-EOS
You need to install rspec in your Redmine project.
Please execute the following code:
gem install rspec-rails
script/generate rspec
EOS
raise error
end
require 'spec/spec_helper'
require 'redmine_factory_girl'
require 'identical_ext'
Fixtures.create_fixtures File.join(File.dirname(__FILE__), "fixtures"), ActiveRecord::Base.connection.tables

Loading…
Cancel
Save