Project

General

Profile

Feature #24277 » 01_add_remaining_hours_field_5.1.2.patch

Radek Pesina, 2024-05-01 03:47

View differences:

app/helpers/issues_helper.rb
265 265
    end
266 266
  end
267 267

  
268
  def issue_remaining_hours_details(issue)
269
    if issue.total_remaining_hours.present?
270
      if issue.total_remaining_hours == issue.remaining_hours
271
        l_hours_short(issue.remaining_hours)
272
      else
273
        s = issue.remaining_hours.present? ? l_hours_short(issue.remaining_hours) : ""
274
        s << " (#{l(:label_total)}: #{l_hours_short(issue.total_remaining_hours)})"
275
        s.html_safe
276
      end
277
    end
278
  end
279

  
268 280
  def issue_spent_hours_details(issue)
269 281
    if issue.total_spent_hours > 0
270 282
      path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
......
540 552
        value = find_name_by_reflection(field, detail.value)
541 553
        old_value = find_name_by_reflection(field, detail.old_value)
542 554

  
543
      when 'estimated_hours'
555
      when 'estimated_hours', 'remaining_hours'
544 556
        value = l_hours_short(detail.value.to_f) unless detail.value.blank?
545 557
        old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
546 558

  
app/models/issue.rb
70 70
  validates_length_of :subject, :maximum => 255
71 71
  validates_inclusion_of :done_ratio, :in => 0..100
72 72
  validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73
  validates :remaining_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 74
  validates :start_date, :date => true
74 75
  validates :due_date, :date => true
75 76
  validate :validate_issue, :validate_required_fields, :validate_permissions
......
109 110

  
110 111
  before_validation :default_assign, on: :create
111 112
  before_validation :clear_disabled_fields
113
  before_validation :update_remaining_hours_from_estimated_hours
112 114
  before_save :close_duplicates, :update_done_ratio_from_issue_status,
113
              :force_updated_on_change, :update_closed_on
115
              :force_updated_on_change, :update_closed_on, :update_remaining_hours
114 116
  after_save do |issue|
115 117
    if !issue.saved_change_to_id? && issue.saved_change_to_project_id?
116 118
      issue.send :after_project_change
......
270 272
    @spent_hours = nil
271 273
    @total_spent_hours = nil
272 274
    @total_estimated_hours = nil
275
    @total_remaining_hours = nil
273 276
    @last_updated_by = nil
274 277
    @last_notes = nil
275 278
    base_reload(*args)
......
486 489
    write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h)
487 490
  end
488 491

  
492
  def remaining_hours=(h)
493
    h = h.is_a?(String) ? h.to_hours : h
494
    # remaining time cannot be less than zero
495
    h = 0 if !h.nil? && h < 0
496
    write_attribute :remaining_hours, h
497
  end
498

  
489 499
  safe_attributes(
490 500
    'project_id',
491 501
    'tracker_id',
......
500 510
    'due_date',
501 511
    'done_ratio',
502 512
    'estimated_hours',
513
    'remaining_hours',
503 514
    'custom_field_values',
504 515
    'custom_fields',
505 516
    'lock_version',
......
1165 1176
    end
1166 1177
  end
1167 1178

  
1179
  def total_remaining_hours
1180
    if leaf?
1181
      remaining_hours
1182
    else
1183
      @total_remaining_hours ||= self_and_descendants.sum(:remaining_hours)
1184
    end
1185
  end
1186

  
1168 1187
  def relations
1169 1188
    @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1170 1189
  end
......
2006 2025
    end
2007 2026
  end
2008 2027

  
2028
  # Callback for setting remaining time to zero when the issue is closed.
2029
  def update_remaining_hours
2030
    if closing? && safe_attribute?('remaining_hours') && self.remaining_hours.to_f > 0
2031
      self.remaining_hours = 0
2032
    end
2033
  end
2034

  
2009 2035
  # Saves the changes in a Journal
2010 2036
  # Called after_save
2011 2037
  def create_journal
......
2095 2121
    roles = user.admin ? Role.all.to_a : user.roles_for_project(project)
2096 2122
    roles.select(&:consider_workflow?)
2097 2123
  end
2124

  
2125
  def update_remaining_hours_from_estimated_hours
2126
    if self.remaining_hours.blank? && self.estimated_hours
2127
      self.remaining_hours = self.estimated_hours
2128
    end
2129
  end
2098 2130
end
app/models/issue_import.rb
213 213
    if estimated_hours = row_value(row, 'estimated_hours')
214 214
      attributes['estimated_hours'] = estimated_hours
215 215
    end
216
    if remaining_hours = row_value(row, 'remaining_hours')
217
      attributes['remaining_hours'] = remaining_hours
218
    end
216 219
    if done_ratio = row_value(row, 'done_ratio')
217 220
      attributes['done_ratio'] = done_ratio
218 221
    end
app/models/issue_query.rb
65 65
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
66 66
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours",
67 67
                    :totalable => true),
