diff --git a/app/controllers/deliverables_controller.rb b/app/controllers/deliverables_controller.rb index 4b11e2fb84..2f5c3a2e10 100644 --- a/app/controllers/deliverables_controller.rb +++ b/app/controllers/deliverables_controller.rb @@ -22,8 +22,11 @@ class DeliverablesController < ApplicationController include SortHelper helper :projects include ProjectsHelper + helper :attachments + include AttachmentsHelper helper :costlog include CostlogHelper + include Redmine::Export::PDF def index # TODO: This is a very naiive implementation. @@ -31,6 +34,12 @@ class DeliverablesController < ApplicationController # (see issues_controller.rb) limit = per_page_option + respond_to do |format| + format.html { } + format.csv { limit = Setting.issues_export_limit.to_i } + format.pdf { limit = Setting.issues_export_limit.to_i } + end + sort_columns = {'id' => "#{Deliverable.table_name}.id", 'subject' => "#{Deliverable.table_name}.subject", @@ -53,6 +62,8 @@ class DeliverablesController < ApplicationController respond_to do |format| format.html { render :action => 'index', :layout => !request.xhr? } + format.csv { send_data(deliverables_to_csv(@deliverables, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } + format.pdf { send_data(deliverables_to_pdf(@deliverables, @project), :type => 'application/pdf', :filename => 'export.pdf') } end end @@ -75,7 +86,7 @@ class DeliverablesController < ApplicationController end @deliverable ||= Deliverable.new - @deliverable.project_id = @project.id unless @deliverable.project + @deliverable.project_id = @project.id @deliverable.author_id = User.current.id # fixed_date must be set before deliverable_costs and deliverable_hours @@ -99,11 +110,37 @@ class DeliverablesController < ApplicationController end end - @deliverable.deliverable_costs.build - @deliverable.deliverable_hours.build + case @deliverable.kind + when CostBasedDeliverable.name + @deliverable.deliverable_costs.build + @deliverable.deliverable_hours.build + end + render :layout => !request.xhr? end + # Blacklist of attributes which can not be updated in edit + EDIT_BLACK_LIST = %w(kind type) + + def edit + if params[:deliverable] + attrs = params[:deliverable].dup + attrs.delete_if {|k,v| EDIT_BLACK_LIST.include?(k)} + + @deliverable.attributes = attrs + end + + if request.post? + if @deliverable.save + flash[:notice] = l(:notice_successful_update) + redirect_to(params[:back_to] || {:action => 'show', :id => @deliverable}) + end + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash.now[:error] = l(:notice_locking_conflict) + end + def preview @deliverable = Deliverables.find_by_id(params[:id]) unless params[:id].blank? @text = params[:notes] || (params[:deliverable] ? params[:deliverable][:description] : nil) @@ -207,19 +244,4 @@ private rescue ActiveRecord::RecordNotFound render_404 end - - def desired_type - if params[:deliverable] - case params[:deliverable].delete(:desired_type) - when "FixedDeliverable" - FixedDeliverable - when "CostBasedDeliverable" - CostBasedDeliverable - else - Deliverable - end - else - Deliverable - end - end end \ No newline at end of file diff --git a/app/helpers/costlog_helper.rb b/app/helpers/costlog_helper.rb index 4da912430b..8942d9e84e 100644 --- a/app/helpers/costlog_helper.rb +++ b/app/helpers/costlog_helper.rb @@ -69,5 +69,17 @@ module CostlogHelper export end + def extended_progress_bar(pcts, options={}) + return progress_bar(pcts, options) unless pcts.is_a?(Numeric) && pcts > 100 + + width = options[:width] || '100px;' + legend = options[:legend] || '' + content_tag('table', + content_tag('tr', + content_tag('td', '', :style => "width: 100%;", :class => 'exceeded') + ), :class => 'progress', :style => "width: #{width};") + + content_tag('p', legend, :class => 'pourcent') + end + end \ No newline at end of file diff --git a/app/helpers/deliverables_helper.rb b/app/helpers/deliverables_helper.rb index 755ae53c11..d04352e25b 100644 --- a/app/helpers/deliverables_helper.rb +++ b/app/helpers/deliverables_helper.rb @@ -7,44 +7,56 @@ module DeliverablesHelper return User.current.allowed_to?(:edit_deliverables, @project) end - def allowed_projects_for_edit - cond = ARCondition.new - cond << Project.allowed_to_condition(User.current, :edit_deliverables) - - projects = User.current.projects.find(:all, :conditions => cond.conditions, :include => :parent).group_by(&:root) - - result = [] - projects.keys.sort.each do |root| - result << root - projects[root].sort.each do |project| - next if project == root - project.name = '» ' + h(project.name) - result << project - end - end - result - end - - def add_deliverable_cost_link(name) - link_to_function name do |page| - page.insert_html :bottom, :deliverable_costs_body, :partial => "deliverable_cost", :object => DeliverableCost.new - end - end - def fields_for_deliverable_cost(deliverable_cost, &block) prefix = deliverable_cost.new_record? ? "new" : "existing" fields_for("deliverable[#{prefix}_deliverable_cost_attributes][]", deliverable_cost, &block) end - - def function_update_deliverable_cost - #remote_function(:url => {:action => :update_deliverable_costs_row}, - # :with => "'cost_type_id=' + encodeURIComponent(value) + '&units=' + encodeURIComponent(this.parentElement.previousSibling.firstChild.value) + '&element_id=deliverable_costs#{suffix}_#{row_id}'") %> - end - def add_deliverable_hour_link(name) - link_to_remote name do |page| - page.insert_html :bottom, "deliverable_hours_body", :partial => "deliverable_hour", :object => DeliverableHour.new + def deliverables_to_csv(deliverables, project) + ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + decimal_separator = l(:general_csv_decimal_separator) + export = StringIO.new + CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| + # csv header fields + headers = [ "#", + l(:field_status), + l(:field_project), + l(:field_subject), + l(:field_score), + l(:field_author), + l(:field_fixed_date), + l(:field_materials_budget), + l(:field_labor_budget), + l(:field_spent), + l(:field_created_on), + l(:field_updated_on), + l(:field_description) + ] + csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + # csv lines + deliverables.each do |issue| + fields = [issue.id, + deliverable.status.name, + deliverable.project.name, + deliverable.subject, + deliverable.score, + deliverable.author.name, + format_date(deliverable.fixed_date), + deliverable.kind == "CostBasedDeliverable" ? number_to_currency(deliverable.materials_budget) : "", + deliverable.kind == "CostBasedDeliverable" ? number_to_currency(deliverable.labor_budget) : "", + deliverable.kind == "CostBasedDeliverable" ? number_to_currency(deliverable.spent) : "", + format_time(deliverable.created_on), + format_time(deliverable.updated_on), + deliverable.description + ] + csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + end end + export.rewind + export end + + + end \ No newline at end of file diff --git a/app/models/cost_based_deliverable.rb b/app/models/cost_based_deliverable.rb index 8323757118..9fd9497427 100644 --- a/app/models/cost_based_deliverable.rb +++ b/app/models/cost_based_deliverable.rb @@ -5,6 +5,11 @@ class CostBasedDeliverable < Deliverable validates_associated :deliverable_costs validates_associated :deliverable_hours + def before_save + # set budget to correct value + self.budget = deliverable_hours.inject(0.0) {|sum,d| sum + d.costs} + deliverable_costs.inject(0.0) {|sum, d| d.costs + sum} + end + def copy_from(arg) deliverable = arg.is_a?(CostBasedDeliverable) ? arg : CostBasedDeliverable.find(arg) self.attributes = deliverable.attributes.dup @@ -17,6 +22,22 @@ class CostBasedDeliverable < Deliverable return l(:label_cost_based_deliverable) end + def materials_budget + if User.current.allowed_to?(:view_unit_price, project) + deliverable_costs.inject(0.0) {|sum, d| d.costs + sum} + else + nil + end + end + + def labor_budget + if User.current.allowed_to?(:view_all_rates, project) + deliverable_hours.inject(0.0) {|sum,d| sum + d.costs} + else + nil + end + end + def new_deliverable_cost_attributes=(deliverable_cost_attributes) deliverable_cost_attributes.each do |index, attributes| deliverable_costs.build(attributes) if attributes[:units].to_i > 0 diff --git a/app/models/deliverable.rb b/app/models/deliverable.rb index 57d41455d9..eaeb33d88d 100644 --- a/app/models/deliverable.rb +++ b/app/models/deliverable.rb @@ -2,19 +2,23 @@ # contain a collection of issues. class Deliverable < ActiveRecord::Base unloadable - validates_presence_of :subject belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' belongs_to :project has_many :issues + acts_as_attachable :after_remove => :attachment_removed + acts_as_event :title => Proc.new {|o| "#{l(:label_deliverable)} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'deliverables', :action => 'show', :id => o.id}} acts_as_activity_provider :find_options => {:include => [:project, :author]}, :timestamp => "#{table_name}.updated_on", :author_key => :author_id - + + validates_presence_of :subject, :project, :author + validates_length_of :subject, :maximum => 255 + def copy_from(arg) deliverable = arg.is_a?(Deliverable) ? arg : Deliverable.find(arg) self.attributes = deliverable.attributes.dup @@ -64,6 +68,7 @@ class Deliverable < ActiveRecord::Base # Amount spent. Virtual accessor that is overriden by subclasses. def spent + # TODO: check rights to see rates 0 end @@ -93,7 +98,7 @@ class Deliverable < ActiveRecord::Base def progress return 0 unless self.issues.size > 0 - total ||= self.issues.collect(&:estimated_hours).compact.sum || 0 + total = self.issues.collect(&:estimated_hours).compact.sum || 0 return 0 unless total > 0 balance = 0.0 diff --git a/app/models/deliverable_cost.rb b/app/models/deliverable_cost.rb index 06ee82ebb5..4fce77f1c1 100644 --- a/app/models/deliverable_cost.rb +++ b/app/models/deliverable_cost.rb @@ -34,4 +34,32 @@ class DeliverableCost < ActiveRecord::Base def costs rate && units ? rate.rate * units : 0.0 end + + + def progres + # TODO: not yet finished + raise NotImplementedError.new + + return 0 unless self.issues.size > 0 + + + + + total_hours = + total_costs = self.issues.collect{|i| i.cost_entries.collect(&:cost).compact.sum || 0}.compact.sum || 0 + + return 0 unless total > 0 + balance = 0.0 + + self.issues.each do |issue| + if use_issue_status_for_done_ratios? + balance += issue.status.default_done_ratio * issue.estimated_hours unless issue.estimated_hours.nil? + else + balance += issue.done_ratio * issue.estimated_hours unless issue.estimated_hours.nil? + end + end + + return (balance / total).round + + end end \ No newline at end of file diff --git a/app/views/deliverables/_deliverable_cost.rhtml b/app/views/deliverables/_deliverable_cost.rhtml index 38b3c7ce2f..d501ca09bb 100644 --- a/app/views/deliverables/_deliverable_cost.rhtml +++ b/app/views/deliverables/_deliverable_cost.rhtml @@ -15,7 +15,7 @@
<%= f.select :kind, [[l(:label_cost_based_deliverable), CostBasedDeliverable.name], [l(:label_fixed_deliverable), FixedDeliverable.name]], {:required => true, :prompt => true }, :autocomplete => 'off' %>
<%= observe_field :deliverable_kind, :url => { :action => :new }, :update => :content, :with => "Form.serialize('deliverable-form')" %><%= f.text_field :subject, :size => 80, :required => true %>
<%= f.text_area :description,
@@ -37,7 +28,7 @@
<%= l(:caption_cost_unit_plural)%>
<%= l(:caption_cost_type) %>
- <% if User.current.allowed_to?(:view_unit_price, @project)%><%= l(:caption_overall_costs) %> <%end%>
+ <% if User.current.allowed_to?(:view_unit_price, @project)%><%= l(:caption_budget) %> <%end%>
<%= render :partial => 'attachments/form' %>
+<% end %> + <%= wikitoolbar_for 'deliverable_description' %> diff --git a/app/views/deliverables/_list.rhtml b/app/views/deliverables/_list.rhtml index 5622b3ada0..7ab1de7ed0 100644 --- a/app/views/deliverables/_list.rhtml +++ b/app/views/deliverables/_list.rhtml @@ -23,12 +23,12 @@ <%= content_tag(:td, h(deliverable.status), :class => 'status') %> <%= content_tag(:td, link_to(h(deliverable.subject), :controller => 'deliverables', :action => 'show', :id => deliverable), :class => 'subject') %> <%= content_tag(:td, h(deliverable.score.to_i), :class => 'score') %> - <%= content_tag(:td, number_to_currency(deliverable.budget || 0.0, :precision => 0), :class => 'budget') %> - <%= content_tag(:td, number_to_currency(deliverable.labor_budget || 0.0, :precision => 0), :class => 'budget') %> - <%= content_tag(:td, number_to_currency(deliverable.materials_budget || 0.0, :precision => 0), :class => 'budget') %> - <%= content_tag(:td, number_to_currency(deliverable.spent, :precision => 0), :class => 'spent') %> + <%= content_tag(:td, number_to_currency(deliverable.budget || 0.0, :precision => 0), :class => 'currency') %> + <%= content_tag(:td, number_to_currency(deliverable.labor_budget || 0.0, :precision => 0), :class => 'currency') %> + <%= content_tag(:td, number_to_currency(deliverable.materials_budget || 0.0, :precision => 0), :class => 'currency') %> + <%= content_tag(:td, number_to_currency(deliverable.spent, :precision => 0), :class => 'currency') %> <%= content_tag(:td, format_date(deliverable.fixed_date), :class => 'fixed_date') %> - <%= content_tag(:td, progress_bar(deliverable.progress, :width => '100%', :class => 'done_ratio')) %> + <%= content_tag(:td, extended_progress_bar(deliverable.progress, :width => '100%')) %> <% end %> diff --git a/app/views/deliverables/_show_cost_based_deliverable.rhtml b/app/views/deliverables/_show_cost_based_deliverable.rhtml new file mode 100644 index 0000000000..563a69acd5 --- /dev/null +++ b/app/views/deliverables/_show_cost_based_deliverable.rhtml @@ -0,0 +1,60 @@ +<% + show_cost_budget_col = User.current.allowed_to?(:view_unit_price, @project) + show_hour_budget_col = User.current.allowed_to?(:view_all_rates, @project) || User.current.allowed_to?(:view_own_rate, @project) +-%> + +
+ <%= l(:caption_materials_budget)%>+ <% unless @deliverable.deliverable_costs.empty? %> +
|
+ <%= l(:caption_labor_budget)%>+ <% unless @deliverable.deliverable_hours.empty? %> +
|
<%=l(:field_type)%>: | <%= @deliverable.type_label %> | -<%=l(:field_fixed_date)%>: | <%= format_date(@deliverable.fixed_date) %> | +<%=l(:field_type)%>: | +<%= @deliverable.type_label %> | + +<%=l(:field_fixed_date)%>: | +<%= format_date(@deliverable.fixed_date) %> |
<%=l(:field_status)%>: | +<%= @deliverable.status %> | + +<%=l(:field_progress)%>: | +<%= extended_progress_bar(@deliverable.progress, :width => '80px', :legend => "#{@deliverable.progress}%") %> | +
<%=l(:field_description)%>
++<%= l(:label_export_to) %> +<%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %> +
+ <% html_title "#{l(:label_deliverable)} ##{@deliverable.id}: #{@deliverable.subject}" %> <% content_for :sidebar do %>