Project

General

Profile

Patch #2268 » total_estimated_hour_2_6_4.diff

Muhammad Azmi Farih, 2015-04-27 12:02

View differences:

app/helpers/issues_helper.rb (working copy)
427 427
      end
428 428
    end
429 429
  end
430

  
431
  def estimated_done(issues)
432
    issues.map(&:estimated_done).reject{|x|x.nil?}.sum.round(2)
433
  end
434

  
435
  def estimated_hours(issues)
436
    issues.map(&:estimated_hours).reject{|x| x.nil?}.sum
437
  end
438

  
439
  def estimated_done_percentage(issues)
440
    (100 * estimated_done(issues) / estimated_hours(issues)).round(2)
441
  end
430 442
end
app/helpers/queries_helper.rb (working copy)
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
module QueriesHelper
21
  include ApplicationHelper
22

  
23 21
  def filters_options_for_select(query)
24 22
    options_for_select(filters_options(query))
25 23
  end
......
83 81
  end
84 82

  
85 83
  def column_content(column, issue)
86
    value = column.value_object(issue)
84
    value = column.value(issue)
87 85
    if value.is_a?(Array)
88 86
      value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
89 87
    else
......
97 95
      link_to value, issue_path(issue)
98 96
    when :subject
99 97
      link_to value, issue_path(issue)
100
    when :parent
101
      value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
102 98
    when :description
103 99
      issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
104 100
    when :done_ratio
105 101
      progress_bar(value, :width => '80px')
102
    when :estimated_done
103
      if (value.nil?)
104
	 value = 0
105
      end
106
      sprintf "%.2f", value
106 107
    when :relations
107 108
      other = value.other_issue(issue)
