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 @@ <%= l(:field_labor) %> <%= l(:field_overhead) %> <%= l(:field_fixed) %> + <%= l(:field_total) %> @@ -135,6 +136,7 @@ <%= format_budget_for_deliverable(deliverable, deliverable.labor_budget_spent, deliverable.labor_budget_total, :class => 'labor') %> <%= format_budget_for_deliverable(deliverable, deliverable.overhead_spent, deliverable.overhead_budget_total, :class => 'overhead') %> <%= format_budget_for_deliverable(deliverable, deliverable.fixed_budget_total_spent, deliverable.fixed_budget_total, :class => 'fixed') %> + <%= format_budget_for_deliverable(deliverable, deliverable.total_spent, deliverable.total, :class => 'total') %> <% end %> diff --git a/app/views/deliverables/_details_row.html.erb b/app/views/deliverables/_details_row.html.erb index c301f60..98cc8c6 100644 --- a/app/views/deliverables/_details_row.html.erb +++ b/app/views/deliverables/_details_row.html.erb @@ -1,6 +1,6 @@ <% validated_period = validate_period(deliverable, period) %> - +
diff --git a/app/views/deliverables/_finance_form.html.erb b/app/views/deliverables/_finance_form.html.erb index f26472b..f006a5e 100644 --- a/app/views/deliverables/_finance_form.html.erb +++ b/app/views/deliverables/_finance_form.html.erb @@ -1,3 +1,21 @@ + + + + + + <% form.inputs :name => label, :class => "deliverable-finances #{fieldset_class}" do %>
  • <%= content_tag(:label, l(:field_labor)) %> - - <% form.fields_for :labor_budgets, labor_budgets do |labor_budget| %> - <%= labor_budget.hidden_field(:year) %> - <%= labor_budget.hidden_field(:month) %> - - - - - - + + <% form.fields_for :labor_budgets, labor_budgets.sort_by(&:id) do |labor_budget| %> + <%= render :partial => 'labor_budget_form', :locals => {:labor_budget => labor_budget} %> <% end %> +
    - <%= 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") %> -
  • <%= content_tag(:label, l(:field_overhead)) %> - - <% form.fields_for :overhead_budgets, overhead_budgets do |overhead_budget| %> - <%= overhead_budget.hidden_field(:year) %> - <%= overhead_budget.hidden_field(:month) %> - - - - - - + + <% form.fields_for :overhead_budgets, overhead_budgets.sort_by(&:id) do |overhead_budget| %> + <%= render :partial => 'overhead_budget_form', :locals => {:overhead_budget => overhead_budget} %> <% end %> +
    - <%= 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") %> -
  • @@ -62,34 +48,9 @@
    - <% form.fields_for :fixed_budgets, fixed_budgets do |fixed_budget| %> - <%= fixed_budget.hidden_field(:year) %> - <%= fixed_budget.hidden_field(:month) %> - -

    <%= 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.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 %>
    diff --git a/app/views/deliverables/_fixed_budget_form.html.erb b/app/views/deliverables/_fixed_budget_form.html.erb new file mode 100644 index 0000000..c65f85d --- /dev/null +++ b/app/views/deliverables/_fixed_budget_form.html.erb @@ -0,0 +1,33 @@ +
    + <%= fixed_budget.hidden_field(:id) unless fixed_budget.object.new_record? %> + <%= fixed_budget.hidden_field(:year) %> + <%= fixed_budget.hidden_field(:month) %> + +

    <%= 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.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;') %> +

    +
    diff --git a/app/views/deliverables/_form.html.erb b/app/views/deliverables/_form.html.erb index 064c115..2b2683b 100644 --- a/app/views/deliverables/_form.html.erb +++ b/app/views/deliverables/_form.html.erb @@ -1,6 +1,7 @@ <%= javascript_tag("var i18nStartDateEmpty = '#{l(:text_start_date_empty)}'") %> <%= javascript_tag("var i18nEndDateEmpty = '#{l(:text_end_date_empty)}'") %> <%= javascript_tag("var i18nChangedPeriodMessage = '#{l(:text_changed_period_message)}'") %> +<%= javascript_tag("var i18nAreYouSure = '#{l(:text_are_you_sure)}'") %> <% if resource.locked? || resource.closed? || resource.contract_locked? || resource.contract_closed? %>
    @@ -56,7 +57,7 @@ <% if resource.retainer? && resource.respond_to?(:months) %> <% if resource.months.present? %> <% resource.months.each do |month| %> -<%= render :partial => 'finance_form', :locals => {:form => form, :labor_budgets => resource.labor_budgets_for_date(month), :overhead_budgets => resource.overhead_budgets_for_date(month), :fixed_budgets => resource.fixed_budgets_for_date(month), :label => l(:text_deliverable_finances_date, :date => month.strftime("%B, %Y")), :fieldset_class => 'date-' + month.strftime('%Y-%m') } %> +<%= render :partial => 'finance_form', :locals => {:resource => resource, :form => form, :labor_budgets => resource.labor_budgets_for_date(month), :overhead_budgets => resource.overhead_budgets_for_date(month), :fixed_budgets => resource.fixed_budgets_for_date(month), :label => l(:text_deliverable_finances_date, :date => month.strftime("%B, %Y")), :fieldset_class => 'date-' + month.strftime('%Y-%m') } %> <% end %> <% else %> <%= content_tag(:p, l(:text_missing_period), :class => 'nodata') %> diff --git a/app/views/deliverables/_labor_budget_form.html.erb b/app/views/deliverables/_labor_budget_form.html.erb new file mode 100644 index 0000000..289302c --- /dev/null +++ b/app/views/deliverables/_labor_budget_form.html.erb @@ -0,0 +1,23 @@ + + + <%= labor_budget.hidden_field(:id) unless labor_budget.object.new_record? %> + <%= labor_budget.hidden_field(:year) %> + <%= labor_budget.hidden_field(:month) %> + + <%= labor_budget.label(:time_entry_activity_id, :class => "hidden") %> + <%= labor_budget.select(:time_entry_activity_id, options_from_collection_for_select(@project.billable_activities, :id, :name, labor_budget.object.time_entry_activity_id), {:include_blank => false}, {:class => 'financial'}) %> + + +

    <%= 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') %> + + + <%= labor_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), 'addNewDeliverableLaborItem()', :class => 'add icon icon-add', :style => 'display:none;') %> + + diff --git a/app/views/deliverables/_overhead_budget_form.html.erb b/app/views/deliverables/_overhead_budget_form.html.erb new file mode 100644 index 0000000..89f7ddb --- /dev/null +++ b/app/views/deliverables/_overhead_budget_form.html.erb @@ -0,0 +1,22 @@ + + + <%= overhead_budget.hidden_field(:year) %> + <%= overhead_budget.hidden_field(:month) %> + + <%= overhead_budget.label(:time_entry_activity_id, :class => "hidden") %> + <%= overhead_budget.select(:time_entry_activity_id, options_from_collection_for_select(@project.non_billable_activities, :id, :name, overhead_budget.object.time_entry_activity_id), {:include_blank => false}, {: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') %> + + + <%= overhead_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), 'addNewDeliverableOverheadItem()', :class => 'add icon icon-add', :style => 'display:none;') %> + + diff --git a/assets/javascripts/contracts.js b/assets/javascripts/contracts.js index 3ac36be..dbe5d7a 100644 --- a/assets/javascripts/contracts.js +++ b/assets/javascripts/contracts.js @@ -68,6 +68,88 @@ jQuery(function($) { } }, + showDeliverableAddButtons = function() { + var laborLinks = $('table.deliverable_finance_table .add-labor a.add') + if (laborLinks.length == 0) { + // No link, add a blank form + addNewDeliverableLaborItem(); + } else { + laborLinks.hide().last().show(); + } + var overheadLinks = $('table.deliverable_finance_table .add-overhead a.add') + if (overheadLinks.length == 0) { + // No link, add a blank form + addNewDeliverableOverheadItem(); + } else { + overheadLinks.hide().last().show(); + } + var fixedLinks = $('#deliverable-fixed .fixed-budget-form .add-fixed a.add') + if (fixedLinks.length == 0) { + // No link, add a blank form + addNewDeliverableFixedItem(); + } else { + fixedLinks.hide().last().show(); + } + }, + + addNewDeliverableLaborItem = function() { + addNewDeliverableFinance('#labor-budget-template', + '#deliverable-labor tbody', + $("tr.labor-budget-form").size(), + ''); + }, + + addNewDeliverableOverheadItem = function() { + addNewDeliverableFinance('#overhead-budget-template', + '#deliverable-overhead tbody', + $("tr.overhead-budget-form").size(), + ''); + }, + + addNewDeliverableFixedItem = function() { + addNewDeliverableFinance('#fixed-budget-template', + '#deliverable-fixed.fixed-item-form', + $("div.fixed-budget-form").size(), + '
    '); + }, + + addNewDeliverableFinance = function(templateSelector, appendTemplateTo, countOfExisting, wrapperElement) { + var t = $(templateSelector).tmpl({}); + if (t.length > 0) { + var recordLocation = countOfExisting + 1; // increments the Rails [n] placeholder + var newContent = t.html().replace(/\[0\]/g, "[" + recordLocation + "]"); + // New ids for textareas for the jsToolBar to attach to + newContent = newContent.replace(/fixed-description\d*/g, "fixed-description" + Math.floor(Math.random() * 100000000)) + var newItem = $(wrapperElement).html(newContent) + + newItem.appendTo(appendTemplateTo); + newItem.find("textarea.wiki-edit").each(function () { + attachWikiToolbar(this.id); + }); + showDeliverableAddButtons(); + } + }, + + // Set the deleted flag for Rails and move it out of the row into + // a hidden table + deleteDeliverableFinance = function(deleteLink) { + if (confirm(i18nAreYouSure)) { + $(deleteLink).parent().find('.delete-flag').val('1') + if ($('#deleted-finances').length == 0) { + $(deleteLink). + closest("form"). + append($("")); + } + $('#deleted-finances').append( + $(deleteLink). // + parent(). // + parent().hide() + ); // + showDeliverableAddButtons(); + } + }, + + showDeliverableAddButtons(); toggleSpecificDeliverableFields($('form.deliverable')); $('select#deliverable_type').change(function() { @@ -125,3 +207,9 @@ jQuery(function($) { } })(jQuery); + +// Global functions outside of jQuery scoping +function attachWikiToolbar(id) { + var jsToolBarInstance = new jsToolBar($(id)); + jsToolBarInstance.draw(); +} diff --git a/assets/javascripts/jquery.tmpl.min.js b/assets/javascripts/jquery.tmpl.min.js new file mode 100644 index 0000000..f08e81d --- /dev/null +++ b/assets/javascripts/jquery.tmpl.min.js @@ -0,0 +1 @@ +(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=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