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 CostObjectsHelper
include Redmine::Export::PDF include Redmine::Export::PDF
menu_item :new_budget, :only => [:new]
menu_item :show_all, :only => [:index]
def index def index
limit = per_page_option limit = per_page_option
respond_to do |format| respond_to do |format|
@ -234,4 +237,4 @@ private
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
end end

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

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

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

@ -1,5 +1,8 @@
class Rate < ActiveRecord::Base 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 :user
belongs_to :project belongs_to :project
@ -12,15 +15,15 @@ class Rate < ActiveRecord::Base
value value
end end
end end
def validate def validate
valid_from.to_date valid_from.to_date
rescue Exception rescue Exception
errors.add :valid_from, :activerecord_error_invalid errors.add :valid_from, :not_a_date
end end
def before_save def before_save
self.valid_from &&= valid_from.to_date self.valid_from &&= valid_from.to_date
end end
end end

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

@ -91,5 +91,5 @@
<%= javascript_include_tag 'subform', :plugin => 'redmine_costs' %> <%= javascript_include_tag 'subform', :plugin => 'redmine_costs' %>
<%= javascript_include_tag 'editinplace', :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 %> <% end %>

@ -1,7 +1,7 @@
<%- <%-
index ||= "INDEX" index ||= "INDEX"
new_or_existing = labor_budget_item.new_record? ? 'new' : 'existing' 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][]" prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][]"
id_prefix = "cost_object_#{new_or_existing}_labor_budget_item_attributes_#{id_or_index}" 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}]" name_prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][#{id_or_index}]"
@ -9,24 +9,27 @@
@labor_budget_item = labor_budget_item @labor_budget_item = labor_budget_item
error_messages = error_messages_for 'labor_budget_item' error_messages = error_messages_for 'labor_budget_item'
-%> -%>
<% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %> <% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, labor_budget_item do |cost_form| %> <% fields_for prefix, labor_budget_item do |cost_form| %>
<tr class="cost_entry <%= classes %>" id="<%= id_prefix %>"> <tr class="cost_entry <%= classes %>" id="<%= id_prefix %>">
<td class="units"> <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 %> <%= cost_form.text_field :hours, :index => id_or_index, :size => 3 %>
</td> </td>
<td class="user"> <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} %> <%= cost_form.select :user_id, @project.assignable_users.sort.collect{|u| [u.name, u.id]}, {:prompt => true}, {:index => id_or_index} %>
</td> </td>
<td class="comment"> <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 %> <%= cost_form.text_field :comments, :index => id_or_index, :size => 40 %>
</td> </td>
<td class="currency"> <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) %> <%= 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| <%= update_page_tag do |page|
page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');" 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 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}'") %> <%= 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>
<td class="delete"> <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> </td>
</tr> </tr>
<% end %> <% 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.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.spent, :precision => 0), :class => 'currency') %>
<%= content_tag(:td, number_to_currency(cost_object.budget - 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 total_budget += cost_object.budget
labor_budget += cost_object.labor_budget labor_budget += cost_object.labor_budget
@ -42,7 +42,7 @@
<td /> <td />
<tr> <tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% end -%> <% end -%>

@ -1,45 +1,48 @@
<%- <%-
index ||= "INDEX" index ||= "INDEX"
new_or_existing = material_budget_item.new_record? ? 'new' : 'existing' 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][]" prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][]"
id_prefix = "cost_object_#{new_or_existing}_material_budget_item_attributes_#{id_or_index}" 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}]" name_prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][#{id_or_index}]"
classes ||= "" classes ||= ""
@material_budget_item = material_budget_item @material_budget_item = material_budget_item
error_messages = error_messages_for 'material_budget_item' error_messages = error_messages_for 'material_budget_item'
-%> -%>
<% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %> <% unless error_messages.blank? %><tr><td colspan="5"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, material_budget_item do |cost_form| %> <% fields_for prefix, material_budget_item do |cost_form| %>
<tr class="cost_entry <%= classes %>" id="<%= id_prefix %>"> <tr class="cost_entry <%= classes %>" id="<%= id_prefix %>">
<td class="units"> <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 %> <%= cost_form.text_field :units, :index => id_or_index, :size => 3 %>
<span id="<%= "#{id_prefix}_unit_name" %>"> <span id="<%= "#{id_prefix}_unit_name" %>">
<%=h material_budget_item.cost_type.unit_plural if material_budget_item.cost_type %> <%=h material_budget_item.cost_type.unit_plural if material_budget_item.cost_type %>
</span> </span>
</td> </td>
<td class="cost_type"> <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} %> <%= 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}_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}'") %> <%= 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>
<td class="comment"> <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 %> <%= cost_form.text_field :comments, :index => id_or_index, :size => 40 %>
</td> </td>
<td class="currency"> <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)) %> <%= number_to_currency(material_budget_item.calculated_costs(@cost_object.fixed_date)) %>
</span> </a>
<%= update_page_tag do |page| <%= update_page_tag do |page|
page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');" 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 page << "edit($('#{id_prefix}_costs'), '#{name_prefix}[budget]', '#{number_to_currency(material_budget_item.budget)}');" if material_budget_item.budget
end %> end %>
</td> </td>
<td class="delete"> <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> </td>
</tr> </tr>
<% end %> <% 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> <h2><%=l(:label_cost_object_plural)%></h2>
<% html_title(l(:label_cost_object_plural)) %> <% html_title(l(:label_cost_object_plural)) %>
<% if @cost_objects.empty? %> <% if @cost_objects.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p> <p class="nodata"><%= l(:label_no_data) %></p>
<% else %> <% else %>
@ -23,10 +19,6 @@
--> -->
</p> </p>
<% content_for :sidebar do %>
<%= render :partial => 'cost_objects/sidebar' %>
<% end %>
<% content_for :header_tags do %> <% content_for :header_tags do %>
<%= javascript_include_tag 'context_menu' %> <%= javascript_include_tag 'context_menu' %>
<%= stylesheet_link_tag 'context_menu' %> <%= stylesheet_link_tag 'context_menu' %>
@ -37,4 +29,4 @@
<% html_title l(:label_cost_object_plural) %> <% html_title l(:label_cost_object_plural) %>
<div id="context-menu" style="display: none;"></div> <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> </div>
<%= submit_tag l(:button_create) %> <%= submit_tag l(:button_create) %>
<%= submit_tag l(:button_create_and_continue), :name => 'continue' %> <%= 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 }, { :url => { :controller => 'cost_objects', :action => 'preview', :project_id => @project },
:method => 'post', :method => 'post',
:update => 'preview', :update => 'preview',
:with => "Form.serialize('cost_object_form')", :with => "Form.serialize('cost_object_form')",
:complete => "Element.scrollTo('preview')" :complete => "Element.scrollTo('preview')"
}, :accesskey => accesskey(:preview) %> }, :accesskey => accesskey(:preview) %>
<%= javascript_tag "Form.Element.focus('cost_object_subject');" %> <%= javascript_tag "Form.Element.focus('cost_object_subject');" %>
<% end %> <% end %>
<div id="preview" class="wiki"></div> <div id="preview" class="wiki"></div>
<% content_for :sidebar do %>
<%= render :partial => 'cost_objects/sidebar' %>
<% end %>
<% content_for :header_tags do %> <% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %> <%= stylesheet_link_tag 'scm' %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %> <%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>