68
    QueryColumn.new(:remaining_hours, :sortable => "#{Issue.table_name}.remaining_hours",
69
                    :totalable => true),
68 70
    EstimatedRemainingHoursColumn.new,
69 71
    QueryColumn.new(
70 72
      :total_estimated_hours,
......
77 79
          " AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
78 80
        end,
79 81
      :default_order => 'desc'),
82
    QueryColumn.new(:total_remaining_hours,
83
      :sortable => "COALESCE((SELECT SUM(remaining_hours) FROM #{Issue.table_name} subtasks" +
84
        " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
85
      :default_order => 'desc'),
80 86
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
81 87
    TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on",
82 88
                             :default_order => 'desc', :groupable => true),
......
223 229
    add_available_filter "start_date", :type => :date
224 230
    add_available_filter "due_date", :type => :date
225 231
    add_available_filter "estimated_hours", :type => :float
232
    add_available_filter "remaining_hours", :type => :float
226 233

  
227 234
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
228 235
      add_available_filter "spent_time", :type => :float, :label => :label_spent_time
......
389 396
    map_total(scope.sum(EstimatedRemainingHoursColumn::COLUMN_SQL)) {|t| t.to_f.round(2)}
390 397
  end
391 398

  
399
  # Returns sum of all the issue's remaining_hours
400
  def total_for_remaining_hours(scope)
401
    map_total(scope.sum(:remaining_hours)) {|t| t.to_f.round(2)}
402
  end
403

  
392 404
  # Returns sum of all the issue's time entries hours
393 405
  def total_for_spent_hours(scope)
394 406
    total = scope.joins(:time_entries).
app/models/mail_handler.rb
484 484
      'start_date' => get_keyword(:start_date, :format => '\d{4}-\d{2}-\d{2}'),
485 485
      'due_date' => get_keyword(:due_date, :format => '\d{4}-\d{2}-\d{2}'),
486 486
      'estimated_hours' => get_keyword(:estimated_hours),
487
      'remaining_hours' => get_keyword(:remaining_hours),
487 488
      'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0'),
488 489
      'is_private' => get_keyword_bool(:is_private),
489 490
      'parent_issue_id' => get_keyword(:parent_issue)
app/models/tracker.rb
23 23
  CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject is_private).freeze
24 24
  # Fields that can be disabled
25 25
  # Other (future) fields should be appended, not inserted!
26
  CORE_FIELDS =
27
    %w(assigned_to_id category_id fixed_version_id parent_issue_id
28
       start_date due_date estimated_hours done_ratio description priority_id).freeze
26
  CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours remaining_hours done_ratio description priority_id).freeze
29 27
  CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
30 28

  
31 29
  before_destroy :check_integrity
app/views/imports/_issues_fields_mapping.html.erb
71 71
  <label for="import_mapping_estimated_hours"><%= l(:field_estimated_hours) %></label>
72 72
  <%= mapping_select_tag @import, 'estimated_hours' %>
73 73
</p>
74
<p>
75
  <label><%= l(:field_remaining_hours) %></label>
76
  <%= mapping_select_tag @import, 'remaining_hours' %>
77
</p>
74 78
<p>
75 79
  <label for="import_mapping_done_ratio"><%= l(:field_done_ratio) %></label>
