File: ResourceScenario.rb

package info (click to toggle)
tj3 3.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,048 kB
  • sloc: ruby: 36,481; javascript: 1,113; sh: 19; makefile: 17
file content (1007 lines) | stat: -rw-r--r-- 36,377 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = ResourceScenario.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2020
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/ScenarioData'

class TaskJuggler

  class ResourceScenario < ScenarioData

    def initialize(resource, scenarioIdx, attributes)
      super

      # Scoreboard may be nil, a Task, or a bit vector encoded as an Integer
      # nil:        Value has not been determined yet.
      # Task:       A reference to a Task object
      # Bit 0:      Reserved
      # Bit 1:      0: Work time (as defined by working hours)
      #             1: No work time (as defined by working hours)
      # Bit 2 - 5:  See Leave class for acutal values.
      # Bit 6 - 7:  Reserved
      # Bit 8:      0: No global override
      #             1: Override global setting
      # The scoreboard is only created when needed to save memory for projects
      # which read-in the coporate employee database but only need a small
      # subset.
      @scoreboard = nil

      # The index of the earliest booked time slot.
      @firstBookedSlot = nil
      # Same but for each assigned resource.
      @firstBookedSlots = {}
      # The index of the last booked time Slot.
      @lastBookedSlot = nil
      # Same but for each assigned resource.
      @lastBookedSlots = {}

      # First available slot of the resource.
      @minslot = nil
      # Last available slot of the resource.
      @maxslot = nil

      # Attributed are only really created when they are accessed the first
      # time. So make sure some needed attributes really exist so we don't
      # have to check for existance each time we access them.
      %w( alloctdeffort chargeset criticalness directreports duties efficiency
          effort limits managers rate reports shifts
          leaves leaveallowances workinghours ).each do |attr|
        @property[attr, @scenarioIdx]
      end

      @dCache = DataCache.instance
    end

    # This method must be called at the beginning of each scheduling run. It
    # initializes variables used during the scheduling process.
    def prepareScheduling
      @effort = 0
      initScoreboard if @property.leaf?
    end

    # The criticalness of a resource is a measure for the probabilty that all
    # allocations can be fullfilled. The smaller the value, the more likely
    # will the tasks get the resource. A value above 1.0 means that
    # statistically some tasks will not get their resources. A value between
    # 0 and 1 implies no guarantee, though.
    def calcCriticalness
      if @scoreboard.nil?
        # Resources that are not allocated are not critical at all.
        @criticalness = 0.0
      else
        freeSlots = 0
        @scoreboard.each do |slot|
          freeSlots += 1 if slot.nil?
        end
        @criticalness = freeSlots == 0 ? 1.0 :
          @alloctdeffort / freeSlots
      end
    end

    def setDirectReports
      @managers.map! do |managerId|
        # First we need to map 'managerId' to an existing Resource.
        if (manager = @project.resource(managerId)).nil?
          error('resource_id_expected', "#{managerId} is not a defined " +
                'resource.', @sourceFileInfo)
        end
        unless manager.leaf?
          error('manager_is_group',
                "Resource #{@property.fullId} has group #{manager.fullId} " +
                "assigned as manager. Managers must be leaf resources.")
        end
        if manager == @property
          error('manager_is_self',
                "Resource #{@property.fullId} cannot manage itself.")
        end

        # Only leaf resources have reporting lines.
        if @property.leaf?
          # The 'directreports' attribute is the reverse link for the 'managers'
          # attribute. In contrast to the 'managers' attribute, the
          # 'directreports' list has no duplicate entries.
          unless manager['directreports', @scenarioIdx].include?(@property)
            manager['directreports', @scenarioIdx] << @property
          end
        end

        manager
      end
      @managers.uniq!
    end

    def setReports
      return unless @directreports.empty?

      @managers.each do |r|
        r.setReports_i(@scenarioIdx, [ @property ])
      end
    end

    def preScheduleCheck
    end

    # This method does some housekeeping work after the scheduling is
    # completed. It's meant to be called for top-level resources and then
    # recursively descends into all child resources.
    def finishScheduling
      # Recursively descend into all child resources.
      @property.children.each do |resource|
        resource.finishScheduling(@scenarioIdx)
      end

      # Add the parent tasks of each task to the duties list.
      @duties.each do |task|
        task.ancestors(true).each do |pTask|
          @duties << pTask unless @duties.include?(pTask)
        end
      end

      # Add the assigned task to the parent(s) resource duties list.
      @property.parents.each do |pResource|
        @duties.each do |task|
          unless pResource['duties', @scenarioIdx].include?(task)
            pResource['duties', @scenarioIdx] << task
          end
        end
      end
    end

    # Returns true if the resource is available at the time specified by
    # _sbIdx_.
    def available?(sbIdx)
      return false unless @scoreboard[sbIdx].nil?

      limits = @limits
      return false if limits && !limits.ok?(sbIdx)

      true
    end

    # Return true if the resource is booked for a tasks at the time specified by
    # _sbIdx_.
    def booked?(sbIdx)
      @scoreboard[sbIdx].is_a?(Task)
    end

    # Return the Task that this resource is booked for at the time specified
    # by _sbIdx_. If not booked to a task, nil is returned.
    def bookedTask(sbIdx)
      return nil unless (sb = @scoreboard[sbIdx]).is_a?(Task)
      sb
    end

    # Book the slot indicated by the scoreboard index +sbIdx+ for Task +task+.
    # If +force+ is true, overwrite the existing booking for this slot. The
    # method returns true if the slot was available.
    def book(sbIdx, task, force = false)
      return false if !force && !available?(sbIdx)

      # Make sure the task is in the list of duties.
      unless @duties.include?(task)
        @duties << task
      end

      #puts "Booking resource #{@property.fullId} at " +
      #     "#{@scoreboard.idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n"
      @scoreboard[sbIdx] = task
      # Track the total allocated slots for this resource.
      @effort += @efficiency
      @limits.inc(sbIdx) if @limits
      task.incLimits(@scenarioIdx, sbIdx, @property)

      # Scoreboard iterations are fairly expensive but they are very frequent
      # operations in later processing. To limit the interations to the
      # relevant intervals, we store the interval for all bookings and for
      # each individual task.
      if @firstBookedSlot.nil? || @firstBookedSlot > sbIdx
        @firstBookedSlot = @firstBookedSlots[task] = sbIdx
      elsif @firstBookedSlots[task].nil? || @firstBookedSlots[task] > sbIdx
        @firstBookedSlots[task] = sbIdx
      end
      if @lastBookedSlot.nil? || @lastBookedSlot < sbIdx
        @lastBookedSlot = @lastBookedSlots[task] = sbIdx
      elsif @lastBookedSlots[task].nil? || @lastBookedSlots[task] < sbIdx
        @lastBookedSlots[task] = sbIdx
      end

      true
    end

    def bookBooking(sbIdx, booking)
      initScoreboard if @scoreboard.nil?

      unless @scoreboard[sbIdx].nil?
        if booked?(sbIdx)
          error('booking_conflict',
                "Resource #{@property.fullId} has multiple conflicting " +
                "bookings for #{@scoreboard.idxToDate(sbIdx)}. The " +
                "conflicting tasks are #{@scoreboard[sbIdx].fullId} and " +
                "#{booking.task.fullId}.", booking.sourceFileInfo)
        end
        val = @scoreboard[sbIdx]
        if ((val & 2) != 0 && booking.overtime < 1)
          # The booking is blocked due to the overtime attribute. Now let's
          # see if the user wants to be warned about it.
          if booking.sloppy < 1
            error('booking_no_duty',
                  "Resource #{@property.fullId} has no duty at " +
                  "#{@scoreboard.idxToDate(sbIdx)}.",
                  booking.sourceFileInfo)
          end
          return false
        end
        if ((val & 0x3C) != 0 && booking.overtime < 2)
          # The booking is blocked due to the overtime attribute. Now let's
          # see if the user wants to be warned about it.
          if booking.sloppy < 2
            error('booking_on_vacation',
                  "Resource #{@property.fullId} is on vacation at " +
                  "#{@scoreboard.idxToDate(sbIdx)}.",
                  booking.sourceFileInfo)
          end
          return false
        end
      end

      book(sbIdx, booking.task, true)
    end

    # @effort only trackes the already allocated effort for leaf resources. It's
    # too expensive to propagate this to the group resources on every booking.
    # If a value for a group effort is needed, it's computed here.
    def bookedEffort
      if @property.leaf?
        @effort
      else
        effort = 0
        @property.kids.each do |r|
          effort += r.bookedEffort(@scenarioIdx)
        end
        effort
      end
    end

    # Compute the annual leave days within the period specified by the
    # _query_. The result is in days.
    def query_annualleave(query)
      query.sortable = query.numerical = val =
        getLeave(query.startIdx, query.endIdx, :annual)
      query.string = query.scaleLoad(val)
    end

    # Compute a list of the annual leave days within the period specified by
    # the _query_. The result is a list of dates and how much of that day was
    # taken.
    def query_annualleavelist(query)
      iv_list = collectLeaveIntervals(Interval.new(query.start, query.end), :annual)
      iv_list.map! do |iv|
        # The interval is at most one working day. We list the date of that
        # day.
        day = iv.start.strftime(@project['timeFormat'])
        # And how much of the working day was taken. A full working day is
        # 1.0, a half working day 0.5.
        days = (iv.end - iv.start) / (60 * 60 * @project['dailyworkinghours'])
        "#{day} (#{'%.1f' % days})"
      end
      query.assignList(iv_list)
    end

    def query_annualleavebalance(query)
      if @property.leaf?
        leave = getLeave(query.startIdx, query.endIdx, :annual)
        allowanceSlots = @leaveallowances.balance(:annual, query.start,
                                                  query.end)
        allowance = @project.slotsToDays(allowanceSlots)
        query.sortable = query.numerical = val = allowance - leave
        query.string = query.scaleLoad(val)
      end
    end


    # Compute the cost generated by this Resource for a given Account during a
    # given interval.  If a Task is provided as scopeProperty only the turnover
    # directly assiciated with the Task is taken into account.
    def query_cost(query)
      if query.costAccount
        query.sortable = query.numerical = cost =
          turnover(query.startIdx, query.endIdx, query.costAccount,
                   query.scopeProperty, true)
        query.string = query.currencyFormat.format(cost)
      else
        query.string = 'No \'balance\' defined!'
      end
    end

    # A list of the tasks that the resource has been  allocated to work on in
    # the report time frame.
    def query_duties(query)
      list = []
      iv = TimeInterval.new(query.start, query.end)
      @duties.each do |task|
        if task.hasResourceAllocated?(@scenarioIdx, iv, @property)
          if query.listItem
            rti = RichText.new(query.listItem, RTFHandlers.create(@project)).
              generateIntermediateFormat
            unless rti
              error('bad_resource_ts_query',
                    "Syntax error in query statement for task attribute " +
                    "'resources'.")
            end
            q = query.dup
            q.property = task
            q.scopeProperty = @property
            rti.setQuery(q)
            list << "<nowiki>#{rti.to_s}</nowiki>"
          else
            list << "<nowiki>#{task.name} (#{task.id})</nowiki>"
          end
        end
      end
      query.assignList(list)
    end

    # The effort allocated to the Resource in the specified interval. In case a
    # Task is given as scope property only the effort allocated to this Task is
    # taken into account.
    def query_effort(query)
      query.sortable = query.numerical = effort =
        getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty)
      query.string = query.scaleLoad(effort)
    end

    # The completed (as of 'now') effort allocated for the resource in the
    # specified interval.  In case a Task is given as scope property only
    # the effort allocated for this Task is taken into account.
    def query_effortdone(query)
      # For this query, we always override the query period.
      query.sortable = query.numerical = effort =
        getEffectiveWork(@project.dateToIdx(@project['start'], false),
                         @project.dateToIdx(@project['now']),
                         query.scopeProperty)
      query.string = query.scaleLoad(effort)
    end


    # The remaining (as of 'now') effort allocated for the resource in the
    # specified interval.  In case a Task is given as scope property only
    # the effort allocated for this Task is taken into account.
    def query_effortleft(query)
      # For this query, we always override the query period.
      query.sortable = query.numerical = effort =
        getEffectiveWork(@project.dateToIdx(@project['now']),
                         @project.dateToIdx(@project['end'], false),
                         query.scopeProperty)
      query.string = query.scaleLoad(effort)
    end

    # The unallocated work time of the Resource during the specified interval.
    def query_freetime(query)
      query.sortable = query.numerical = time =
        getEffectiveFreeTime(query.startIdx, query.endIdx) / (60 * 60 * 24)
      query.string = query.scaleDuration(time)
    end

    # The unallocated effort of the Resource during the specified interval.
    def query_freework(query)
      query.sortable = query.numerical = work =
        getEffectiveFreeWork(query.startIdx, query.endIdx)
      query.string = query.scaleLoad(work)
    end

    # The the Full-time equivalent for the resource or group.
    def query_fte(query)
      fte = 0.0
      if @property.container?
        # Accumulate the FTEs of all sub-resources.
        @property.kids.each do |resource|
          resource.query_fte(@scenarioIdx, query)
          fte += query.to_num
        end
      else
        # TODO: Getting the globalWorkSlots is relatively expensive. We
        # probably don't need to compute this for every resource.
        globalWorkSlots = @project.getWorkSlots(query.startIdx, query.endIdx)
        workSlots = getWorkSlots(query.startIdx, query.endIdx)
        if globalWorkSlots > 0
          fte = (workSlots.to_f / globalWorkSlots) * @efficiency
        end
      end

      query.sortable = query.numerical = fte
      query.string = query.numberFormat.format(fte)
    end

    # The headcount of the resource or group.
    def query_headcount(query)
      headcount = 0
      if @property.container?
        @property.kids.each do |resource|
          resource.query_headcount(@scenarioIdx, query)
          headcount += query.to_num
        end
      else
        if employed?(@project.dateToIdx(query.start))
          # We only count headcount that is employed at the start date of the
          # query interval.
          headcount += @efficiency.round
        end
      end

      query.sortable = query.numerical = headcount
      query.string = query.numberFormat.format(headcount)
    end

    # Get the rate of the resource.
    def query_rate(query)
      query.sortable = query.numerical = r = rate
      query.string = query.currencyFormat.format(r)
    end

    # Compute the revenue generated by this Resource for a given Account during
    # a given interval.  If a Task is provided as scopeProperty only the
    # revenue directly associated to this Task is taken into account.
    def query_revenue(query)
      if query.revenueAccount
        query.sortable = query.numerical = revenue =
          turnover(query.startIdx, query.endIdx, query.revenueAccount,
                   query.scopeProperty)
        query.string = query.currencyFormat.format(revenue)
      else
        query.string = 'No \'balance\' defined!'
      end
    end

    # Compute the sick leave days within the period specified by the
    # _query_. The result is in days.
    def query_sickleave(query)
      query.sortable = query.numerical = val =
        getLeave(query.startIdx, query.endIdx, :sick)
      query.string = query.scaleLoad(val)
    end

    # Compute the special leave days within the period specified by the
    # _query_. The result is in days.
    def query_specialleave(query)
      query.sortable = query.numerical = val =
        getLeave(query.startIdx, query.endIdx, :special)
      query.string = query.scaleLoad(val)
    end

    # The work time of the Resource that was blocked by leaves during the
    # specified TimeInterval. The result is in working days (effort).
    def query_timeoffdays(query)
      query.sortable = query.numerical = time =
        getTimeOffDays(query.startIdx, query.endIdx)
      query.string = query.scaleLoad(time)
    end

    # Compute the unpaid leave days within the period specified by the
    # _query_. The result is in days.
    def query_unpaidleave(query)
      query.sortable = query.numerical = val =
        getLeave(query.startIdx, query.endIdx, :unpaid)
      query.string = query.scaleLoad(val)
    end

    # A generic tree iterator that recursively accumulates the result of the
    # block for each leaf object.
    def treeSum(startIdx, endIdx, *args, &block)
      cacheTag = "#{self.class}.#{caller[0][/`.*'/][1..-2]}"
      treeSumR(cacheTag, startIdx, endIdx, *args, &block)
    end

    # Recursing method for treeSum.
    def treeSumR(cacheTag, startIdx, endIdx, *args, &block)
      # Check if the value to be computed is already in the data cache. If so,
      # return it. Otherwise we have to compute it first and then store it in
      # the cache. We use the signature of the method that called treeSum()
      # and its arguments together with 'self' as index to the cache.
      @dCache.cached(self, cacheTag, startIdx, endIdx, *args) do
        if @property.container?
          sum = 0.0
          # Iterate over all the kids and accumulate the result of the
          # recursively called method.
          @property.kids.each do |resource|
            sum += resource.treeSumR(@scenarioIdx, cacheTag, startIdx, endIdx,
                                     *args, &block)
          end
          sum
        else
          instance_eval(&block)
        end
      end
    end

    # Returns the number of leave days for the period described by _startIdx_
    # and _endIdx_ for the given _type_ of leave.
    def getLeave(startIdx, endIdx, type)
      treeSum(startIdx, endIdx, type) do
        @project.convertToDailyLoad(@project['scheduleGranularity'] *
            getLeaveSlots(startIdx, endIdx, type))
      end
    end


    # Returns the work of the resource (and its children) weighted by their
    # efficiency. If _task_ is provided, only the work for this task and all
    # its sub tasks are being counted.
    def getEffectiveWork(startIdx, endIdx, task = nil)
      # Make sure we have the real Task and not a proxy.
      task = task.ptn if task
      # There can't be any effective work if the start is after the end or the
      # todo list doesn't contain the specified task.
      return 0.0 if startIdx >= endIdx || (task && !@duties.include?(task))
      # Temporary workaround until @duties is fixed again.

      # The unique key we use to address the result in the cache.
      @dCache.cached(self, :ResourceScenarioGetEffectiveWork, startIdx, endIdx,
                     task) do
        work = 0.0
        if @property.container?
          @property.kids.each do |resource|
            work += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx,
                                              task)
          end
        else
          unless @scoreboard.nil?
            work = @project.convertToDailyLoad(
                     getAllocatedSlots(startIdx, endIdx, task) *
                     @project['scheduleGranularity']) * @efficiency
          end
        end
        work
      end
    end

    # Returns the allocated accumulated time of this resource and its children.
    def getAllocatedTime(startIdx, endIdx, task = nil)
      treeSum(startIdx, endIdx, task) do
        return 0 if @scoreboard.nil?

        @project.convertToDailyLoad(@project['scheduleGranularity'] *
            getAllocatedSlots(startIdx, endIdx, task))
      end
    end

    # Return the unallocated work time (in seconds) of the resource and its
    # children.
    def getEffectiveFreeTime(startIdx, endIdx)
      treeSum(startIdx, endIdx) do
        getFreeSlots(startIdx, endIdx) * @project['scheduleGranularity']
      end
    end

    # Return the unallocated work of the resource and its children weighted by
    # their efficiency.
    def getEffectiveFreeWork(startIdx, endIdx)
      treeSum(startIdx, endIdx) do
        @project.convertToDailyLoad(getFreeSlots(startIdx, endIdx) *
                                    @project['scheduleGranularity']) *
                                    @efficiency
      end
    end

    # Return the number of working days that are blocked by leaves.
    def getTimeOffDays(startIdx, endIdx)
      treeSum(startIdx, endIdx) do
        @project.convertToDailyLoad(getTimeOffSlots(startIdx, endIdx) *
                                    @project['scheduleGranularity']) *
                                    @efficiency
      end
    end

    def turnover(startIdx, endIdx, account, task = nil, includeKids = false)
      amount = 0.0
      if @property.container? && includeKids
        @property.kids.each do |child|
          amount += child.turnover(@scenarioIdx, startIdx, endIdx, account,
                                   task)
        end
      else
        if task
          # If we have a known task, we only include the amount that is
          # specific to this resource, this task and the chargeset of the
          # task.
          amount += task.turnover(@scenarioIdx, startIdx, endIdx, account,
                                  @property)
        elsif !@chargeset.empty?
          # If no tasks was provided, we include the amount of this resource,
          # weighted by the chargeset of this resource.
          totalResourceCost = cost(startIdx, endIdx)
          @chargeset.each do |set|
            set.each do |accnt, share|
              if share > 0.0 && (accnt == account || accnt.isChildOf?(account))
                amount += totalResourceCost * share
              end
            end
          end
        end
      end

      amount
    end

    # Returns the cost for using this resource during the specified
    # TimeInterval _period_. If a Task _task_ is provided, only the work on
    # this particular task is considered.
    def cost(startIdx, endIdx, task = nil)
      getAllocatedTime(startIdx, endIdx, task) * @rate
    end

    # Returns true if the resource or any of its children is allocated during
    # the period specified with the TimeInterval _iv_. If task is not nil
    # only allocations to this tasks are respected.
    def allocated?(iv, task = nil)
      return false if task && !@duties.include?(task)

      startIdx = @project.dateToIdx(iv.start)
      endIdx = @project.dateToIdx(iv.end)

      startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
      return false if startIdx >= endIdx

      return allocatedSub(startIdx, endIdx, task)
    end

    # Iterate over the scoreboard and turn its content into a set of Bookings.
    #  _iv_ can be a TimeInterval to limit the bookings within the provided
    #  period. if _hashByTask_ is true, the result is a Hash of Arrays with
    #  bookings hashed by Task. Otherwise it's just a plain Array with
    #  Bookings.
    def getBookings(iv = nil, hashByTask = true)
      bookings = hashByTask ? {} : []
      return bookings if @property.container? || @scoreboard.nil? ||
                         @firstBookedSlot.nil? || @lastBookedSlot.nil?

      # To speedup the collection we start with the first booked slot and end
      # with the last booked slot.
      startIdx = @firstBookedSlot
      endIdx = @lastBookedSlot + 1

      # If the user provided a TimeInterval, we only return bookings within
      # this TimeInterval.
      if iv
        ivStartIdx = @project.dateToIdx(iv.start)
        ivEndIdx = @project.dateToIdx(iv.end)
        startIdx = ivStartIdx if ivStartIdx > startIdx
        endIdx = ivEndIdx if ivEndIdx < endIdx
      end

      lastTask = nil
      bookingStart = nil

      startIdx.upto(endIdx) do |idx|
        task = @scoreboard[idx]
        # Now we watch for task changes.
        if task != lastTask ||
           (task.is_a?(Task) && (lastTask.nil? || idx == endIdx))
          if lastTask
            # We've found the end of a task booking series.
            # If we don't have a Booking for the task yet, we create one.
            if hashByTask
              if bookings[lastTask].nil?
                bookings[lastTask] = Booking.new(@property, lastTask, [])
              end
              # Append the new interval to the Booking.
              bookings[lastTask].intervals <<
              TimeInterval.new(@scoreboard.idxToDate(bookingStart),
                               @scoreboard.idxToDate(idx))
            else
              if bookings.empty? || bookings.last.task != lastTask
                bookings << Booking.new(@property, lastTask, [])
              end
              # Append the new interval to the Booking.
              bookings.last.intervals <<
              TimeInterval.new(@scoreboard.idxToDate(bookingStart),
                               @scoreboard.idxToDate(idx))
            end

          end
          # Get ready for the next task booking interval
          if task.is_a?(Task)
            lastTask = task
            bookingStart = idx
          else
            lastTask = bookingStart = nil
          end
        end
      end
      bookings
    end

    # Return a list of scoreboard intervals that are at least _minDuration_ long
    # and contain only off-duty and leave slots. The result is an Array of
    # [ start, end ] TjTime values.
    def collectTimeOffIntervals(iv, minDuration)
      # Time-off intervals are only useful for leaf resources. Group resources
      # would just default to the global working hours.
      return [] unless @property.leaf?

      initScoreboard if @scoreboard.nil?

      @scoreboard.collectIntervals(iv, minDuration) do |val|
        val.is_a?(Integer) && (val & 0x3E) != 0
      end
    end

    # Return a list of scoreboard intervals that are at least _minDuration_ long
    # and only contain leave slots of the given type. The result is an Array of
    # [ start, end ] TjTime values.
    def collectLeaveIntervals(iv, type)
      # Time-off intervals are only useful for leaf resources. Group resources
      # would just default to the global working hours.
      return [] unless @property.leaf?

      initScoreboard if @scoreboard.nil?

      @scoreboard.collectIntervals(iv, 60 * 60) do |val|
        val.is_a?(Integer) && (val & 0x3E) == (Leave::Types[type] << 2)
      end
    end

    # Count the booked slots between the start and end index. If _task_ is not
    # nil count only those slots that are assigned to this particular task or
    # any of its sub tasks.
    def getAllocatedSlots(startIdx, endIdx, task = nil)
      # If there is no scoreboard, we don't have any allocations.
      return 0 unless @scoreboard

      startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
      return 0 if startIdx >= endIdx

      bookedSlots = 0
      taskList = task ? task.all : []
      @scoreboard.each(startIdx, endIdx) do |slot|
        if slot.is_a?(Task) && (task.nil? || taskList.include?(slot))
          bookedSlots += 1
        end
      end

      bookedSlots
    end

    # Count the number of slots betweend the _startIdx_ and _endIdx_ that can
    # be used for work
    def getWorkSlots(startIdx, endIdx)
      countSlots(startIdx, endIdx) do |val|
        # We count free slots and assigned slots.
        val.nil? || val.is_a?(Task)
      end
    end

    # Count the number of slots that are work time slots but marked as annual
    # leave.
    def getLeaveSlots(startIdx, endIdx, type)
      countSlots(startIdx, endIdx) do |val|
        val.is_a?(Integer) && (val & 0x3E) == (Leave::Types[type] << 2)
      end
    end

    # Count the free slots between the start and end index.
    def getFreeSlots(startIdx, endIdx)
      countSlots(startIdx, endIdx) do |val|
        val.nil?
      end
    end

    # Count the regular work time slots between the start and end index that
    # have been blocked by leaves.
    def getTimeOffSlots(startIdx, endIdx)
      countSlots(startIdx, endIdx) do |val|
        # Bit 1 needs to be unset and the leave bits must not be 0.
        val.is_a?(Integer) && (val & 0x2) == 0 && (val & 0x3C) != 0
      end
    end

    # Get the first available slot of the resource.
    def getMinSlot
      initScoreboard unless @minslot

      @minslot
    end

    # Get the last available slot of the resource.
    def getMaxSlot
      initScoreboard unless @maxslot

      @maxslot
    end

  private

    def initScoreboard
      # Create scoreboard and mark all slots as non-working-time.
      @scoreboard = Scoreboard.new(@project['start'], @project['end'],
                                   @project['scheduleGranularity'], 2)

      # Change all work time slots to nil (available) again.
      @project.scoreboardSize.times do |i|
        @scoreboard[i] = nil if onShift?(i)
      end

      # Mark all global leave slots as such
      @project['leaves'].each do |leave|
        startIdx = @scoreboard.dateToIdx(leave.interval.start)
        endIdx = @scoreboard.dateToIdx(leave.interval.end)
        startIdx.upto(endIdx - 1) do |i|
          sb = @scoreboard[i]
          # We preseve the work-time bit (#1).
          @scoreboard[i] = (sb.nil? ? 0 : 2) | (leave.typeIdx << 2)
        end
      end

      # Mark all resource specific leave slots as such
      @leaves.each do |leave|
        startIdx = @scoreboard.dateToIdx(leave.interval.start)
        endIdx = @scoreboard.dateToIdx(leave.interval.end)
        startIdx.upto(endIdx - 1) do |i|
          if (sb = @scoreboard[i])
            # The slot is already marked as non-working slot. We override the
            # leave type if the new type is larger than the old one.
            leaveIdx = (sb & 0x3C) >> 2
            if leave.typeIdx > leaveIdx
              # The work-time bit (#1) is preserved.
              @scoreboard[i] = (sb & 0x2) | (leave.typeIdx << 2)
            end
          else
            # This marks a working time slot as a leave slot. Since bit 1 is
            # not set, we still know that this could be a working slot.
            @scoreboard[i] = leave.typeIdx << 2
          end
        end
      end

      unless @shifts.nil?
        # Mark the leaves from all the shifts the resource is assigned to.
        @project.scoreboardSize.times do |i|
          v = @shifts.getSbSlot(i)
          # Make sure a shift is actually assigned.
          next unless v

          if (v & (1 << 8)) != 0
            # Check if the leave replacement bit (#8) is set. In that case we
            # copy the whole interval over to the resource scoreboard
            # overriding any global leaves.
            @scoreboard[i] = (v & 0x3E == 0) ? nil : (v & 0x3D)
          elsif ((sb = @scoreboard[i]).nil? || ((sb & 0x3C) < (v & 0x3C))) &&
                (v & 0x3C) != 0
            # In merge mode, we only add the shift leaves with higher type
            # index or unassigned slots.
            @scoreboard[i] = v & 0x3E
          end
        end
      end

      # Set minimum and maximum availability
      idx = 0
      while idx < @scoreboard.size
        if available?(idx)
          @minslot = idx
          break
        end
        idx += 1
      end
      idx = @scoreboard.size - 1
      while idx >= 0
        if available?(idx)
          @maxslot = idx
          break
        end
        idx -= 1
      end
    end

    def countSlots(startIdx, endIdx)
      return 0 if startIdx >= endIdx

      initScoreboard unless @scoreboard

      slots = 0
      startIdx.upto(endIdx - 1) do |idx|
        slots += 1 if yield(@scoreboard[idx])
      end
      slots
    end

    # Limit the _startIdx_ and _endIdx_ to the actually assigned interval.
    # If _task_ is provided, fit it for the bookings of this particular task.
    def fitIndicies(startIdx, endIdx, task = nil)
      if task
        startIdx = @firstBookedSlots[task] if @firstBookedSlots[task] &&
                                              startIdx < @firstBookedSlots[task]
        endIdx = @lastBookedSlots[task] + 1 if @lastBookedSlots[task] &&
                                               endIdx >
                                               @lastBookedSlots[task] + 1
      else
        startIdx = @firstBookedSlot if @firstBookedSlot &&
                                       startIdx < @firstBookedSlot
        endIdx = @lastBookedSlot + 1 if @lastBookedSlot &&
                                        endIdx > @lastBookedSlot + 1
      end
      [ startIdx, endIdx ]
    end

    def setReports_i(reports)
      if reports.include?(@property)
        # A manager must never show up in the list of his/her own reports.
        error('manager_loop',
              "Management loop detected. #{@property.fullId} has self " +
              "in list of reports")
      end
      @reports += reports
      # Resources can end up multiple times in the list if they have multiple
      # reporting chains. We only need them once in the list.
      @reports.uniq!

      @managers.each do |r|
        r.setReports_i(@scenarioIdx, @reports)
      end
    end

    def onShift?(sbIdx)
      if @shifts && @shifts.assigned?(sbIdx)
        return @shifts.onShift?(sbIdx)
      else
        @workinghours.onShift?(sbIdx)
      end
    end

    def employed?(sbIdx)
      initScoreboard unless @scoreboard

      val = @scoreboard[sbIdx]
      return true unless val.is_a?(Integer)

      leave_type = (val >> 2) & 0xF
      leave_type < Leave::Types[:unemployed]
    end

    # Returns true if the resource or any of its children is allocated during
    # the period specified with _startIdx_ and _endIdx_. If task is not nil
    # only allocations to this tasks are respected.
    def allocatedSub(startIdx, endIdx, task)
      if @property.container?
        @property.kids.each do |resource|
          return true if resource.allocatedSub(@scenarioIdx, startIdx, endIdx,
                                               task)
        end
      else
        return false unless @scoreboard && @duties.include?(task)

        startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
        return false if startIdx >= endIdx

        startIdx.upto(endIdx - 1) do |idx|
          return true if @scoreboard[idx] == task
        end
      end
      false
    end

    # Return the daily cost of a resource or resource group.
    def rate
      if @property.container?
        dailyRate = 0.0
        @property.kids.each do |resource|
          dailyRate += resource.rate(@scenarioIdx)
        end
        dailyRate
      else
        @rate
      end
    end

  end

end