@ -52,12 +52,8 @@
<% html_title "#{l(:label_cost_object)} ##{@cost_object.id}: #{@cost_object.subject}" %> <% 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 %> <% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %> <%= stylesheet_link_tag 'scm' %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %> <%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<%= javascript_include_tag 'cost_objects', :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"%> <%= content_tag :td, number_to_currency( rate ? rate.rate : 0.0), :class => "currency", :id => "cost_type_#{cost_type.id}_rate"%>
<td> <td>
<% form_for :cost_type, cost_type, :url => { :controller => 'cost_types', :action => 'set_rate', :id => cost_type } do |f| %> <% 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'] %> <label class="hidden-for-sighted" for="<%= "rate_field_#{cost_type.id}" %>"><%= l(:caption_set_rate) %></label>
<%= image_submit_tag "save.png" %> <%= 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 %> <% end %>
</td> </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> <td>
<% form_for :cost_type, cost_type, :url => { :controller => 'cost_types', :action => 'toggle_delete', :id => cost_type } do |f| %> <% 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 %> <% end %>
</td> </td>
</tr> </tr>
@ -40,4 +41,4 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% end %> <% end %>

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

@ -1,7 +1,7 @@
<%- <%-
index ||= "INDEX" index ||= "INDEX"
new_or_existing = rate.new_record? ? 'new' : 'existing' 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][]" prefix = "cost_type[#{new_or_existing}_rate_attributes][]"
id_prefix = "cost_type_#{new_or_existing}_rate_attributes_#{id_or_index}" id_prefix = "cost_type_#{new_or_existing}_rate_attributes_#{id_or_index}"
name_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 @rate = rate
error_messages = error_messages_for 'rate' error_messages = error_messages_for 'rate'
-%> -%>
<% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %> <% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, rate do |rate_form| %> <% fields_for prefix, rate do |rate_form| %>
<tr class="<%= classes %>" id="<%= id_prefix %>"> <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>
<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> <label class="hidden-for-sighted", for="<%= "#{id_prefix}_valid_from" %>"><%= l(:caption_valid_from) %></label>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)"%></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">
<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> </tr>
<% end %> <% end %>

@ -10,7 +10,7 @@
<p><%= f.text_field :unit_plural, :size => 20 %></p> <p><%= f.text_field :unit_plural, :size => 20 %></p>
<p><%= f.check_box :default %></p> <p><%= f.check_box :default %></p>
</div> </div>
<h3><%= l :caption_rate_history %></h3> <h3><%= l :caption_rate_history %></h3>
<% javascript_tag do -%> <% javascript_tag do -%>
RatesForm = new Subform('<%= escape_javascript(render(:partial => "rate", :object => CostRate.new )) %>',<%= @cost_type.rates.length %>,'rates_body'); RatesForm = new Subform('<%= escape_javascript(render(:partial => "rate", :object => CostRate.new )) %>',<%= @cost_type.rates.length %>,'rates_body');
@ -22,13 +22,22 @@
<th></th> <th></th>
</tr></thead> </tr></thead>
<tbody id="rates_body"> <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')} %> <%= render :partial => 'rate', :object => rate, :locals => {:index => index, :classes => cycle('odd', 'even')} %>
<%- end -%> <%- end -%>
</tbody> </tbody>
</table> </table>
<div> <div>
<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") %> <%= 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"} %> <%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {:class => "icon icon-add"} %>
</div> </div>

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