76 80
  <%= mapping_select_tag @import, 'done_ratio' %>
app/views/issues/_attributes.html.erb
83 83
</p>
84 84
<% end %>
85 85

  
86
<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
87
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
88
<% end %>
89

  
86 90
<% if @issue.safe_attribute? 'estimated_hours' %>
87 91
<p><%= f.hours_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
88 92
<% end %>
89 93

  
90
<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
91
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
94
<% if @issue.safe_attribute? 'remaining_hours' %>
95
<p><%= f.text_field :remaining_hours, :size => 3, :required => @issue.required_attribute?('remaining_hours') %> <%= l(:field_hours) %></p>
92 96
<% end %>
93 97
</div>
94 98
</div>
app/views/issues/bulk_edit.html.erb
175 175
</p>
176 176
<% end %>
177 177

  
178
<% if @safe_attributes.include?('remaining_hours') %>
179
<p>
180
  <label for='issue_remaining_hours'><%= l(:field_remaining_hours) %></label>
181
  <%= text_field_tag 'issue[remaining_hours]', '', :value => @issue_params[:remaining_hours], :size => 10 %>
182
  <label class="inline"><%= check_box_tag 'issue[remaining_hours]', 'none', (@issue_params[:remaining_hours] == 'none'), :id => nil, :data => {:disables => '#issue_remaining_hours'} %><%= l(:button_clear) %></label>
183
</p>
184
<% end %>
185

  
178 186
<% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
179 187
<p>
180 188
  <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
app/views/issues/index.api.rsb
19 19
      api.done_ratio  issue.done_ratio
20 20
      api.is_private  issue.is_private
21 21
      api.estimated_hours issue.estimated_hours
22
      api.remaining_hours issue.remaining_hours
22 23
      api.total_estimated_hours issue.total_estimated_hours
23 24
      if User.current.allowed_to?(:view_time_entries, issue.project)
24 25
        api.spent_hours(issue.spent_hours)
app/views/issues/show.api.rsb
17 17
  api.done_ratio @issue.done_ratio
18 18
  api.is_private @issue.is_private
19 19
  api.estimated_hours @issue.estimated_hours
20
  api.remaining_hours @issue.remaining_hours
20 21
  api.total_estimated_hours @issue.total_estimated_hours
22
  api.total_remaining_hours @issue.total_remaining_hours
21 23
  if User.current.allowed_to?(:view_time_entries, @project)
22 24
    api.spent_hours(@issue.spent_hours)
23 25
    api.total_spent_hours(@issue.total_spent_hours)
app/views/issues/show.html.erb
73 73
  unless @issue.disabled_core_fields.include?('estimated_hours')
74 74
    rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
75 75
  end
76
  unless @issue.disabled_core_fields.include?('remaining_hours')
77
    rows.right l(:field_remaining_hours), issue_remaining_hours_details(@issue), :class => 'remaining-hours'
78
  end
76 79
  if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0
77 80
    rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
78 81
  end
config/locales/en.yml
409 409
  field_full_width_layout: Full width layout
410 410
  field_digest: Checksum
411 411
  field_default_assigned_to: Default assignee
412
  field_remaining_hours: Remaining time
413
  field_total_remaining_hours: Total remaining time
412 414
  field_recently_used_projects: Number of recently used projects in jump box
413 415
  field_history_default_tab: Issue's history default tab
414 416
  field_unique_id: Unique ID
db/migrate/20160920184857_add_remaining_hours_to_issues.rb
1
class AddRemainingHoursToIssues < ActiveRecord::Migration[4.2]
2
  def self.up
3
    add_column :issues, :remaining_hours, :float
4
    Issue.where("estimated_hours > ?", 0).update_all("remaining_hours = estimated_hours")
5
  end
6

  
7
  def self.down
8
    remove_column :issues, :remaining_hours
9
   end