108 109
      content_tag('span',
109 110
        (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
110 111
        :class => value.css_classes_for(issue))
111
    else
112
      else
112 113
      format_object(value)
113
    end
114
      end
114 115
  end
115 116

  
116 117
  def csv_content(column, issue)
117
    value = column.value_object(issue)
118
    value = column.value(issue)
118 119
    if value.is_a?(Array)
119 120
      value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
120
    else
121
      else
121 122
      csv_value(column, issue, value)
122
    end
123
      end
123 124
  end
124 125

  
125
  def csv_value(column, object, value)
126
    format_object(value, false) do |value|
127
      case value.class.name
128
      when 'Float'
129
        sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
130
      when 'IssueRelation'
131
        other = value.other_issue(object)
132
        l(value.label_for(object)) + " ##{other.id}"
133
      when 'Issue'
134
        if object.is_a?(TimeEntry)
135
          "#{value.tracker} ##{value.id}: #{value.subject}"
136
        else
137
          value.id
138
        end
139
      else
140
        value
141
      end
126
  def csv_value(column, issue, value)
127
    case value.class.name
128
    when 'Time'
129
      format_time(value)
130
    when 'Date'
131
      format_date(value)
132
    when 'Float'
133
      sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
134
    when 'IssueRelation'
135
      other = value.other_issue(issue)
136
      l(value.label_for(issue)) + " ##{other.id}"
137
    when 'TrueClass'
138
      l(:general_text_Yes)
139
    when 'FalseClass'
140
      l(:general_text_No)
141
    else
142
      value.to_s
142 143
    end
143 144
  end
144 145

  
app/models/query.rb (working copy)
57 57
    object.send name
58 58
  end
59 59

  
60
  def value_object(object)
61
    object.send name
62
  end
63

  
64 60
  def css_classes
65 61
    name
66 62
  end
......
84 80
    @cf
85 81
  end
86 82

  
87
  def value_object(object)
83
  def value(object)
88 84
    if custom_field.visible_by?(object.project, User.current)
89
      cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
90
      cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
85
      cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
86
      cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
91 87
    else
92 88
      nil
93
    end
94 89
  end
95

  
96
  def value(object)
97
    raw = value_object(object)
98
    if raw.is_a?(Array)
99
      raw.map {|r| @cf.cast_value(r.value)}
100
    elsif raw
101
      @cf.cast_value(raw.value)
102
    else
103
      nil
104
    end
105 90
  end
106 91

  
107 92
  def css_classes
......
120 105
    @association = association
121 106
  end
122 107

  
123
  def value_object(object)
108
  def value(object)
124 109
    if assoc = object.send(@association)
125 110
      super(assoc)
126 111
    end
......
159 144

  
160 145
  after_save do |query|
161 146
    if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
162
      query.roles.clear
163
    end
147
	    query.roles.clear
148
	  end
164 149
  end
165 150

  
166 151
  class_attribute :operators
167 152
  self.operators = {
168 153
    "="   => :label_equals,
169
    "!"   => :label_not_equals,
170
    "o"   => :label_open_issues,
171
    "c"   => :label_closed_issues,
172
    "!*"  => :label_none,
154
                  "!"   => :label_not_equals,
155
                  "o"   => :label_open_issues,
156
                  "c"   => :label_closed_issues,
157
                  "!*"  => :label_none,
173 158
    "*"   => :label_any,
174
    ">="  => :label_greater_or_equal,
175
    "<="  => :label_less_or_equal,
176
    "><"  => :label_between,
177
    "<t+" => :label_in_less_than,
178
    ">t+" => :label_in_more_than,
159
                  ">="  => :label_greater_or_equal,
160
                  "<="  => :label_less_or_equal,
161
                  "><"  => :label_between,
162
                  "<t+" => :label_in_less_than,
163
                  ">t+" => :label_in_more_than,
179 164
    "><t+"=> :label_in_the_next_days,
180
    "t+"  => :label_in,
181
    "t"   => :label_today,
165
                  "t+"  => :label_in,
166
                  "t"   => :label_today,
182 167
    "ld"  => :label_yesterday,
183
    "w"   => :label_this_week,
168
                  "w"   => :label_this_week,
184 169
    "lw"  => :label_last_week,
185 170
    "l2w" => [:label_last_n_weeks, {:count => 2}],
186 171
    "m"   => :label_this_month,
187 172
    "lm"  => :label_last_month,
188 173
    "y"   => :label_this_year,
189
    ">t-" => :label_less_than_ago,
190
    "<t-" => :label_more_than_ago,
174
                  ">t-" => :label_less_than_ago,
175
                  "<t-" => :label_more_than_ago,
191 176
    "><t-"=> :label_in_the_past_days,
192
    "t-"  => :label_ago,
193
    "~"   => :label_contains,
177
                  "t-"  => :label_ago,
178
                  "~"   => :label_contains,
194 179
    "!~"  => :label_not_contains,
195 180
    "=p"  => :label_any_issues_in_project,
196 181
    "=!p" => :label_any_issues_not_in_project,
......
200 185
  class_attribute :operators_by_filter_type
201 186
  self.operators_by_filter_type = {
202 187
    :list => [ "=", "!" ],
203
    :list_status => [ "o", "=", "!", "c", "*" ],
204
    :list_optional => [ "=", "!", "!*", "*" ],
205
    :list_subprojects => [ "*", "!*", "=" ],
188
                                 :list_status => [ "o", "=", "!", "c", "*" ],
189
                                 :list_optional => [ "=", "!", "!*", "*" ],
190
                                 :list_subprojects => [ "*", "!*", "=" ],
206 191
    :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
207 192
    :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
208
    :string => [ "=", "~", "!", "!~", "!*", "*" ],
209
    :text => [  "~", "!~", "!*", "*" ],
210
    :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
193
                                 :string => [ "=", "~", "!", "!~", "!*", "*" ],
194
                                 :text => [  "~", "!~", "!*", "*" ],
195
                                 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
211 196
    :float => [ "=", ">=", "<=", "><", "!*", "*" ],
212 197
    :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
213 198
  }
......
301 286
    json = {}
302 287
    available_filters.each do |field, options|
303 288
      json[field] = options.slice(:type, :name, :values).stringify_keys
304
    end
289
        end
305 290
    json
306
  end
291
      end
307 292

  
308 293
  def all_projects
309 294
    @all_projects ||= Project.visible.all
310
  end
295
        end
311 296

  
312 297
  def all_projects_values
313 298
    return @all_projects_values if @all_projects_values
314 299

  
315 300
    values = []
316
    Project.project_tree(all_projects) do |p, level|
317
      prefix = (level > 0 ? ('--' * level + ' ') : '')
301
        Project.project_tree(all_projects) do |p, level|
302
          prefix = (level > 0 ? ('--' * level + ' ') : '')
318 303
      values << ["#{prefix}#{p.name}", p.id.to_s]
319
    end
304
        end
320 305
    @all_projects_values = values
321
  end
306
      end
322 307

  
323 308
  # Adds available filters
324 309
  def initialize_available_filters
325 310
    # implemented by sub-classes
326
  end
311
    end
327 312
  protected :initialize_available_filters
328 313

  
329 314
  # Adds an available filter
......
331 316
    @available_filters ||= ActiveSupport::OrderedHash.new
332 317
    @available_filters[field] = options
333 318
    @available_filters
334
  end
319
    end
335 320

  
336 321
  # Removes an available filter
337 322
  def delete_available_filter(field)
338 323
    if @available_filters
339 324
      @available_filters.delete(field)
340
    end
341
  end
325
      end
326
      end
342 327

  
343 328
  # Return a hash of available filters
344 329
  def available_filters
......
417 402
  # Returns a Hash of columns and the key for sorting
418 403
  def sortable_columns
419 404
    available_columns.inject({}) {|h, column|
420
      h[column.name.to_s] = column.sortable
421
      h
405
                                               h[column.name.to_s] = column.sortable
406
                                               h
422 407
    }
423 408
  end
424 409

  
......
436 421

  
437 422
  def block_columns
438 423
    columns.reject(&:inline?)
439
  end
424
    end
440 425

  
441 426
  def available_inline_columns
442 427
    available_columns.select(&:inline?)
......
556 541
      next unless v and !v.empty?
557 542
      operator = operator_for(field)
558 543

  
559
      # "me" value substitution
544
      # "me" value subsitution
560 545
      if %w(assigned_to_id author_id user_id watcher_id).include?(field)
561 546
        if v.delete("me")
562 547
          if User.current.logged?
......
622 607
      customized_key = "#{assoc}_id"
623 608
      customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
624 609
      raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
625
    end
610
  end
626 611
    where = sql_for_field(field, operator, value, db_table, db_field, true)
627 612
    if operator =~ /[<>]/
628 613
      where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
......
793 778
    if assoc.present?
794 779
      filter_id = "#{assoc}.#{filter_id}"
795 780
      filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
796
    end
781
        end
797 782
    add_available_filter filter_id, options.merge({
798 783
      :name => filter_name,
799 784
      :field => field
800 785
    })
801
  end
786
      end
802 787

  
803 788
  # Adds filters for the given custom fields scope
804 789
  def add_custom_fields_filters(scope, assoc=nil)
app/models/issue_query.rb (working copy)
38 38
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
39 39
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
40 40
    QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
41
    QueryColumn.new(:estimated_done, :sortable => "#{Issue.table_name}.estimated_done", :caption => :field_estimated_done),
41 42
    QueryColumn.new(:relations, :caption => :label_related_issues),
42 43
    QueryColumn.new(:description, :inline => false)
43 44
  ]
......
147 148
    end
148 149
    principals.uniq!
149 150
    principals.sort!
150
    principals.reject! {|p| p.is_a?(GroupBuiltin)}
151 151
    users = principals.select {|p| p.is_a?(User)}
152 152

  
153 153
    add_available_filter "status_id",
......
184 184
      :type => :list_optional, :values => assigned_to_values
185 185
    ) unless assigned_to_values.empty?