@ -9,8 +9,10 @@
<%= radio_button_tag 'period_type', '2', @free_period %> <%= radio_button_tag 'period_type', '2', @free_period %>
<span onclick="$('period_type_2').checked = true;"> <span onclick="$('period_type_2').checked = true;">
<%= l(:label_date_from) %> <%= l(:label_date_from) %>
<%= label_tag 'from', l(:description_date_range_list) %>
<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %> <%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %>
<%= l(:label_date_to) %> <%= l(:label_date_to) %>
<%= label_tag 'to', l(:description_date_range_interval) %>
<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %> <%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %>
</span> </span>
<%= submit_tag l(:button_apply), :name => nil %> <%= 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'") %> <%= 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>
<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 %> <% 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) %> <%= number_to_currency(@cost_entry.calculated_costs) %>
</span> </a>
<%= update_page_tag do |page| <%= update_page_tag do |page|
page << "makeEditable('cost_entry_costs', 'cost_entry[overridden_costs]');" 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 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> </span>
<br /><em><%= l(:help_override_rate) %></em> <br /><em><%= l(:help_override_rate) %></em>
<% end %> <% end %>
</p> </p>
<p><%= f.text_field :comments, :size => 100 %></p> <p><%= f.text_field :comments, :size => 100 %></p>
</div> </div>
@ -44,7 +44,7 @@
<% content_for :header_tags do %> <% content_for :header_tags do %>
<%= javascript_include_tag 'editinplace', :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')}\" value=\"#{l(:label_cancel)}\" alt=\"#{l(:button_cancel_edit_costs)}\" title=\"#{l(:button_cancel_edit_costs)}\"' )" %>
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %> <%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<% end %> <% end %>

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

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

