Index: app/controllers/issues_controller.rb =================================================================== --- app/controllers/issues_controller.rb (revision 14229) +++ app/controllers/issues_controller.rb (working copy) @@ -81,7 +81,12 @@ :order => sort_clause, :offset => @offset, :limit => @limit) + + @all_issues = @query.issues(:include => [:status, :project, :assigned_to, :tracker, :priority, :category, :fixed_version]) + @issue_count_by_group = @query.issue_count_by_group + @issue_sum_by_group = @query.issue_sum_by_group + @issue_progress_by_group = @query.issue_progress_by_group respond_to do |format| format.html { render :template => 'issues/index', :layout => !request.xhr? } @@ -148,6 +153,7 @@ call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) if @issue.save + @issue.update_estimated_done call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) respond_to do |format| format.html { @@ -195,6 +201,7 @@ end if saved + @issue.update_estimated_done render_attachment_warning_if_needed(@issue) flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? Index: app/helpers/issues_helper.rb =================================================================== --- app/helpers/issues_helper.rb (revision 14229) +++ app/helpers/issues_helper.rb (working copy) @@ -427,4 +427,16 @@ end end end + + def estimated_done(issues) + issues.map(&:estimated_done).reject{|x|x.nil?}.sum.round(2) + end + + def estimated_hours(issues) + issues.map(&:estimated_hours).reject{|x| x.nil?}.sum + end + + def estimated_done_percentage(issues) + (100 * estimated_done(issues) / estimated_hours(issues)).round(2) + end end Index: app/helpers/queries_helper.rb =================================================================== --- app/helpers/queries_helper.rb (revision 14229) +++ app/helpers/queries_helper.rb (working copy) @@ -103,6 +103,8 @@ issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' when :done_ratio progress_bar(value, :width => '80px') + when :estimated_done + sprintf "%.2f", value when :relations other = value.other_issue(issue) content_tag('span', Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 14229) +++ app/models/issue.rb (working copy) @@ -356,6 +356,10 @@ write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end + def estimated_done=(h) + write_attribute :estimated_done, (h.is_a?(String) ? h.to_hours : h) + end + safe_attributes 'project_id', :if => lambda {|issue, user| if issue.new_record? @@ -569,7 +573,7 @@ private :workflow_rule_by_attribute def done_ratio - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0 status.default_done_ratio else read_attribute(:done_ratio) @@ -651,7 +655,7 @@ # Set the done_ratio using the status if that setting is set. This will keep the done_ratios # even if the user turns off the setting later def update_done_ratio_from_issue_status - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0 self.done_ratio = status.default_done_ratio end end @@ -770,6 +774,15 @@ !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? end + def update_estimated_done + if children.count < 1 + x1 = Issue.find_by_id(id).done_ratio.to_f + x2 = Issue.find_by_id(id).estimated_hours.to_f + r = ((x1 * x2)/100).round(2) + Issue.update(id, :estimated_done => r) + end + end + # Returns an array of statuses that user is able to apply def new_statuses_allowed_to(user=User.current, include_default=false) if new_record? && @copied_from @@ -1406,17 +1419,19 @@ if average == 0 average = 1 end - done = p.leaves.joins(:status). - sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " + - "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f - progress = done / (average * leaves_count) - p.done_ratio = progress.round end end # estimate = sum of leaves estimates p.estimated_hours = p.leaves.sum(:estimated_hours).to_f + p.estimated_done = p.leaves.sum(:estimated_done).to_f + + if (p.estimated_hours > 0) + p.done_ratio = (100 * p.estimated_done / p.estimated_hours).to_f.round(2) + end + p.estimated_hours = nil if p.estimated_hours == 0.0 + p.estimated_done = nil if p.estimated_done == 0.0 # ancestors will be recursively updated p.save(:validate => false) @@ -1611,3 +1626,5 @@ group by s.id, s.is_closed, j.id") end end + + Index: app/models/issue_query.rb =================================================================== --- app/models/issue_query.rb (revision 14229) +++ app/models/issue_query.rb (working copy) @@ -38,6 +38,7 @@ QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), + QueryColumn.new(:estimated_done, :sortable => "#{Issue.table_name}.estimated_done", :caption => :field_estimated_done), QueryColumn.new(:relations, :caption => :label_related_issues), QueryColumn.new(:description, :inline => false) ] @@ -214,6 +215,7 @@ add_available_filter "due_date", :type => :date add_available_filter "estimated_hours", :type => :float add_available_filter "done_ratio", :type => :integer + add_available_filter "estimated_done", :type => :float if User.current.allowed_to?(:set_issues_private, nil, :global => true) || User.current.allowed_to?(:set_own_issues_private, nil, :global => true) @@ -293,7 +295,21 @@ rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end + + # Returns sum of all the issue's estimated_hours + def issue_sum + Issue.visible.sum(:estimated_hours, :include => [:status, :project], :conditions => statement) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns sum of all the issue's estimated_done + def issue_sum_in_progress + Issue.visible.sum(:estimated_done, :include => [:status, :project], :conditions => statement) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns the issue count by group or nil if query is not grouped def issue_count_by_group r = nil @@ -319,6 +335,46 @@ raise StatementInvalid.new(e.message) end + # Returns sum of the issue's estimated_hours by group or nil if query is not grouped + def issue_sum_by_group + r = nil + if grouped? + begin + r = Issue.visible.sum(:estimated_hours, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) + rescue ActiveRecord::RecordNotFound + r= {r => issue_sum} + end + + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h,k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns sum of the issue's estimated_done by group or nil if query is not grouped + def issue_progress_by_group + r = nil + if grouped? + begin + r = Issue.visible.sum(:estimated_done, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) + rescue ActiveRecord::RecordNotFound + r= {r => issue_sum_by_group} + end + + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + # Returns the issues # Valid options are :order, :offset, :limit, :include, :conditions def issues(options={}) Index: app/views/issues/_list.html.erb =================================================================== --- app/views/issues/_list.html.erb (revision 14229) +++ app/views/issues/_list.html.erb (working copy) @@ -14,20 +14,25 @@ <% end %> - <% previous_group, first = false, true %> + <% previous_group = false %> <% issue_list(issues) do |issue, level| -%> - <% if @query.grouped? && ((group = @query.group_by_column.value(issue)) != previous_group || first) %> + <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %> <% reset_cycle %>   - <%= (group.blank? && group != false) ? l(:label_none) : column_content(@query.group_by_column, issue) %> <%= @issue_count_by_group[group] %> - <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", - "toggleAllRowGroups(this)", :class => 'toggle-all') %> + <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <%= @issue_count_by_group[group] %>, Est Done: <%= (@issue_progress_by_group[group] * 100).round / 100.0 %> + <% if @issue_sum_by_group[group] > 0 %> + (<%= (100 * @issue_progress_by_group[group] / @issue_sum_by_group[group]).round(2) %>%), + <% else %> + (0.0%), + <% end %> + <%= l(:label_total) %>: <%= @issue_sum_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> - <% previous_group, first = group, false %> + <% previous_group = group %> <% end %> "> <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> @@ -45,3 +50,11 @@ <% end -%> +

+ Current page: <%=estimated_hours(issues) %> + Est Done: <%= estimated_done(@all_issues) %> <% if estimated_hours(@all_issues) > 0 %> + (<%= "#{estimated_done_percentage(@all_issues)}%" %>) + <% else %>(0.0%) + <% end %> + <%= l(:label_total) %>: <%=@query.issue_sum %> +

Index: app/views/issues/show.html.erb =================================================================== --- app/views/issues/show.html.erb (revision 14229) +++ app/views/issues/show.html.erb (working copy) @@ -60,6 +60,9 @@ rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' end end + unless @issue.disabled_core_fields.include?('estimated_hours') + rows.right "Estimated done", l_hours(@issue.estimated_done), :class => 'estimated-hours' + end if User.current.allowed_to?(:view_time_entries, @project) rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time' end Index: config/locales/en.yml =================================================================== --- config/locales/en.yml (revision 14229) +++ config/locales/en.yml (working copy) @@ -300,6 +300,7 @@ field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time + field_estimated_done: Estimated done field_column_names: Columns field_time_entries: Log time field_time_zone: Time zone