186 186

  
187
    group_values = Group.givable.collect {|g| [g.name, g.id.to_s] }
187
    group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
188 188
    add_available_filter("member_of_group",
189 189
      :type => :list_optional, :values => group_values
190 190
    ) unless group_values.empty?
......
214 214
    add_available_filter "due_date", :type => :date
215 215
    add_available_filter "estimated_hours", :type => :float
216 216
    add_available_filter "done_ratio", :type => :integer
217
    add_available_filter "estimated_done", :type => :float
217 218

  
218 219
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
219 220
      User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
......
293 294
  rescue ::ActiveRecord::StatementInvalid => e
294 295
    raise StatementInvalid.new(e.message)
295 296
  end
297
  
298
  # Returns sum of all the issue's estimated_hours 
299
  def issue_sum
300
    Issue.visible.sum(:estimated_hours, :include => [:status, :project], :conditions => statement)
301
  rescue ::ActiveRecord::StatementInvalid => e
302
    raise StatementInvalid.new(e.message)  
303
  end  
296 304

  
305
  # Returns sum of all the issue's estimated_done 
306
  def issue_sum_in_progress
307
    Issue.visible.sum(:estimated_done, :include => [:status, :project], :conditions => statement)
308
  rescue ::ActiveRecord::StatementInvalid => e