@ -17,7 +17,7 @@
<tr class="<%= cycle('odd', 'even') %>"> <tr class="<%= cycle('odd', 'even') %>">
<td style="padding-right: 1em;"><%= rate.valid_from %></td> <td style="padding-right: 1em;"><%= rate.valid_from %></td>
<td class="currency"><%= number_to_currency(rate.rate) %></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> </tr>
<%- end -%> <%- end -%>
</tbody> </tbody>

@ -1,7 +1,7 @@
<% <%
rates = @rates unless rates rates = @rates unless rates
project = @project unless project project = @project unless project
current_rate = @user.current_rate(project) current_rate = @user.current_rate(project)
%> %>
@ -26,9 +26,9 @@
<tr class="<%= cycle('odd', 'even') %>"> <tr class="<%= cycle('odd', 'even') %>">
<td style="padding-right: 1em;"><%= rate.valid_from %></td> <td style="padding-right: 1em;"><%= rate.valid_from %></td>
<td class="currency"><%= number_to_currency(rate.rate) %></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> </tr>
<%- end -%> <%- end -%>
</tbody> </tbody>
</table> </table>
<% end %> <% end %>

@ -1,7 +1,7 @@
<%- <%-
index ||= "INDEX" index ||= "INDEX"
new_or_existing = rate.new_record? ? 'new' : 'existing' 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][]" prefix = "user[#{new_or_existing}_rate_attributes][]"
id_prefix = "user_#{new_or_existing}_rate_attributes_#{id_or_index}" id_prefix = "user_#{new_or_existing}_rate_attributes_#{id_or_index}"
name_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 @rate = rate
error_messages = error_messages_for 'rate' error_messages = error_messages_for 'rate'
-%> -%>
<% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %> <% unless error_messages.blank? %><tr><td colspan="3"><%= error_messages %></td></tr><% end %>
<% fields_for prefix, rate do |rate_form| %> <% fields_for prefix, rate do |rate_form| %>
<tr class="<%= classes %>" id="<%= id_prefix %>"> <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>
<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> <label class="hidden-for-sighted", for="<%= "#{id_prefix}_valid_from" %>"><%= l(:caption_valid_from) %></label>
<td><%= image_to_function 'delete.png', "var e = $('#{id_prefix}');parent=e.up();e.remove();recalculate_even_odd(parent)"%></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">
<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> </tr>
<% end %> <% end %>

@ -26,6 +26,7 @@
</table> </table>
<div> <div>
<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") %> <%= 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"} %> <%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {:class => "icon icon-add"} %>
</div> </div>

