diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b8e1dd4140..daa7bc5c71 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -276,6 +276,16 @@ module IssuesHelper end end + def issue_spent_time_ratio_details(issue) + if issue.total_spent_time_ratio + if issue.total_spent_time_ratio == issue.spent_time_ratio + "#{issue.spent_time_ratio.to_s(:percentage, precision: 2)}" + else + "#{issue.spent_time_ratio.to_s(:percentage, precision: 2)} (#{l(:label_total)}: #{issue.total_spent_time_ratio.to_s(:percentage, precision: 2)})" + end + end + end + def issue_due_date_details(issue) return if issue&.due_date.nil? diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 9b223f84a7..967b45a907 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -260,6 +260,8 @@ module QueriesHelper item.last_notes.present? ? content_tag('div', textilizable(item, :last_notes), :class => "wiki") : '' when :done_ratio progress_bar(value) + when :spent_time_ratio, :total_spent_time_ratio + value ? value.to_s(:percentage, precision: 2) : '' when :relations content_tag( 'span', diff --git a/app/models/issue.rb b/app/models/issue.rb index 7ce04ad64b..4eff0331ff 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -733,6 +733,18 @@ class Issue < ActiveRecord::Base end end + def spent_time_ratio + if estimated_hours.to_f > 0 + (spent_hours.to_f / estimated_hours.to_f * 100).round(2) + end + end + + def total_spent_time_ratio + if total_estimated_hours.to_f > 0 + (total_spent_hours.to_f / total_estimated_hours.to_f * 100).round(2) + end + end + def self.use_status_for_done_ratio? Setting.issue_done_ratio == 'issue_status' end diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index efd0ad0f1d..84d4148f66 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -61,6 +61,35 @@ class IssueQuery < Query end, :default_order => 'desc'), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), + QueryColumn.new( + :spent_time_ratio, + :sortable => + lambda do + "COALESCE((" \ + " SELECT ROUND(CAST(COALESCE(SUM(hours), 0) / NULLIF(#{Issue.table_name}.estimated_hours, 0) * 100 AS DECIMAL(4,2)), 2) " \ + " FROM #{TimeEntry.table_name}" \ + " WHERE issue_id = #{Issue.table_name}.id), NULL)" + end, + :default_order => 'desc'), + QueryColumn.new( + :total_spent_time_ratio, + :sortable => + lambda do + "COALESCE(ROUND(CAST(" \ + "COALESCE((SELECT SUM(hours)" \ + " FROM #{TimeEntry.table_name}" \ + " JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id" \ + " JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" \ + " WHERE (#{TimeEntry.visible_condition(User.current)})" \ + " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" \ + " / " \ + "NULLIF((SELECT SUM(estimated_hours)" \ + " FROM #{Issue.table_name} subtasks" \ + " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')}" \ + " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" \ + "* 100 AS DECIMAL(4,2)), 2), NULL)" + end, + :default_order => 'desc'), TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc', :groupable => true), TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", @@ -203,6 +232,8 @@ class IssueQuery < Query end add_available_filter "done_ratio", :type => :integer + add_available_filter "spent_time_ratio", :type => :float + add_available_filter "total_spent_time_ratio", :type => :float if User.current.allowed_to?(:set_issues_private, nil, :global => true) || User.current.allowed_to?(:set_own_issues_private, nil, :global => true) @@ -508,6 +539,49 @@ class IssueQuery < Query "WHERE issue_id = #{Issue.table_name}.id), 0) #{sql_op}" end + def sql_for_spent_time_ratio_field(field, operator, value) + first, second = value.first.to_f, value.second.to_f + sql_op = + case operator + when "=", ">=", "<=" then "#{operator} #{first}" + when "><" then "BETWEEN #{first} AND #{second}" + when "*" then ">= 0" + when "!*" then "IS NULL" + else + return nil + end + "COALESCE((" + + "SELECT ROUND(CAST(COALESCE(SUM(hours), 0) / NULLIF(#{Issue.table_name}.estimated_hours, 0) * 100 AS DECIMAL(4,2)), 2) " + + "FROM #{TimeEntry.table_name} " + + "WHERE issue_id = #{Issue.table_name}.id), NULL) #{sql_op}" + end + + def sql_for_total_spent_time_ratio_field(field, operator, value) + first, second = value.first.to_f, value.second.to_f + sql_op = + case operator + when "=", ">=", "<=" then "#{operator} #{first}" + when "><" then "BETWEEN #{first} AND #{second}" + when "*" then ">= 0" + when "!*" then "IS NULL" + else + return nil + end + "COALESCE(ROUND(CAST(" + + "COALESCE((SELECT SUM(hours)" + + " FROM #{TimeEntry.table_name}" + + " JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id" + + " JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" + + " WHERE (#{TimeEntry.visible_condition(User.current)})" + + " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" + + " / " + + "NULLIF((SELECT SUM(estimated_hours)" + + " FROM #{Issue.table_name} subtasks" + + " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')}" + + " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" + + " * 100 AS DECIMAL(4,2)), 2), NULL) #{sql_op}" + end + def sql_for_watcher_id_field(field, operator, value) db_table = Watcher.table_name me, others = value.partition {|id| ['0', User.current.id.to_s].include?(id)} diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 6b84bdfb3d..1bf7ef2ff4 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -73,6 +73,9 @@ end if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' + if @issue.total_spent_time_ratio.present? + rows.right l(:field_spent_time_ratio), issue_spent_time_ratio_details(@issue), :class => 'spent-time' + end end end %> <%= render_half_width_custom_fields_rows(@issue) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 90eff9ad34..31e2f92ea2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -338,6 +338,8 @@ en: field_onthefly: On-the-fly user creation field_start_date: Start date field_done_ratio: "% Done" + field_spent_time_ratio: Spent Time Ratio + field_total_spent_time_ratio: Total spent time Ratio field_auth_source: Authentication mode field_hide_mail: Hide my email address field_comments: Comment