10
end
test/fixtures/mail_handler/ticket_on_given_project.eml
18 18
X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 19
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20 20

  
21
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet 
22
turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus 
23
blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti 
24
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In 
25
in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras 
26
sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum 
27
id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus 
28
eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique 
29
sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et 
30
malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse 
21
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22
turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23
blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25
in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26
sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27
id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28
eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29
sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et
30
malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 31
platea dictumst.
32 32

  
33 33
Project: onlinestore
......
37 37
Assigned to: John Smith
38 38
fixed version: alpha
39 39
estimated hours: 2.5
40
remaining hours: 1
40 41
done ratio: 30
41 42
parent issue: 4
42 43

  
......
52 53

  
53 54
This paragraph is after the delimiter so it shouldn't appear.
54 55

  
55
Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque 
56
sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. 
57
Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, 
58
dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, 
59
massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo 
56
Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
57
sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
58
Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
59
dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
60
massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
60 61
pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
61 62

  
test/functional/issues_controller_test.rb
1400 1400
    assert_equal hours.sort.reverse, hours
1401 1401
  end
1402 1402

  
1403
  def test_index_sort_by_total_remaining_hours
1404
    get :index, :sort => 'total_remaining_hours:desc'
1405
    assert_response :success
1406
    hours = assigns(:issues).collect(&:total_remaining_hours)
1407
    assert_equal hours.sort.reverse, hours
1408
  end
1409

  
1403 1410
  def test_index_sort_by_user_custom_field
1404 1411
    cf = IssueCustomField.