@ -1,10 +1,19 @@
<div class="contextual"> <% content_for :header_tags do %>
<%= 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)) %> <%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %> <% end %>
<%= 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']}) %> <% content_for :action_menu_main do %>
<%= 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_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_copy), {:controller => 'issue_moves', :action => 'new', :id => @issue, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy' %> <%= li_unless_nil(watcher_link(@issue,
<%= link_to_if_authorized l(:button_move), {:controller => 'issue_moves', :action => 'new', :id => @issue}, :class => 'icon icon-move' %> User.current,
<%= 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' %> { :class => 'watcher_link',
</div> :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_objects_title: Budgets
cost_types_title: Kostenarten cost_types_title: Kostenarten
cost_reports_title: Reports cost_reports_title: Reports
project_module_costs_module: Controlling project_module_costs_module: Controlling
currency_separator: "," currency_separator: ","
currency_delimiter: . currency_delimiter: .
x_entries: x_entries:
one: "1 Eintrag" one: "1 Eintrag"
other: "%{count} Einträge" other: "%{count} Einträge"
@ -35,9 +35,10 @@ de:
caption_costs: Kosten caption_costs: Kosten
caption_current_rate: Aktueller Satz caption_current_rate: Aktueller Satz
caption_default: Standard caption_default: Standard
field_rate: Satz
field_rates: Sätze
caption_default_rate_history_for: "Standardsatz-Historie f\xC3\xBCr %{user}" caption_default_rate_history_for: "Standardsatz-Historie f\xC3\xBCr %{user}"
caption_default_rates: "Standards\xC3\xA4tze" caption_default_rates: "Standards\xC3\xA4tze"
caption_deleted_at: "Gel\xC3\xB6scht am"
caption_fixed_date: Referenzdatum caption_fixed_date: Referenzdatum
caption_issue: Ticket caption_issue: Ticket
caption_labor: Personal caption_labor: Personal
@ -57,6 +58,7 @@ de:
caption_status: Status caption_status: Status
caption_subject: Titel caption_subject: Titel
caption_valid_from: "G\xC3\xBCltig ab" 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" 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_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_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." 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_objects_title: Budgets
cost_types_title: Cost Types cost_types_title: Cost Types
cost_reports_title: Cost Reports cost_reports_title: Cost Reports
@ -7,7 +7,7 @@ en:
currency_separator: . currency_separator: .
currency_delimiter: "," currency_delimiter: ","
x_entries: x_entries:
one: "1 Entry" one: "1 Entry"
other: "%{count} Entries" other: "%{count} Entries"
@ -34,9 +34,10 @@ en:
caption_costs: Costs caption_costs: Costs
caption_current_rate: Current Rate caption_current_rate: Current Rate
caption_default: Default caption_default: Default
field_rate: Rate
field_rates: Rates
caption_default_rate_history_for: Default Rate History for %{user} caption_default_rate_history_for: Default Rate History for %{user}
caption_default_rates: Default Rates caption_default_rates: Default Rates
caption_deleted_at: Deleted on
caption_fixed_date: Fixed Date caption_fixed_date: Fixed Date
caption_issue: Issue caption_issue: Issue
caption_labor: Labor caption_labor: Labor
@ -56,9 +57,10 @@ en:
caption_status: Status caption_status: Status
caption_subject: Title caption_subject: Title
caption_valid_from: Valid from 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." 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_budget_ratio: Spent Budget
field_client_signoff: Client signoff field_client_signoff: Client signoff
field_cost_object: Budget field_cost_object: Budget
@ -81,11 +83,11 @@ en:
field_unit_plural: Pluralized unit name field_unit_plural: Pluralized unit name
field_unit_price: Unit price field_unit_price: Unit price
field_units: Units field_units: Units
help_click_to_edit: Click here to edit. 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_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. help_override_rate: Enter a value here to override the default rate.
label_awaiting_client: Awaiting client response label_awaiting_client: Awaiting client response
label_status_awaiting_client: Awaiting client response label_status_awaiting_client: Awaiting client response
label_awaiting_manager: Awaiting manager response label_awaiting_manager: Awaiting manager response
@ -131,12 +133,12 @@ en:
label_variable_cost_object: Variable rate based budget label_variable_cost_object: Variable rate based budget
label_view_all_cost_objects: View all Budgets label_view_all_cost_objects: View all Budgets
label_yes: "Yes" label_yes: "Yes"
notice_cost_object_conflict: "Issues must be of the same project." notice_cost_object_conflict: "Issues must be of the same project."
notice_no_cost_objects_available: "No budgets available." notice_no_cost_objects_available: "No budgets available."
notice_something_wrong: Something went wrong. Please try again. notice_something_wrong: Something went wrong. Please try again.
notice_successful_restore: Successful restore. notice_successful_restore: Successful restore.
permission_block_tickets: Block tickets permission_block_tickets: Block tickets
permission_edit_cost_entries: Edit booked unit costs permission_edit_cost_entries: Edit booked unit costs
permission_edit_cost_objects: Edit Budgets permission_edit_cost_objects: Edit Budgets
@ -149,7 +151,7 @@ en:
permission_view_cost_rates: View cost rates permission_view_cost_rates: View cost rates
permission_view_hourly_rates: View all hourly rates permission_view_hourly_rates: View all hourly rates
permission_view_own_hourly_rate: View own hourly rate 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_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_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 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_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_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." 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
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' require_dependency 'costs_issue_observer'
# loading the class so that acts_as_journalized gets registered
VariableCostObject
end end
# Hooks # Hooks
@ -60,7 +62,7 @@ Redmine::Plugin.register :redmine_costs do
author 'Holger Just @ finnlabs' author 'Holger Just @ finnlabs'
author_url 'http://finn.de/team#h.just' author_url 'http://finn.de/team#h.just'
description 'The costs plugin provides basic cost management functionality for Redmine.' 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' 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'}, menu :project_menu, :cost_objects, {:controller => 'cost_objects', :action => 'index'},
:param => :project_id, :before => :settings, :caption => :cost_objects_title :param => :project_id, :before => :settings, :caption => :cost_objects_title
# Activities menu :project_menu, :new_budget, {:action => 'new', :controller => 'cost_objects' }, :param => :project_id, :caption => :label_cost_object_new, :parent => :cost_objects
activity_provider :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 end
# Observers # 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 # disabled for now, implements part of ticket blocking
alias_method_chain :validate, :cost_object 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 def spent_hours
# overwritten method # overwritten method
@spent_hours ||= self.time_entries.visible(User.current).sum(:hours) || 0 @spent_hours ||= self.time_entries.visible(User.current).sum(:hours) || 0

