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.