1405 1412
           create!(
......
1630 1637
    assert_select 'table.issues td.total_estimated_hours'
1631 1638
  end
1632 1639

  
1640
  def test_index_with_total_remaining_hours_column
1641
    get :index, :set_filter => 1, :c => %w(subject total_remaining_hours)
1642
    assert_select 'table.issues td.total_remaining_hours'
1643
  end
1644

  
1633 1645
  def test_index_should_not_show_spent_hours_column_without_permission
1634 1646
    Role.anonymous.remove_permission! :view_time_entries
1635 1647
    get(
......
1934 1946
    assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]'
1935 1947
  end
1936 1948

  
1949
  def test_index_with_remaining_hours_total
1950
    Issue.delete_all
1951
    Issue.generate!(:remaining_hours => 5.4)
1952
    Issue.generate!(:remaining_hours => 1.1)
1953

  
1954
    get :index, :t => %w(remaining_hours)
1955
    assert_response :success
1956
    assert_select '.query-totals'
1957
    assert_select '.total-for-remaining-hours span.value', :text => '6.50'
1958
    assert_select 'input[type=checkbox][name=?][value=remaining_hours][checked=checked]', 't[]'
1959
  end
1960

  
1937 1961
  def test_index_with_grouped_query_and_estimated_hours_total
1938 1962
    Issue.delete_all
1939 1963
    Issue.generate!(:estimated_hours => '5:30', :category_id => 1)
......
4075 4099
              :priority_id => 5,
4076 4100
              :start_date => '2010-11-07',
4077 4101
              :estimated_hours => '',
4102
              :remaining_hours => '',
4078 4103
              :custom_field_values => {
4079 4104
                '2' => 'Value for field 2'
4080 4105
              }
......
4092 4117
    assert_equal 2, issue.status_id
4093 4118
    assert_equal Date.parse('2010-11-07'), issue.start_date
4094 4119
    assert_nil issue.estimated_hours
4120
    assert_nil issue.remaining_hours
4095 4121
    v = issue.custom_values.where(:custom_field_id => 2).first
4096 4122
    assert_not_nil v
4097 4123
    assert_equal 'Value for field 2', v.value
......
7480 7506
    assert_equal 4.25, Issue.find(2).estimated_hours
7481 7507
  end
7482 7508

  
7509
  def test_bulk_update_remaining_hours
7510
    @request.session[:user_id] = 2
7511
    post :bulk_update, :ids => [1, 2], :issue => {:remaining_hours => 4.15}
7512

  
7513
    assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
7514
    assert_equal 4.15, Issue.find(1).remaining_hours
7515
    assert_equal 4.15, Issue.find(2).remaining_hours
7516
  end
7517

  
7483 7518
  def test_bulk_update_custom_field
7484 7519
    @request.session[:user_id] = 2
7485 7520
    # update issues priority
test/helpers/issues_helper_test.rb
208 208
    assert_match '6:18', show_detail(detail, true)
209 209
  end
210 210

  
211
  test 'show_detail should show old and new values with a remaining hours attribute' do
212
    detail = JournalDetail.new(:property => 'attr', :prop_key => 'remaining_hours',
213
                               :old_value => '6.3', :value => '5')
214
    assert_match '5.00', show_detail(detail, true)
215
    assert_match '6.30', show_detail(detail, true)
216
  end
217

  
211 218
  test 'show_detail should not show values with a description attribute' do
212 219
    detail = JournalDetail.new(:property => 'attr', :prop_key => 'description',
213 220
                               :old_value => 'Foo', :value => 'Bar')
test/integration/api_test/issues_test.rb
456 456
    end
457 457
  end
458 458

  
459
  test "GET /issues/:id.xml should contains total_estimated_hours and total_spent_hours" do
459
  test "GET /issues/:id.xml should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do
460 460
    parent = Issue.find(3)
461 461
    parent.update_columns :estimated_hours => 2.0
462
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
462
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
463 463
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
464 464
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
465 465
    get '/issues/3.xml'
......
468 468
    assert_select 'issue' do
469 469
      assert_select 'estimated_hours',       parent.estimated_hours.to_s
470 470
      assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s
471
      assert_select 'remaining_hours',       parent.remaining_hours.to_s
472
      assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s
471 473
      assert_select 'spent_hours',           parent.spent_hours.to_s
472 474
      assert_select 'total_spent_hours',     (parent.spent_hours.to_f + 2.5).to_s
473 475
    end
474 476
  end
475 477

  
476
  test "GET /issues/:id.xml should contains total_estimated_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
478
  test "GET /issues/:id.xml should contains total_estimated_hours and total_remaining_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
477 479
    parent = Issue.find(3)
478 480
    parent.update_columns :estimated_hours => 2.0
479
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
481
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
480 482
    Role.anonymous.remove_permission! :view_time_entries
481 483
    get '/issues/3.xml'
482 484

  
......
484 486
    assert_select 'issue' do
485 487
      assert_select 'estimated_hours',       parent.estimated_hours.to_s
486 488
      assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s
489
      assert_select 'remaining_hours',       parent.remaining_hours.to_s
490
      assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s
487 491
      assert_select 'spent_hours',           false
488 492
      assert_select 'total_spent_hours',     false
489 493
    end
......
506 510
    end
507 511
  end
508 512

  
509
  test "GET /issues/:id.json should contains total_estimated_hours and total_spent_hours" do
513
  test "GET /issues/:id.json should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do
510 514
    parent = Issue.find(3)
511 515
    parent.update_columns :estimated_hours => 2.0
512
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
516
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
513 517
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
514 518
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
515 519
    get '/issues/3.json'
......
518 522
    json = ActiveSupport::JSON.decode(response.body)
519 523
    assert_equal parent.estimated_hours, json['issue']['estimated_hours']
520 524
    assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours']
525
    assert_equal parent.remaining_hours, json['issue']['remaining_hours']
526
    assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours']
521 527
    assert_equal parent.spent_hours, json['issue']['spent_hours']
522 528
    assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours']
523 529
  end
524 530

  
525
  test "GET /issues/:id.json should contains total_estimated_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
531
  test "GET /issues/:id.json should contains total_estimated_hours and total_remaining_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
526 532
    parent = Issue.find(3)
527 533
    parent.update_columns :estimated_hours => 2.0
528
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
534
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
529 535
    Role.anonymous.remove_permission! :view_time_entries
530 536
    get '/issues/3.json'
531 537

  
......
533 539
    json = ActiveSupport::JSON.decode(response.body)
534 540
    assert_equal parent.estimated_hours, json['issue']['estimated_hours']
535 541
    assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours']
542
    assert_equal parent.remaining_hours, json['issue']['remaining_hours']
543
    assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours']
536 544
    assert_nil json['issue']['spent_hours']
537 545
    assert_nil json['issue']['total_spent_hours']
538 546
  end
test/unit/issue_subtasking_test.rb
34 34
  def test_leaf_planning_fields_should_be_editable
35 35
    issue = Issue.generate!
36 36
    user = User.find(1)
37
    %w(priority_id done_ratio start_date due_date estimated_hours).each do |attribute|
37
    %w(priority_id done_ratio start_date due_date estimated_hours remaining_hours).each do |attribute|
38 38
      assert issue.safe_attribute?(attribute, user)
39 39
    end
40 40
  end
......
377 377
    assert !child.save
378 378
    assert_include I18n.t("activerecord.errors.messages.open_issue_with_closed_parent"), child.errors.full_messages
379 379
  end
380

  
381
  def test_parent_total_remaining_hours_should_be_sum_of_descendants
382
    parent = Issue.generate!
383
    parent.generate_child!(:remaining_hours => nil)
384
    assert_equal 0, parent.reload.total_remaining_hours
385
    parent.generate_child!(:remaining_hours => 5)
386
    assert_equal 5, parent.reload.total_remaining_hours
387
    parent.generate_child!(:remaining_hours => 7)
388
    assert_equal 12, parent.reload.total_remaining_hours
389
  end
380 390
end
test/unit/issue_test.rb
60 60
    issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
61 61
                      :status_id => 1, :priority => IssuePriority.first,
62 62
                      :subject => 'test_create',
63
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30')
63
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1')
64 64
    assert issue.save
65 65
    issue.reload
66 66
    assert_equal 1.5, issue.estimated_hours
67
    assert_equal 1, issue.remaining_hours
67 68
  end
68 69

  
69 70
  def test_create_minimal
......
72 73
    assert_equal issue.tracker.default_status, issue.status
73 74
    assert issue.description.nil?
74 75
    assert_nil issue.estimated_hours
76
    assert_nil issue.remaining_hours
75 77
  end
76 78

  
77 79
  def test_create_with_all_fields_disabled
......
148 150
    end
149 151
  end
150 152

  
153
  def test_remaining_hours_update_with_negative_value_should_set_to_zero
154
    set_language_if_valid 'en'
155
    ['-4'].each do |invalid|
156
      issue = Issue.new(:remaining_hours => invalid)
157
      assert_equal 0, issue.remaining_hours
158
    end
159
  end
160

  
161
  def test_remaining_hours_should_be_set_from_estimated_hours_when_is_empty
162
      issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
163
                      :status_id => 1, :priority => IssuePriority.all.first,
164
                      :subject => 'test_create',
165
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30')
166
      assert issue.save
167
      assert_equal 1.5, issue.remaining_hours
168
  end
169

  
151 170
  def test_create_with_required_custom_field
152 171
    set_language_if_valid 'en'
153 172
    field = IssueCustomField.find_by_name('Database')
......
2727 2746
    user = User.find(3)
2728 2747
    user.members.update_all ["mail_notification = ?", false]
2729 2748
    user.update! :mail_notification => 'only_assigned'
2749
    puts user.inspect
2730 2750

  
2731 2751
    with_settings :notified_events => %w(issue_updated) do
2732 2752
      issue = Issue.find(2)
......
3284 3304
    assert_equal false, issue.closing?
3285 3305
  end
3286 3306

  
3307
  def test_closing_should_set_remaining_hours_to_zero
3308
    issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
3309
                      :status_id => 1, :priority => IssuePriority.all.first,
3310
                      :subject => 'test_create',
3311
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1')
3312
    assert_equal 1, issue.remaining_hours
3313
    issue.status_id = 5
3314
    issue.save!
3315
    assert_equal 0, issue.remaining_hours
3316
  end
3317

  
3287 3318
  def test_reopening_should_return_true_when_reopening_an_issue
3288 3319
    issue = Issue.find(8)
3289 3320
    issue.status = IssueStatus.find(6)
test/unit/mail_handler_test.rb
48 48
        'ticket_on_given_project.eml',
49 49
        :allow_override =>
50 50
          ['status', 'start_date', 'due_date', 'assigned_to',
51
           'fixed_version', 'estimated_hours', 'done_ratio',
51
           'fixed_version', 'estimated_hours', 'remaining_hours', 'done_ratio',
52 52
           'parent_issue']
53 53
      )