@ -24,7 +24,7 @@ module CostsUserPatch
before_save :save_rates before_save :save_rates
alias_method_chain :allowed_to?, :inheritance register_allowance_evaluator Costs::PrincipalAllowanceEvaluator::Costs
end end
end end
@ -37,8 +37,13 @@ module CostsUserPatch
perm = Redmine::AccessControl.permission(action) perm = Redmine::AccessControl.permission(action)
if perm.granular_for if perm.granular_for
allowed && users.include?(options[:for] || self) allowed && users.include?(options[:for] || self)
elsif !allowed && options[:for] && granulars = Redmine::AccessControl.permissions.select{|p| p.granular_for == perm} elsif !allowed &&
granulars.detect{|p| self.allowed_to? p.name, project, options} 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 else
allowed allowed
end end
@ -68,73 +73,6 @@ module CostsUserPatch
roles roles
end 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) def allowed_for(permission, projects = nil)
unless projects.nil? or projects.blank? unless projects.nil? or projects.blank?
projects = [projects] unless projects.is_a? Array projects = [projects] unless projects.is_a? Array
@ -248,20 +186,6 @@ module CostsUserPatch
roles = {} roles = {}
member_roles.each do |r| member_roles.each do |r|
roles[r.role] = [self] 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 end
roles roles
end 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 RAILS_ENV = "test" unless defined? RAILS_ENV
# prevent case where we are using rubygems and test-unit 2.x is installed require 'spec/spec_helper'
begin require 'redmine_factory_girl'
require 'rubygems' require 'identical_ext'
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
Fixtures.create_fixtures File.join(File.dirname(__FILE__), "fixtures"), ActiveRecord::Base.connection.tables Fixtures.create_fixtures File.join(File.dirname(__FILE__), "fixtures"), ActiveRecord::Base.connection.tables

Loading…
Cancel
Save