lib/ResourceScenario.rb in taskjuggler-0.0.4 vs lib/ResourceScenario.rb in taskjuggler-0.0.5

- old
+ new

@@ -18,15 +18,22 @@ class ResourceScenario < ScenarioData def initialize(resource, scenarioIdx, attributes) super - # The scoreboard entries are either nil, a number or a task reference. nil - # means the slot is unassigned. The task reference means assigned to this - # task. The numbers have the following values: - # 1: Off hour - # 2: Vacation + # Scoreboard may be nil, a Task, or a bit vector encoded as a Fixnum + # nil: Value has not been determined yet. + # Task: A reference to a Task object + # Bit 0: Reserved + # Bit 1: 0: Work time + # 1: Off time + # Bit 2 - 5: 0: No vacation or leave time + # 1: Regular vacation + # 2 - 15: Reserved + # Bit 6: Reserved + # Bit 7: 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 @@ -37,10 +44,12 @@ # This method must be called at the beginning of each scheduling run. It # initializes variables used during the scheduling process. def prepareScheduling @property['effort', @scenarioIdx] = 0 initScoreboard + + setDirectReports 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 @@ -58,10 +67,76 @@ @property['criticalness', @scenarioIdx] = freeSlots == 0 ? 1.0 : a('alloctdeffort') / freeSlots end end + def setDirectReports + # Only leaf resources have reporting lines. + return unless @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. + a('managers').each do |manager| + unless manager['directreports', @scenarioIdx].include?(@property) + manager['directreports', @scenarioIdx] << @property + end + end + end + + def setReports + return unless a('directreports').empty? + + a('managers').each do |r| + r.setReports_i(@scenarioIdx, [ @property ]) + end + end + + + def preScheduleCheck + a('managers').each do |manager| + 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 + end + end + + def postScheduleCheck + if a('fail') || a('warn') + queryAttrs = { 'project' => @project, + 'scenarioIdx' => @scenarioIdx, + 'property' => @property, + 'scopeProperty' => nil, + 'start' => @project['start'], + 'end' => @project['end'], + 'loadUnit' => :days, + 'numberFormat' => @project['numberFormat'], + 'timeFormat' => @project['timeFormat'], + 'currencyFormat' => @project['currencyFormat'] } + query = Query.new(queryAttrs) + if a('fail') && a('fail').eval(query) + error('resource_fail_check', + "User defined check failed for resource #{@property.fullId} \n" + + "Condition: #{a('fail').to_s}\n" + + "Result: #{a('fail').to_s(query)}") + end + if a('warn') && a('warn').eval(query) + warning('resource_warn_check', + "User defined warning triggered for resource " + + "#{@property.fullId} \n" + + "Condition: #{a('warn').to_s}\n" + + "Result: #{a('warn').to_s(query)}") + 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? @@ -94,11 +169,13 @@ t = t.parent end a('limits').inc(@scoreboard.idxToDate(sbIdx)) if a('limits') # Make sure the task is in the list of duties. - @property['duties', @scenarioIdx] << task unless a('duties').include?(task) + unless a('duties').include?(task) + @property['duties', @scenarioIdx] << task + end if @firstBookedSlot.nil? || @firstBookedSlot > sbIdx @firstBookedSlot = sbIdx end if @lastBookedSlot.nil? || @lastBookedSlot < sbIdx @@ -116,18 +193,26 @@ "Resource #{@property.fullId} has multiple conflicting " + "bookings for #{@scoreboard.idxToDate(sbIdx)}. The " + "conflicting tasks are #{@scoreboard[sbIdx].fullId} and " + "#{booking.task.fullId}.", true, booking.sourceFileInfo) end - if @scoreboard[sbIdx] > booking.overtime - if @scoreboard[sbIdx] == 1 && booking.sloppy == 0 + 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)}.", true, booking.sourceFileInfo) end - if @scoreboard[sbIdx] == 2 && booking.sloppy <= 1 + 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)}.", true, booking.sourceFileInfo) end @@ -193,10 +278,18 @@ else query.string = 'No revenue account' end end + # The work time of the Resource that was blocked by a vacation during the + # specified Interval. The result is in working days (effort). + def query_vacationdays(query) + query.sortable = query.numerical = time = + getVacationDays(query.startIdx, query.endIdx) + query.string = query.scaleLoad(time) + end + # Returns the work of the resource (and its children) weighted by their # efficiency. def getEffectiveWork(startIdx, endIdx, task = nil) # Convert the interval dates to indexes if needed. startIdx = @project.dateToIdx(startIdx, true) if startIdx.is_a?(TjTime) @@ -270,12 +363,10 @@ @property.children.each do |resource| freeTime += resource.getEffectiveFreeTime(@scenarioIdx, startIdx, endIdx) end else - initScoreboard if @scoreboard.nil? - freeTime = getFreeSlots(startIdx, endIdx) * @project['scheduleGranularity'] end freeTime end @@ -291,19 +382,39 @@ if @property.container? @property.children.each do |resource| work += resource.getEffectiveFreeWork(@scenarioIdx, startIdx, endIdx) end else - initScoreboard if @scoreboard.nil? - work = @project.convertToDailyLoad( getFreeSlots(startIdx, endIdx) * @project['scheduleGranularity']) * a('efficiency') end work end + # Return the number of working days that are blocked by vacations. + def getVacationDays(startIdx, endIdx) + # Convert the interval dates to indexes if needed. + startIdx = @project.dateToIdx(startIdx, true) if startIdx.is_a?(TjTime) + endIdx = @project.dateToIdx(endIdx, true) if endIdx.is_a?(TjTime) + + vacationDays = 0.0 + if @property.container? + @property.children.each do |resource| + vacationDays += resource.getVacationDays(@scenarioIdx, + startIdx, endIdx) + end + else + initScoreboard if @scoreboard.nil? + + vacationDays = @project.convertToDailyLoad( + getVacationSlots(startIdx, endIdx) * + @project['scheduleGranularity']) * a('efficiency') + end + vacationDays + end + def turnover(startIdx, endIdx, account, task = nil) amount = 0.0 if @property.container? @property.children.each do |child| amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, task) @@ -389,24 +500,75 @@ end bookings end # Return a list of scoreboard intervals that are at least _minDuration_ long - # and contain only 1 and 2. These values determine off-hours of the - # resource. The result is an Array of [ start, end ] TjTime values. + # and contain only off-duty and vacation slots. The result is an Array of + # [ start, end ] TjTime values. def collectTimeOffIntervals(iv, minDuration) initScoreboard if @scoreboard.nil? - @scoreboard.collectTimeOffIntervals(iv, minDuration, [ 1, 2 ]) + @scoreboard.collectIntervals(iv, minDuration) do |val| + val.is_a?(Fixnum) && (val & 0x3E) != 0 + 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. + def getAllocatedSlots(startIdx, endIdx, task) + # To speedup the counting we start with the first booked slot and end + # with the last booked slot. + startIdx = @firstBookedSlot if @firstBookedSlot && + startIdx < @firstBookedSlot + endIdx = @lastBookedSlot + 1 if @lastBookedSlot && + endIdx > @lastBookedSlot + 1 + + initScoreboard if @scoreboard.nil? + bookedSlots = 0 + startIdx.upto(endIdx - 1) do |idx| + if (task.nil? && @scoreboard[idx].is_a?(Task)) || + (task && @scoreboard[idx] == task) + bookedSlots += 1 + end + end + + bookedSlots + end + + # Count the free slots between the start and end index. + def getFreeSlots(startIdx, endIdx) + initScoreboard if @scoreboard.nil? + + freeSlots = 0 + startIdx.upto(endIdx - 1) do |idx| + freeSlots += 1 if @scoreboard[idx].nil? + end + + freeSlots + end + + # Count the regular work time slots between the start and end index that + # have been blocked by a vacation. + def getVacationSlots(startIdx, endIdx) + initScoreboard if @scoreboard.nil? + + vacationSlots = 0 + startIdx.upto(endIdx - 1) do |idx| + val = @scoreboard[idx] + # Bit 1 needs to be unset and the vacation bits must not be 0. + vacationSlots += 1 if val.is_a?(Fixnum) && (val & 0x2) == 0 && + (val & 0x3C) != 0 + end + vacationSlots + end + private def initScoreboard # Create scoreboard and mark all slots as unavailable @scoreboard = Scoreboard.new(@project['start'], @project['end'], - @project['scheduleGranularity'], 1) + @project['scheduleGranularity'], 2) # We'll need this frequently and can savely cache it here. @shifts = a('shifts') @workinghours = a('workinghours') @@ -416,91 +578,76 @@ @project.scoreboardSize.times do |i| @scoreboard[i] = nil if onShift?(date) date += delta end - # Mark all resource specific vacation slots as such (2) + # Mark all resource specific vacation slots as such a('vacations').each do |vacation| startIdx = @scoreboard.dateToIdx(vacation.start, true) endIdx = @scoreboard.dateToIdx(vacation.end, true) startIdx.upto(endIdx - 1) do |i| - @scoreboard[i] = 2 + # If the slot is nil, we don't set the time-off bit. + @scoreboard[i] = (@scoreboard[i].nil? ? 0 : 2) | (1 << 2) end end - # Mark all global vacation slots as such (2) + # Mark all global vacation slots as such @project['vacations'].each do |vacation| startIdx = @scoreboard.dateToIdx(vacation.start, true) endIdx = @scoreboard.dateToIdx(vacation.end, true) startIdx.upto(endIdx - 1) do |i| - @scoreboard[i] = 2 + # If the slot is nil or set to 4 then don't set the time-off bit. + sb = @scoreboard[i] + @scoreboard[i] = ((sb.nil? || sb == 4) ? 0 : 2) | (1 << 2) end end unless @shifts.nil? # Mark the vacations from all the shifts the resource is assigned to. @project.scoreboardSize.times do |i| v = @shifts.getSbSlot(@scoreboard.idxToDate(i)) # Check if the vacation replacement bit is set. In that case we copy - # the while interval over to the resource scoreboard overriding any + # the whole interval over to the resource scoreboard overriding any # global vacations. - if (v & (1 << 8)) > 0 - # The ShiftAssignments scoreboard and the ResourceScenario scoreboard - # unfortunately can't use the same values for a certain meaning. So, - # we have to use a map to translate the values. - map = [ nil, nil, 1, 2 ] - @scoreboard[i] = map[v & 0xFF] - elsif (v & 0xFF) == 3 - # 3 in ShiftAssignments means 2 in ResourceScenario (on vacation) - @scoreboard[i] = 2 + if (v & (1 << 8)) != 0 + if (v & 0x3E) == 0 + @scoreboard[i] = nil + else + @scoreboard[i] = v & 0x3E + end + elsif ((sbV = @scoreboard[i]).nil? || (sbV & 0x3C) == 0) && + (v & 0x3C) != 0 + # We only add the shift vacations but don't turn global vacation + # slots into working slots again. + @scoreboard[i] = v & 0x3E end end end end - def onShift?(date) - # The more redable but slower form would be: - # if @shifts.assigned?(date) - # return @shifts.onShift?(date) - # else - # @workinghours.onShift?(date) - # end - if @shifts && (v = (@shifts.getSbSlot(date) & 0xFF)) > 0 - v == 1 - else - @workinghours.onShift?(date) + 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 - end + @property['reports', @scenarioIdx] += reports + # Resources can end up multiple times in the list if they have multiple + # reporting chains. We only need them once in the list. + a('reports').uniq! - # 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. - def getAllocatedSlots(startIdx, endIdx, task) - # To speedup the counting we start with the first booked slot and end - # with the last booked slot. - startIdx = @firstBookedSlot if @firstBookedSlot && - startIdx < @firstBookedSlot - endIdx = @lastBookedSlot + 1 if @lastBookedSlot && - endIdx > @lastBookedSlot + 1 - - bookedSlots = 0 - startIdx.upto(endIdx - 1) do |idx| - if (task.nil? && @scoreboard[idx].is_a?(Task)) || - (task && @scoreboard[idx] == task) - bookedSlots += 1 - end + a('managers').each do |r| + r.setReports_i(@scenarioIdx, a('reports')) end - - bookedSlots end - # Count the free slots between the start and end index. - def getFreeSlots(startIdx, endIdx) - freeSlots = 0 - startIdx.upto(endIdx - 1) do |idx| - freeSlots += 1 if @scoreboard[idx].nil? + def onShift?(date) + if @shifts && @shifts.assigned?(date) + return @shifts.onShift?(date) + else + @workinghours.onShift?(date) end - - freeSlots 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.