54 54
    assert issue.is_a?(Issue)
......
65 65
    assert_equal User.find_by_login('jsmith'), issue.assigned_to
66 66
    assert_equal Version.find_by_name('Alpha'), issue.fixed_version
67 67
    assert_equal 2.5, issue.estimated_hours
68
    assert_equal 1, issue.remaining_hours
68 69
    assert_equal 30, issue.done_ratio
69 70
    assert_equal Issue.find(4), issue.parent
70 71
    # keywords should be removed from the email body
71 72
    assert !issue.description.match(/^Project:/i)
72 73
    assert !issue.description.match(/^Status:/i)
73 74
    assert !issue.description.match(/^Start Date:/i)
75
    assert !issue.description.match(/^remaining hours:/i)
74 76
  end
75 77

  
76 78
  def test_add_issue_with_all_overrides
......
87 89
    assert_equal User.find_by_login('jsmith'), issue.assigned_to
88 90
    assert_equal Version.find_by_name('Alpha'), issue.fixed_version
89 91
    assert_equal 2.5, issue.estimated_hours
92
    assert_equal 1, issue.remaining_hours
90 93
    assert_equal 30, issue.done_ratio
91 94
    assert_equal Issue.find(4), issue.parent
92 95
  end
......
109 112
    assert_nil issue.assigned_to