309
    raise StatementInvalid.new(e.message)  
310
  end
311

  
297 312
  # Returns the issue count by group or nil if query is not grouped
298 313
  def issue_count_by_group
299 314
    r = nil
......
318 333
  rescue ::ActiveRecord::StatementInvalid => e
319 334
    raise StatementInvalid.new(e.message)
320 335
  end
321

  
336
  
337
  # Returns  sum of the issue's estimated_hours by group or nil if query is not grouped
338
  def issue_sum_by_group
339
    r = nil
340
    if grouped?
341
      begin
342
        r = Issue.visible.sum(:estimated_hours, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
343
      rescue ActiveRecord::RecordNotFound
344
        r= {r => issue_sum}
345
      end
346
       
347
      c = group_by_column
348
      if c.is_a?(QueryCustomFieldColumn)
349
        r = r.keys.inject({}) {|h,k| h[c.custom_field.cast_value(k)] = r[k]; h}
350
      end
351
    end
352
    r
353
  rescue ::ActiveRecord::StatementInvalid => e
354
    raise StatementInvalid.new(e.message)
355
  end
356
  
357
  # Returns  sum of the issue's estimated_done by group or nil if query is not grouped
358
  def issue_progress_by_group
359
    r = nil
360
    if grouped?
361
      begin
362
        r = Issue.visible.sum(:estimated_done, :joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
363
      rescue ActiveRecord::RecordNotFound
364
        r= {r => issue_sum_by_group}
365
      end
366
       
367
      c = group_by_column
368
      if c.is_a?(QueryCustomFieldColumn)
369
        r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
370
      end
371
    end
372
    r
373
  rescue ::ActiveRecord::StatementInvalid => e
374
    raise StatementInvalid.new(e.message)
375
  end
376
   
322 377
  # Returns the issues
323 378
  # Valid options are :order, :offset, :limit, :include, :conditions
324 379
  def issues(options={})
......
405 460

  
406 461
  def sql_for_member_of_group_field(field, operator, value)
407 462
    if operator == '*' # Any group
408
      groups = Group.givable
463
      groups = Group.all
409 464
      operator = '=' # Override the operator since we want to find by assigned_to
410 465
    elsif operator == "!*"
411
      groups = Group.givable
466
      groups = Group.all
412 467
      operator = '!' # Override the operator since we want to find by assigned_to
413 468
    else
414 469
      groups = Group.where(:id => value).all
app/models/issue.rb (working copy)
33 33
  has_many :visible_journals,
34 34
    :class_name => 'Journal',
35 35
    :as => :journalized,
36
    :conditions => Proc.new {
36
    :conditions => Proc.new { 
37 37
      ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
38 38
    },
39 39
    :readonly => true
......
94 94
  before_create :default_assign
95 95
  before_save :close_duplicates, :update_done_ratio_from_issue_status,
96 96
              :force_updated_on_change, :update_closed_on, :set_assigned_to_was
97
  after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
97
  after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} 
98 98
  after_save :reschedule_following_issues, :update_nested_set_attributes,
99 99
             :update_parent_attributes, :create_journal
100 100
  # Should be after_create but would be called before previous after_save callbacks
......
122 122
        end
123 123
      else
124 124
        "(#{table_name}.is_private = #{connection.quoted_false})"
125
      end
126 125
    end
127 126
  end
127
  end
128 128

  
129 129
  # Returns true if usr or current user is allowed to view the issue
130 130
  def visible?(usr=nil)
......
142 142
        end
143 143
      else
144 144
        !self.is_private?
145
      end
146 145
    end
147 146
  end
147
  end
148 148

  
149 149
  # Returns true if user or current user is allowed to edit or add a note to the issue
150 150
  def editable?(user=User.current)
......
195 195
    @workflow_rule_by_attribute = nil
196 196
    @assignable_versions = nil
197 197
    @relations = nil
198
    @spent_hours = nil
199 198
    base_reload(*args)
200 199
  end
201 200

  
......
219 218
    self.status = issue.status
220 219
    self.author = User.current
221 220
    unless options[:attachments] == false
222
      self.attachments = issue.attachments.map do |attachement|
221
      self.attachments = issue.attachments.map do |attachement| 
223 222
        attachement.copy(:container => self)
224 223
      end
225 224
    end
......
356 355
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
357 356
  end
358 357

  
358
  def estimated_done=(h)
359
    write_attribute :estimated_done, (h.is_a?(String) ? h.to_hours : h)
360
  end
361
  
359 362
  safe_attributes 'project_id',
360 363
    :if => lambda {|issue, user|
361 364
      if issue.new_record?
......
395 398
    :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
396 399

  
397 400
  safe_attributes 'private_notes',
398
    :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
401
    :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} 
399 402

  
400 403
  safe_attributes 'watcher_user_ids',
401
    :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
404
    :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} 
402 405

  
403 406
  safe_attributes 'is_private',
404 407
    :if => lambda {|issue, user|
......
454 457
      s = attrs['parent_issue_id'].to_s
455 458
      unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
456 459
        @invalid_parent_issue_id = attrs.delete('parent_issue_id')
457
      end
458 460
    end
461
    end
459 462

  
460 463
    if attrs['custom_field_values'].present?
461 464
      editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
......
530 533
    return {} if roles.empty?
531 534

  
532 535
    result = {}
533
    workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id))
