diff --git a/Rakefile b/Rakefile index a1bee84..5b7fa75 100755 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each RedminePluginSupport::Base.setup do |plugin| plugin.project_name = 'redmine_contracts' plugin.default_task = [:test] - plugin.tasks = [:db, :doc, :release, :clean, :test, :stats, :metrics] + plugin.tasks = [:db, :doc, :release, :clean, :test, :stats] # TODO: gem not getting this automaticly plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../') end diff --git a/app/models/deliverable.rb b/app/models/deliverable.rb index 78a395d..9a74526 100644 --- a/app/models/deliverable.rb +++ b/app/models/deliverable.rb @@ -11,9 +11,9 @@ class Deliverable < ActiveRecord::Base has_many :fixed_budgets has_many :issues, :dependent => :nullify - accepts_nested_attributes_for :labor_budgets - accepts_nested_attributes_for :overhead_budgets - accepts_nested_attributes_for :fixed_budgets + accepts_nested_attributes_for :labor_budgets, :allow_destroy => true + accepts_nested_attributes_for :overhead_budgets, :allow_destroy => true + accepts_nested_attributes_for :fixed_budgets, :allow_destroy => true # Validations validates_presence_of :title @@ -31,7 +31,8 @@ class Deliverable < ActiveRecord::Base delegate "open?", :to => :contract, :prefix => true, :allow_nil => true delegate "closed?", :to => :contract, :prefix => true, :allow_nil => true delegate "locked?", :to => :contract, :prefix => true, :allow_nil => true - + delegate :project, :to => :contract, :allow_nil => true + # Callbacks before_destroy :block_on_locked_contracts before_destroy :block_on_closed_contracts @@ -280,6 +281,11 @@ class Deliverable < ActiveRecord::Base end end + # Required attribute for AAJ's JournalFormatter + def name + title + end + # Accessors from the budget plugin that need to be wrapped def subject warn "[DEPRECATION] Deliverable#subject is deprecated. Please use Deliverable#title instead." diff --git a/app/models/labor_budget.rb b/app/models/labor_budget.rb index 831fc91..71c0319 100644 --- a/app/models/labor_budget.rb +++ b/app/models/labor_budget.rb @@ -3,8 +3,10 @@ class LaborBudget < ActiveRecord::Base # Associations belongs_to :deliverable + belongs_to :time_entry_activity # Validations + validates_presence_of :time_entry_activity_id # Accessors include DollarizedAttribute diff --git a/app/models/overhead_budget.rb b/app/models/overhead_budget.rb index ebc13b4..518c549 100644 --- a/app/models/overhead_budget.rb +++ b/app/models/overhead_budget.rb @@ -3,8 +3,10 @@ class OverheadBudget < ActiveRecord::Base # Associations belongs_to :deliverable + belongs_to :time_entry_activity # Validations + validates_presence_of :time_entry_activity_id # Accessors include DollarizedAttribute diff --git a/app/views/contracts/show.html.erb b/app/views/contracts/show.html.erb index 79e0e81..618d1b5 100644 --- a/app/views/contracts/show.html.erb +++ b/app/views/contracts/show.html.erb @@ -121,6 +121,7 @@
| - <%= release(3, "Select field for the Time Entry Activity in a td") %> - | -
- <%= labor_budget.label(:hours, l(:text_short_hours)) %> - <%= labor_budget.text_field(:hours, :value => format_deliverable_value_fields(labor_budget.object.hours), :class => 'financial') %> - |
-
- <%= labor_budget.label(:budget, l(:text_dollar_sign)) %> - <%= labor_budget.text_field(:budget, :value => format_deliverable_value_fields(labor_budget.object.budget), :class => 'financial') %> - |
- - <%= release(3, "Green Add button for multiple records") %> - | -
| - <%= release(3, "Select field for the Time Entry Activity in a td") %> - | -
- <%= overhead_budget.label(:hours, l(:text_short_hours)) %> - <%= overhead_budget.text_field(:hours, :value => format_deliverable_value_fields(overhead_budget.object.hours),:class => 'financial') %> - |
-
- <%= overhead_budget.label(:budget, l(:text_dollar_sign)) %> - <%= overhead_budget.text_field(:budget, :value => format_deliverable_value_fields(overhead_budget.object.budget), :class => 'financial') %> - |
- - <%= release(3, "Green Add button for multiple records") %> - | -
<%= fixed_budget.label(:title, l(:field_title))%> - <%= fixed_budget.text_field(:title) %> -
- -- <%= fixed_budget.label(:budget, l(:field_budget))%> <%= l(:text_dollar_sign) %> - <%= fixed_budget.text_field(:budget, :value => format_deliverable_value_fields(fixed_budget.object.budget), :class => 'financial') %> -
- -- <%= fixed_budget.label(:markup, l(:field_markup)) %> <%= l(:field_discount_hint) %> - <%= fixed_budget.text_field(:markup, :value => format_deliverable_value_fields_as_dollar_or_percent(fixed_budget.object.markup), :class => 'financial') %> -
- -- <%= fixed_budget.label(:paid, l(:field_paid)) %> - <%= fixed_budget.check_box(:paid) %> -
- -<%= fixed_budget.label(:description, l(:field_description), :for => "fixed-description#{fixed_budget.object.object_id}")%>
- <%= fixed_budget.text_area(:description, :class => 'wiki-edit', :rows => '5', :id => "fixed-description#{fixed_budget.object.object_id}") %> + <% form.fields_for :fixed_budgets, fixed_budgets.sort_by(&:id) do |fixed_budget| %> + <%= render :partial => 'fixed_budget_form', :locals => {:fixed_budget => fixed_budget} %> <%= wikitoolbar_for "fixed-description#{fixed_budget.object.object_id}" %> - -<%= release(3, "Green Add button for multiple records") %>
<% end %><%= fixed_budget.label(:title, l(:field_title))%> + <%= fixed_budget.text_field(:title) %> +
+ ++ <%= fixed_budget.label(:budget, l(:field_budget))%> <%= l(:text_dollar_sign) %> + <%= fixed_budget.text_field(:budget, :value => format_deliverable_value_fields(fixed_budget.object.budget), :class => 'financial') %> +
+ ++ <%= fixed_budget.label(:markup, l(:field_markup)) %> <%= l(:field_discount_hint) %> + <%= fixed_budget.text_field(:markup, :value => format_deliverable_value_fields_as_dollar_or_percent(fixed_budget.object.markup), :class => 'financial') %> +
+ ++ <%= fixed_budget.label(:paid, l(:field_paid)) %> + <%= fixed_budget.check_box(:paid) %> +
+ +<%= fixed_budget.label(:description, l(:field_description), :for => "fixed-description#{fixed_budget.object.object_id}")%>
+ <%= fixed_budget.text_area(:description, :class => 'wiki-edit', :rows => '5', :id => "fixed-description#{fixed_budget.object.object_id}") %> + ++ <%= fixed_budget.hidden_field "_destroy", :class=> "delete-flag" %> + <%= link_to_function(l(:button_delete), 'deleteDeliverableFinance(this)', :class => 'delete icon icon-del') %> + <%= link_to_function(l(:button_add), 'addNewDeliverableFixedItem()', :class => 'add icon icon-add', :style => 'display:none;') %> +
+<%= labor_budget.label(:hours, l(:text_short_hours)) %>
+ <%= labor_budget.text_field(:hours, :value => format_deliverable_value_fields(labor_budget.object.hours), :class => 'financial') %> +<%= labor_budget.label(:budget, l(:text_dollar_sign)) %>
+ <%= labor_budget.text_field(:budget, :value => format_deliverable_value_fields(labor_budget.object.budget), :class => 'financial') %> +<%= overhead_budget.label(:hours, l(:text_short_hours)) %>
+ <%= overhead_budget.text_field(:hours, :value => format_deliverable_value_fields(overhead_budget.object.hours),:class => 'financial') %> +<%= overhead_budget.label(:budget, l(:text_dollar_sign)) %>
+ <%= overhead_budget.text_field(:budget, :value => format_deliverable_value_fields(overhead_budget.object.budget), :class => 'financial') %> +=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) \ No newline at end of file diff --git a/assets/stylesheets/redmine_contracts.css b/assets/stylesheets/redmine_contracts.css index 20b0b48..e6e7717 100644 --- a/assets/stylesheets/redmine_contracts.css +++ b/assets/stylesheets/redmine_contracts.css @@ -26,6 +26,7 @@ html>body .tabular li {overflow:hidden;} .tabular li.hidden { height: 0; padding: 0; margin: 0; } /* End tabular */ +.hidden { display: none; } a.contract-delete {color: red; } .overage { color: #A40000; } @@ -521,3 +522,5 @@ input.financial{ #edit_contract_1 li#contract_billable_rate_input div{ margin-bottom: 10px; } + +div.fixed-budget-form {padding: 5px 0;} diff --git a/config/locales/en.yml b/config/locales/en.yml index acb3852..fc61d00 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -100,3 +100,4 @@ en: text_deliverable_closed_warning: "This deliverable is closed and cannot be saved without changing it's status to Open." text_contract_locked_warning: "This contract is locked and cannot be saved without changing it's status to Open." text_contract_closed_warning: "This contract is closed and cannot be saved without changing it's status to Open." + field_time_entry_activity: "Activity" diff --git a/db/migrate/020_add_time_entry_activity_id_to_labor_budgets.rb b/db/migrate/020_add_time_entry_activity_id_to_labor_budgets.rb new file mode 100644 index 0000000..95f9251 --- /dev/null +++ b/db/migrate/020_add_time_entry_activity_id_to_labor_budgets.rb @@ -0,0 +1,10 @@ +class AddTimeEntryActivityIdToLaborBudgets < ActiveRecord::Migration + def self.up + add_column :labor_budgets, :time_entry_activity_id, :integer + add_index :labor_budgets, :time_entry_activity_id + end + + def self.down + remove_column :labor_budgets, :time_entry_activity_id + end +end diff --git a/db/migrate/021_add_time_entry_activity_id_to_overhead_budgets.rb b/db/migrate/021_add_time_entry_activity_id_to_overhead_budgets.rb new file mode 100644 index 0000000..66ea53c --- /dev/null +++ b/db/migrate/021_add_time_entry_activity_id_to_overhead_budgets.rb @@ -0,0 +1,10 @@ +class AddTimeEntryActivityIdToOverheadBudgets < ActiveRecord::Migration + def self.up + add_column :overhead_budgets, :time_entry_activity_id, :integer + add_index :overhead_budgets, :time_entry_activity_id + end + + def self.down + remove_column :overhead_budgets, :time_entry_activity_id + end +end diff --git a/lib/redmine_contracts/budget_plugin_migration.rb b/lib/redmine_contracts/budget_plugin_migration.rb index 63e5260..db055c7 100644 --- a/lib/redmine_contracts/budget_plugin_migration.rb +++ b/lib/redmine_contracts/budget_plugin_migration.rb @@ -103,7 +103,8 @@ module RedmineContracts if old_deliverable['total_hours'].present? || old_deliverable['cost_per_hour'].present? deliverable.labor_budgets << LaborBudget.new(:deliverable => deliverable, :budget => @total_cost, - :hours => old_deliverable['total_hours']) + :hours => old_deliverable['total_hours'], + :time_entry_activity => first_billable_activity(project)) end else @total_cost = 0 @@ -170,7 +171,8 @@ module RedmineContracts deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable, :budget => old_deliverable['overhead'], - :hours => hours.to_f.round(2)) + :hours => hours.to_f.round(2), + :time_entry_activity => first_non_billable_activity(deliverable.project)) elsif old_deliverable['overhead_percent'].present? overhead = total * (old_deliverable['overhead_percent'].to_f / 100) if @overhead_rate != 0 @@ -181,7 +183,8 @@ module RedmineContracts deliverable.overhead_budgets << OverheadBudget.new(:deliverable => deliverable, :budget => overhead, - :hours => hours.to_f.round(2)) + :hours => hours.to_f.round(2), + :time_entry_activity => first_non_billable_activity(deliverable.project)) end end @@ -228,5 +231,13 @@ module RedmineContracts def self.append_old_deliverable_to_notes(old_deliverable, new_deliverable) new_deliverable.notes += "Converted data:\n
" + old_deliverable.pretty_inspect + "" end + + def self.first_billable_activity(project) + project.billable_activities.first || TimeEntryActivity.first + end + + def self.first_non_billable_activity(project) + project.non_billable_activities.first || TimeEntryActivity.first + end end end diff --git a/lib/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook.rb b/lib/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook.rb index d3af4c8..86c9aa2 100644 --- a/lib/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook.rb +++ b/lib/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook.rb @@ -8,6 +8,11 @@ module RedmineContracts # * :detail => Detail about the journal change # def helper_issues_show_detail_after_setting(context = { }) + # This will be skipped in ChiliProject 2.x because + # acts_as_journalized overrides the prop_key with the label + # 'deliverable_id' becomes 'Deliverable' (i18n) + # + # register_on_journal_formatter is used for ChiliProject 2.x support # TODO Later: Overwritting the caller is bad juju if context[:detail].prop_key == 'deliverable_id' context[:detail].reload diff --git a/lib/redmine_contracts/hooks/view_layouts_base_html_head_hook.rb b/lib/redmine_contracts/hooks/view_layouts_base_html_head_hook.rb index 01d054b..f6f1301 100644 --- a/lib/redmine_contracts/hooks/view_layouts_base_html_head_hook.rb +++ b/lib/redmine_contracts/hooks/view_layouts_base_html_head_hook.rb @@ -9,6 +9,7 @@ module RedmineContracts return stylesheet_link_tag("redmine_contracts", :plugin => "redmine_contracts", :media => "screen") + javascript_include_tag('jquery-1.4.2.min.js', :plugin => 'redmine_contracts') + + javascript_include_tag('jquery.tmpl.min.js', :plugin => 'redmine_contracts') + javascript_tag('jQuery.noConflict();') + javascript_include_tag('contracts.js', :plugin => 'redmine_contracts') diff --git a/lib/redmine_contracts/patches/issue_patch.rb b/lib/redmine_contracts/patches/issue_patch.rb index f1157ad..f6bf31b 100644 --- a/lib/redmine_contracts/patches/issue_patch.rb +++ b/lib/redmine_contracts/patches/issue_patch.rb @@ -12,6 +12,17 @@ module RedmineContracts delegate :title, :to => :deliverable, :prefix => true, :allow_nil => true delegate :contract, :to => :deliverable, :allow_nil => true + # ChiliProject 2.x support for acts_as_journalized. + # Used to format the journal details on the Issue page + # + # See RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHook + # for <2.x and Redmine version + # + # TODO: Will not support permissions or custom code in the formatter. + if Issue.respond_to?(:register_on_journal_formatter) + register_on_journal_formatter(:named_association, 'deliverable_id') + end + def contract_name contract.try(:name) end diff --git a/lib/redmine_contracts/patches/project_patch.rb b/lib/redmine_contracts/patches/project_patch.rb index f462171..195b7eb 100644 --- a/lib/redmine_contracts/patches/project_patch.rb +++ b/lib/redmine_contracts/patches/project_patch.rb @@ -16,6 +16,27 @@ module RedmineContracts end module InstanceMethods + def billable_activities + activities_sorted_by_billable[:billable] + end + + def non_billable_activities + activities_sorted_by_billable[:non_billable] + end + + private + + def activities_sorted_by_billable + split_activities = activities.partition do |activity| + activity.billable? + end + + { + :billable => split_activities.first, + :non_billable => split_activities.second + } + + end end end end diff --git a/test/integration/contracts_show_test.rb b/test/integration/contracts_show_test.rb index 46884e8..31aad71 100644 --- a/test/integration/contracts_show_test.rb +++ b/test/integration/contracts_show_test.rb @@ -261,7 +261,52 @@ class ContractsShowTest < ActionController::IntegrationTest assert_select "td.fixed.spent-amount", :text => /1,000/ end end - + + should "show the total budget for a Deliverable" do + @manager = User.generate! + + @deliverable1 = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :total => '5404') + + visit_contract_page(@contract) + assert_select "table#deliverables" do + assert_select "td.total.total-amount", :text => /5,404/ + end + + end + + should "show the total spent for a Deliverable" do + configure_overhead_plugin + @manager = User.generate! + @contract.billable_rate = 200 + assert @contract.save + + @deliverable1 = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :total => '1504') + + @issue1 = Issue.generate_for_project!(@project) + @time_entry1 = TimeEntry.generate!(:issue => @issue1, + :project => @project, + :activity => @billable_activity, + :spent_on => Date.today, + :hours => 15, + :user => @manager) + + @rate = Rate.generate!(:project => @project, + :user => @manager, + :date_in_effect => Date.yesterday, + :amount => 100) + + @deliverable1.issues << @issue1 + + assert_equal 1, @deliverable1.issues.count + + visit_contract_page(@contract) + assert_select "table#deliverables" do + # Using the contract billable rate and not the user rate because it's income, not an expense + assert_select "td.total.spent-amount", :text => /3,000/ + end + + end + should "show each fixed budget item in the details for the Deliverable" do @manager = User.generate! diff --git a/test/integration/deliverables_edit_test.rb b/test/integration/deliverables_edit_test.rb index 8d1b569..0f5f155 100644 --- a/test/integration/deliverables_edit_test.rb +++ b/test/integration/deliverables_edit_test.rb @@ -13,6 +13,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest @hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly') @user = User.generate_user_with_permission_to_manage_budget(:project => @project) + configure_overhead_plugin login_as(@user.login, 'contracts') end @@ -420,10 +421,12 @@ class DeliverablesEditTest < ActionController::IntegrationTest # * labor hidden month # * labor hours # * labor amount + # * labor deleted (hidden) # * overhead hidden year # * overhead hidden month # * overhead hours # * overhead amount + # * overhead deleted (hidden) # * fixed hidden year # * fixed hidden month # * fixed title @@ -431,9 +434,10 @@ class DeliverablesEditTest < ActionController::IntegrationTest # * fixed markup # * fixed paid checkbox # * fixed paid hidden field + # * fixed deleted (hidden) # * total (hidden) assert_select ".date-2010-01" do - assert_select "input", :count => 16 + assert_select "input", :count => 19 assert_select "textarea.wiki-edit", :count => 1 # Fixed description end diff --git a/test/integration/deliverables_new_test.rb b/test/integration/deliverables_new_test.rb index 97edac4..37dae03 100644 --- a/test/integration/deliverables_new_test.rb +++ b/test/integration/deliverables_new_test.rb @@ -7,6 +7,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest @project = Project.generate!(:identifier => 'main') @contract = Contract.generate!(:project => @project) @user = User.generate_user_with_permission_to_manage_budget(:project => @project) + configure_overhead_plugin login_as(@user.login, 'contracts') end @@ -231,11 +232,13 @@ class DeliverablesNewTest < ActionController::IntegrationTest end within("#deliverable-labor") do + select @billable_activity.name, :from => 'Activity' fill_in "hrs", :with => '20' fill_in "$", :with => '$2,000' end within("#deliverable-overhead") do + select @non_billable_activity.name, :from => 'Activity' fill_in "hrs", :with => '10' fill_in "$", :with => '$1,000' end @@ -258,11 +261,13 @@ class DeliverablesNewTest < ActionController::IntegrationTest @labor_budget = @deliverable.labor_budgets.first assert_equal 20, @labor_budget.hours assert_equal 2000.0, @labor_budget.budget + assert_equal @billable_activity, @labor_budget.time_entry_activity assert_equal 1, @deliverable.overhead_budgets.count @overhead_budget = @deliverable.overhead_budgets.first assert_equal 10, @overhead_budget.hours assert_equal 1000.0, @overhead_budget.budget + assert_equal @non_billable_activity, @overhead_budget.time_entry_activity assert_equal 1, @deliverable.fixed_budgets.count @fixed_budget = @deliverable.fixed_budgets.first diff --git a/test/test_helper.rb b/test/test_helper.rb index ac37e49..e5e5c02 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -92,6 +92,12 @@ class ActionController::IntegrationTest end class ActiveSupport::TestCase + begin + require 'ruby_gc_test_patch' + include RubyGcTestPatch + rescue LoadError + end + def configure_overhead_plugin @custom_field = TimeEntryActivityCustomField.generate! Setting['plugin_redmine_overhead'] = { diff --git a/test/unit/labor_budget_test.rb b/test/unit/labor_budget_test.rb index f264ce5..9d44c98 100644 --- a/test/unit/labor_budget_test.rb +++ b/test/unit/labor_budget_test.rb @@ -2,6 +2,9 @@ require File.dirname(__FILE__) + '/../test_helper' class LaborBudgetTest < ActiveSupport::TestCase should_belong_to :deliverable + should_belong_to :time_entry_activity + + should_validate_presence_of :time_entry_activity_id context "#budget=" do should "strip dollar signs when writing" do diff --git a/test/unit/overhead_budget_test.rb b/test/unit/overhead_budget_test.rb index 38a3a31..79722b9 100644 --- a/test/unit/overhead_budget_test.rb +++ b/test/unit/overhead_budget_test.rb @@ -2,6 +2,9 @@ require File.dirname(__FILE__) + '/../test_helper' class OverheadBudgetTest < ActiveSupport::TestCase should_belong_to :deliverable + should_belong_to :time_entry_activity + + should_validate_presence_of :time_entry_activity_id context "#budget=" do should "strip dollar signs when writing" do