110 113
    assert_nil issue.fixed_version
111 114
    assert_nil issue.estimated_hours
115
    assert_nil issue.remaining_hours
112 116
    assert_equal 0, issue.done_ratio
113 117
    assert_nil issue.parent
114 118
  end
test/unit/query_test.rb
2347 2347

  
2348 2348
  def test_set_totalable_names
2349 2349
    q = IssueQuery.new
2350
    q.totalable_names = ['estimated_hours', :spent_hours, '']
2351
    assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
2350
    q.totalable_names = ['estimated_hours', 'remaining_hours', :spent_hours, '']
2351
    assert_equal [:estimated_hours, :remaining_hours, :spent_hours], q.totalable_columns.map(&:name)
2352 2352
  end
2353 2353

  
2354 2354
  def test_totalable_columns_should_default_to_settings
......
2368 2368
    assert_include :estimated_remaining_hours, q.available_totalable_columns.map(&:name)
2369 2369
  end
2370 2370

  
2371
  def test_available_totalable_columns_should_include_remaining_hours
2372
    q = IssueQuery.new
2373
    assert_include :remaining_hours, q.available_totalable_columns.map(&:name)
2374
  end
2375

  
2371 2376
  def test_available_totalable_columns_should_include_spent_hours
2372 2377
    User.current = User.find(1)
2373 2378

  
......
2464 2469
    )
2465 2470
  end
2466 2471

  
2472
  def test_total_for_remaining_hours
2473
    Issue.delete_all
2474
    Issue.generate!(:remaining_hours => 5.5)
2475
    Issue.generate!(:remaining_hours => 1.1)
2476
    Issue.generate!
2477

  
2478
    q = IssueQuery.new
2479
    assert_equal 6.6, q.total_for(:remaining_hours)
2480
  end
2481

  
2482
  def test_total_by_group_for_remaining_hours
2483
    Issue.delete_all
2484
    Issue.generate!(:remaining_hours => 5.5, :assigned_to_id => 2)
2485
    Issue.generate!(:remaining_hours => 1.1, :assigned_to_id => 3)
2486
    Issue.generate!(:remaining_hours => 3.5)
2487

  
2488
    q = IssueQuery.new(:group_by => 'assigned_to')
2489
    assert_equal(
2490
      {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
2491
      q.total_by_group_for(:remaining_hours)
2492
    )
2493
  end
2494

  
2467 2495
  def test_total_for_spent_hours
2468 2496
    TimeEntry.delete_all
2469 2497
    TimeEntry.generate!(:hours => 5.5)
(11-11/14)