536
    workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
534 537
    if workflow_permissions.any?
535 538
      workflow_rules = workflow_permissions.inject({}) do |h, wp|
536 539
        h[wp.field_name] ||= {}
......
569 572
  private :workflow_rule_by_attribute
570 573

  
571 574
  def done_ratio
572
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
575
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0
573 576
      status.default_done_ratio
574 577
    else
575 578
      read_attribute(:done_ratio)
......
641 644
          errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
642 645
        end
643 646
      else
644
        if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
647
        if respond_to?(attribute) && send(attribute).blank?
645 648
          errors.add attribute, :blank
646 649
        end
647 650
      end
......
651 654
  # Set the done_ratio using the status if that setting is set.  This will keep the done_ratios
652 655
  # even if the user turns off the setting later
653 656
  def update_done_ratio_from_issue_status
654
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
657
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio && self.leaves && self.leaves.count == 0
655 658
      self.done_ratio = status.default_done_ratio
656 659
    end
657 660
  end
......
757 760
      elsif project_id_changed?
758 761
        if project.shared_versions.include?(fixed_version)
759 762
          versions << fixed_version
760
        end
763
  end
761 764
      else
762 765
        versions << fixed_version
763 766
      end
......
770 773
    !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
771 774
  end
772 775

  
776
  def update_estimated_done
777
     if children.count < 1
778
       x1 = Issue.find_by_id(id).done_ratio.to_f
779
       x2 = Issue.find_by_id(id).estimated_hours.to_f
780
       r = ((x1 * x2)/100).round(2)
781
       Issue.update(id, :estimated_done => r)
782
     end
783
  end
784
 
773 785
  # Returns an array of statuses that user is able to apply
774 786
  def new_statuses_allowed_to(user=User.current, include_default=false)
775 787
    if new_record? && @copied_from
......
784 796
      initial_status ||= status
785 797

  
786 798
      initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
787
      assignee_transitions_allowed = initial_assigned_to_id.present? &&
799
      assignee_transitions_allowed = initial_assigned_to_id.present? && 
788 800
        (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
789 801

  
790 802
      statuses = initial_status.find_new_statuses_allowed_to(
......
977 989
          elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
978 990
            queue << child
979 991
            issue_status[child] = ePROCESS_ALL
980
          end
992
    end
981 993
        end
982 994
      end
983 995

  
......
1080 1092
          # or if it starts before the given date
1081 1093
          if start_date == leaf.start_date || date > leaf.start_date
1082 1094
            leaf.reschedule_on!(date)
1083
          end
1095
      end
1084 1096
        else
1085 1097
          leaf.reschedule_on!(date)
1086
        end
1087
      end
1088 1098
    end
1089 1099
  end
1100
    end
1101
  end
1090 1102

  
1091 1103
  def <=>(issue)
1092 1104
    if issue.nil?
......
1129 1141
  def self.update_versions_from_hierarchy_change(project)
1130 1142
    moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1131 1143
    # Update issues of the moved projects and issues assigned to a version of a moved project
1132
    Issue.update_versions(
1133
            ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1134
             moved_project_ids, moved_project_ids]
1135
          )
1144
    Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1136 1145
  end
1137 1146

  
1138 1147
  def parent_issue_id=(arg)
1139 1148
    s = arg.to_s.strip.presence
1140 1149
    if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1150
      @parent_issue.id
1141 1151
      @invalid_parent_issue_id = nil
1142 1152
    elsif s.blank?
1143 1153
      @parent_issue = nil
......
1177 1187
    end
1178 1188
  end
1179 1189

  
1180
  # Returns an issue scope based on project and scope
1181 1190
  def self.cross_project_scope(project, scope=nil)
1182 1191
    if project.nil?
1183 1192
      return Issue
......
1199 1208
    end
1200 1209
  end
1201 1210

  
1211

  
1202 1212
  # Extracted from the ReportsController.
1203 1213
  def self.by_tracker(project)
1204 1214
    count_and_group_by(:project => project,
......
1237 1247
  end
1238 1248

  
1239 1249
  def self.by_subproject(project)
1240
    ActiveRecord::Base.connection.select_all("select    s.id as status_id,
1241
                                                s.is_closed as closed,
1250
    ActiveRecord::Base.connection.select_all("select    s.id as status_id, 
1251
                                                s.is_closed as closed, 
1242 1252
                                                #{Issue.table_name}.project_id as project_id,
1243
                                                count(#{Issue.table_name}.id) as total
1244
                                              from
1253
                                                count(#{Issue.table_name}.id) as total 
1254
                                              from 
1245 1255
                                                #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1246
                                              where
1256
                                              where 
1247 1257
                                                #{Issue.table_name}.status_id=s.id
1248 1258
                                                and #{Issue.table_name}.project_id = #{Project.table_name}.id
1249 1259
                                                and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
......
1333 1343
    if root_id.nil?
1334 1344
      # issue was just created
1335 1345
      self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1336
      Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1346
      set_default_left_and_right
1347
      Issue.where(["id = ?", id]).
1348
        update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt])
1337 1349
      if @parent_issue
1338 1350
        move_to_child_of(@parent_issue)
1339 1351
      end
......
1356 1368
        move_to_right_of(root)
1357 1369
      end
1358 1370
      old_root_id = root_id
1359
      in_tenacious_transaction do
1360
        @parent_issue.reload_nested_set if @parent_issue
1361
        self.reload_nested_set
1362
        self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1363
        cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1364
        self.class.base_class.select('id').lock(true).where(cond)
1365
        offset = right_most_bound + 1 - lft
1366
        Issue.where(cond).
1367
          update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1368
      end
1371
      self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1372
      target_maxright = nested_set_scope.maximum(right_column_name) || 0
1373
      offset = target_maxright + 1 - lft
1374
      Issue.where(["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]).
1375
        update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1376
      self[left_column_name] = lft + offset
1377
      self[right_column_name] = rgt + offset
1369 1378
      if @parent_issue
1370 1379
        move_to_child_of(@parent_issue)
1371 1380
      end
......
1397 1406
      if p.start_date && p.due_date && p.due_date < p.start_date
1398 1407
        p.start_date, p.due_date = p.due_date, p.start_date
1399 1408
      end
1400

  
1409
  
1401 1410
      # done ratio = weighted average ratio of leaves
1402 1411
      unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1403 1412
        leaves_count = p.leaves.count
......
1406 1415
          if average == 0
1407 1416
            average = 1
1408 1417
          end
1409
          done = p.leaves.joins(:status).
1410
            sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1411
                "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1412
          progress = done / (average * leaves_count)
1413
          p.done_ratio = progress.round
1418
          
1419
          #--original code--
1420
          #change done_ratio to be the sum of children done_ratio
1421
          #done = p.leaves.joins(:status).
1422
          #  sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1423
          #     "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1424
          #progress = done / (average * leaves_count)
1425
          #p.done_ratio = progress.round
1426
          #--end of original code---
1414 1427
        end
1428

  
1415 1429
      end
1416

  
1417
      # estimate = sum of leaves estimates
1430
	  
1431
	  # estimate = sum of leaves estimates
1418 1432
      p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1433
      p.estimated_done = p.leaves.sum(:estimated_done).to_f
1434
	  
1435
      if (p.estimated_hours > 0)
1436
        p.done_ratio = (100 * p.estimated_done / p.estimated_hours).to_f.round(2)
1437
      end
1438
	  
1419 1439
      p.estimated_hours = nil if p.estimated_hours == 0.0
1420

  
1440
      p.estimated_done = nil if p.estimated_done == 0.0
1441
	  
1421 1442
      # ancestors will be recursively updated
1422 1443
      p.save(:validate => false)
1423 1444
    end
......
1477 1498
  def close_duplicates
1478 1499
    if closing?
1479 1500
      duplicates.each do |duplicate|
1480
        # Reload is needed in case the duplicate was updated by a previous duplicate
1501
        # Reload is need in case the duplicate was updated by a previous duplicate
1481 1502
        duplicate.reload
1482 1503
        # Don't re-close it if it's already closed
1483 1504
        next if duplicate.closed?
......
1497 1518
      self.updated_on = current_time_from_proper_timezone
1498 1519
      if new_record?
1499 1520
        self.created_on = updated_on
1500
      end
1501 1521
    end
1502 1522
  end
1523
  end
1503 1524

  
1504 1525
  # Callback for setting closed_on when the issue is closed.
1505 1526
  # The closed_on attribute stores the time of the last closing
......
1532 1553
          before = @custom_values_before_change[c.custom_field_id]
1533 1554
          after = c.value
1534 1555
          next if before == after || (before.blank? && after.blank?)
1535

  
1556
          
1536 1557
          if before.is_a?(Array) || after.is_a?(Array)
1537 1558
            before = [before] unless before.is_a?(Array)
1538 1559
            after = [after] unless after.is_a?(Array)
1539

  
1560
            
1540 1561
            # values removed
1541 1562
            (before - after).reject(&:blank?).each do |value|
1542 1563
              @current_journal.details << JournalDetail.new(:property => 'cf',
......
1597 1618

  
1598 1619
    where = "#{Issue.table_name}.#{select_field}=j.id"
1599 1620

  
1600
    ActiveRecord::Base.connection.select_all("select    s.id as status_id,
1601
                                                s.is_closed as closed,
1621
    ActiveRecord::Base.connection.select_all("select    s.id as status_id, 
1622
                                                s.is_closed as closed, 
1602 1623
                                                j.id as #{select_field},
1603
                                                count(#{Issue.table_name}.id) as total
1604
                                              from
1624
                                                count(#{Issue.table_name}.id) as total 
1625
                                              from 
1605 1626
                                                  #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1606
                                              where
1607
                                                #{Issue.table_name}.status_id=s.id
1627
                                              where 
1628
                                                #{Issue.table_name}.status_id=s.id 
1608 1629
                                                and #{where}
1609 1630
                                                and #{Issue.table_name}.project_id=#{Project.table_name}.id
1610 1631
                                                and #{visible_condition(User.current, :project => project)}
1611 1632
                                              group by s.id, s.is_closed, j.id")
1612 1633
  end
1613 1634
end
1635

  
1636

  
app/controllers/issues_controller.rb (working copy)
81 81
                              :order => sort_clause,
82 82
                              :offset => @offset,
83 83
                              :limit => @limit)
84

  
85
      @all_issues = @query.issues(:include => [:status, :project, :assigned_to, :tracker, :priority, :category, :fixed_version])
86
      
84 87
      @issue_count_by_group = @query.issue_count_by_group
88
      @issue_sum_by_group = @query.issue_sum_by_group
89
      @issue_progress_by_group = @query.issue_progress_by_group
85 90

  
86 91
      respond_to do |format|
87 92
        format.html { render :template => 'issues/index', :layout => !request.xhr? }
......
148 153
    call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
149 154
    @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
150 155
    if @issue.save
156
      @issue.update_estimated_done
151 157
      call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
152 158
      respond_to do |format|
153 159
        format.html {
......
195 201
    end
196 202

  
197 203
    if saved
204
      @issue.update_estimated_done
198 205
      render_attachment_warning_if_needed(@issue)
199 206
      flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200 207

  
app/views/issues/_list.html.erb (working copy)
1 1
<%= form_tag({}) do -%>
2 2
<%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 3
<div class="autoscroll">
4
<table class="list issues <%= sort_css_classes %>">
4
<table class="list issues">
5 5
  <thead>
6 6
    <tr>
7 7
      <th class="checkbox hide-when-print">
8 8
        <%= link_to image_tag('toggle_check.png'), {},
9 9
                              :onclick => 'toggleIssuesSelection(this); return false;',
10
                              :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
11
      </th>
10
                                                           :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
11
        </th>
12 12
      <% query.inline_columns.each do |column| %>
13
        <%= column_header(column) %>
14
      <% end %>
13
          <%= column_header(column) %>
14
        <% end %>
15 15
    </tr>
16 16
  </thead>
17
  <% previous_group, first = false, true %>
17
  <% previous_group = false %>
18 18
  <tbody>
19 19
  <% issue_list(issues) do |issue, level| -%>
20
  <% if @query.grouped? && ((group = @query.group_by_column.value(issue)) != previous_group || first) %>
20
  <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
21 21
    <% reset_cycle %>
22 22
    <tr class="group open">
23 23
      <td colspan="<%= query.inline_columns.size + 2 %>">
24 24
        <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
25
        <%= (group.blank? && group != false) ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
26
        <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
27
                             "toggleAllRowGroups(this)", :class => 'toggle-all') %>
25
        <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %>, Est Done:  <%= (@issue_progress_by_group[group] * 100).round / 100.0 %>
26
		  <% if @issue_sum_by_group[group] > 0 %>
27
		    (<%= (100 * @issue_progress_by_group[group] / @issue_sum_by_group[group]).round(2) %>%),
28
		  <% else %>
29
		    (0.0%),
30
		  <% end %>
31
		<%= l(:label_total) %>: <%= @issue_sum_by_group[group] %>)</span>
32
        <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
28 33
      </td>
29 34
    </tr>
30
    <% previous_group, first = group, false %>
35
    <% previous_group = group %>
31 36
  <% end %>
32 37
  <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
33 38
    <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
......
45 50
</table>
46 51
</div>
47 52
<% end -%>
53
 <p align="right">
54
     Current page: <b><%=estimated_hours(issues) %></b>
55
     Est Done: <b><%= estimated_done(@all_issues) %> <% if estimated_hours(@all_issues) > 0 %>
56
	 	 (<%= "#{estimated_done_percentage(@all_issues)}%" %>)
57
		 <% else %>(0.0%)
58
		 <% end %></b>
59
     <%= l(:label_total) %>: <b><%=@query.issue_sum %></b>
60
 </p>
app/views/issues/show.html.erb (working copy)
58 58
  unless @issue.disabled_core_fields.include?('estimated_hours')
59 59
    unless @issue.estimated_hours.nil?
60 60
      rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
61
    end
61
  	end
62 62
  end
63
  unless @issue.disabled_core_fields.include?('estimated_hours')
64
     rows.right "Estimated done", l_hours(@issue.estimated_done), :class => 'estimated-hours'
65
  end
63 66
  if User.current.allowed_to?(:view_time_entries, @project)
64
    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'
67
    rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? (link_to l_hours(@issue.total_spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-"), :class => 'spent-time'
65 68
  end
66 69
end %>
67 70
<%= render_custom_fields_rows(@issue) %>
config/locales/en.yml (working copy)
300 300
  field_assignable: Issues can be assigned to this role
301 301
  field_redirect_existing_links: Redirect existing links
302 302
  field_estimated_hours: Estimated time
303
  field_estimated_done: Estimated done
303 304
  field_column_names: Columns
304 305
  field_time_entries: Log time
305 306
  field_time_zone: Time zone
(4-4/5)