Create a new TaskScenario object.
# File lib/TaskScenario.rb, line 22 22: def initialize(task, scenarioIdx, attributes) 23: super 24: 25: # A list of all allocated leaf resources. 26: @candidates = [] 27: end
The parser only stores the full task IDs for each of the dependencies. This function resolves them to task references and checks them. In addition to the ‘depends’ and ‘precedes’ property lists we also keep 4 additional lists. startpreds: All precedessors to the start of this task startsuccs: All successors to the start of this task endpreds: All predecessors to the end of this task endsuccs: All successors to the end of this task Each list element consists of a reference/boolean pair. The reference points to the dependent task and the boolean specifies whether the dependency originates from the end of the task or not.
# File lib/TaskScenario.rb, line 109 109: def Xref 110: @property['depends', @scenarioIdx].each do |dependency| 111: depTask = checkDependency(dependency, 'depends') 112: a('startpreds').push([ depTask, dependency.onEnd ]) 113: depTask[dependency.onEnd ? 'endsuccs' : 'startsuccs', @scenarioIdx]. 114: push([ @property, false ]) 115: end 116: 117: @property['precedes', @scenarioIdx].each do |dependency| 118: predTask = checkDependency(dependency, 'precedes') 119: a('endsuccs').push([ predTask, dependency.onEnd ]) 120: predTask[dependency.onEnd ? 'endpreds' : 'startpreds', @scenarioIdx]. 121: push([@property, true ]) 122: end 123: end
# File lib/TaskScenario.rb, line 1090 1090: def addBooking(booking) 1091: if a('booking').empty? 1092: # For the first item use the assignment form so that the 'provided' 1093: # attribute is set properly. 1094: @property['booking', @scenarioIdx] = [ booking ] 1095: else 1096: @property['booking', @scenarioIdx] << booking 1097: end 1098: end
Determine the criticalness of the individual task. This is a measure for the likelyhood that this task will get the resources that it needs to complete the effort. Tasks without effort are not cricital. The only exception are milestones which get an arbitrary value between 0 and 2 based on their priority.
# File lib/TaskScenario.rb, line 686 686: def calcCriticalness 687: @property['criticalness', @scenarioIdx] = 0.0 688: @property['pathcriticalness', @scenarioIdx] = nil 689: 690: # Users feel that milestones are somewhat important. So we use an 691: # arbitrary value larger than 0 for them. We make it priority dependent, 692: # so the user has some control over it. Priority 0 is 0, 500 is 1.0 and 693: # 1000 is 2.0. These values are pretty much randomly picked and probably 694: # require some more tuning based on real projects. 695: if a('milestone') 696: @property['criticalness', @scenarioIdx] = a('priority') / 500.0 697: end 698: 699: # Task without efforts of allocations are not critical. 700: return if a('effort') <= 0 || @candidates.empty? 701: 702: # Determine the average criticalness of all allocated resources. 703: criticalness = 0.0 704: @candidates.each do |resource| 705: criticalness += resource['criticalness', @scenarioIdx] 706: end 707: criticalness /= @candidates.length 708: 709: # The task criticalness is the product of effort and average resource 710: # criticalness. 711: @property['criticalness', @scenarioIdx] = a('effort') * criticalness 712: end
The path criticalness is a measure for the overall criticalness of the task taking the dependencies into account. The fact that a task is part of a chain of effort-based task raises all the task in the chain to a higher criticalness level than the individual tasks. In fact, the path criticalness of this chain is equal to the sum of the individual criticalnesses of the tasks.
# File lib/TaskScenario.rb, line 720 720: def calcPathCriticalness(atEnd = false) 721: # If we have computed this already, just return the value. If we are only 722: # at the end of the task, we do not include the criticalness of this task 723: # as it is not really part of the path. 724: if a('pathcriticalness') 725: return a('pathcriticalness') - (atEnd ? 0 : a('criticalness')) 726: end 727: 728: maxCriticalness = 0.0 729: 730: if atEnd 731: # At the end, we only care about pathes through the successors of this 732: # task or its parent tasks. 733: if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness 734: maxCriticalness = criticalness 735: end 736: else 737: # At the start of the task, we have two options. 738: if @property.container? 739: # For container tasks, we ignore all dependencies and check the pathes 740: # through all the children. 741: @property.children.each do |task| 742: if (criticalness = task.calcPathCriticalness(@scenarioIdx, false)) > 743: maxCriticalness 744: maxCriticalness = criticalness 745: end 746: end 747: else 748: # For leaf tasks, we check all pathes through the start successors and 749: # then the pathes through the end successors of this task and all its 750: # parent tasks. 751: a('startsuccs').each do |task, onEnd| 752: if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) > 753: maxCriticalness 754: maxCriticalness = criticalness 755: end 756: end 757: 758: if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness 759: maxCriticalness = criticalness 760: end 761: 762: maxCriticalness += a('criticalness') 763: end 764: end 765: 766: @property['pathcriticalness', @scenarioIdx] = maxCriticalness 767: end
This function determines if a task can inherit the start or end date from a parent task or the project time frame. atEnd specifies whether the check should be done for the task end (true) or task start (false).
# File lib/TaskScenario.rb, line 876 876: def canInheritDate?(atEnd) 877: # Inheriting a start or end date from the enclosing task or the project 878: # is allowed for the following scenarios: 879: # - --> - inherit start and end when no bookings but allocations 880: # present 881: # - <-- - dito 882: # 883: # - x-> - inhS 884: # - x-> | inhS 885: # - x-> -D inhS 886: # - x-> |D inhS 887: # - --> | inhS 888: # - --> -D inhS 889: # - --> |D inhS 890: # - <-- | inhS 891: # | --> - inhE 892: # | <-x - inhE 893: # |D <-x - inhE 894: # - <-x - inhE 895: # -D <-x - inhE 896: # | <-- - inhE 897: # |D <-- - inhE 898: # -D <-- - inhE 899: # Return false if we already have a date or if we have a dependency for 900: # this end. 901: thisEnd = atEnd ? 'end' : 'start' 902: hasThisDeps = !a(thisEnd + 'preds').empty? || !a(thisEnd + 'succs').empty? 903: hasThisSpec = a(thisEnd) || hasThisDeps 904: return false if hasThisSpec 905: 906: # Containter task can inherit the date if they have no dependencies. 907: return true if @property.container? 908: 909: thatEnd = atEnd ? 'start' : 'end' 910: hasThatDeps = !a(thatEnd + 'preds').empty? || !a(thatEnd + 'succs').empty? 911: hasThatSpec = a(thatEnd) || hasThatDeps 912: 913: # Check for tasks that have no start and end spec, no duration spec but 914: # allocates. They can inherit the start and end date. 915: return true if hasThatSpec && !hasDurationSpec? && !a('allocate').empty? 916: 917: if a('forward') ^ atEnd 918: # the scheduling direction is pointing away from this end 919: return true if hasDurationSpec? || !a('booking').empty? 920: 921: return hasThatSpec 922: else 923: # the scheduling direction is pointing towards this end 924: return a(thatEnd) && !hasDurationSpec? && 925: a('booking').empty? #&& a('allocate').empty? 926: end 927: end
This function must be called before prepareScheduling(). It compiles the list of leaf resources that are allocated to this task.
# File lib/TaskScenario.rb, line 656 656: def candidates 657: @candidates = [] 658: a('allocate').each do |allocation| 659: allocation.candidates.each do |candidate| 660: candidate.allLeaves.each do |resource| 661: @candidates << resource unless @candidates.include?(resource) 662: end 663: end 664: end 665: @candidates 666: end
# File lib/TaskScenario.rb, line 497 497: def checkForLoops(path, atEnd, fromOutside) 498: # Check if we have been here before on this path. 499: if path.include?([ @property, atEnd ]) 500: error('loop_detected', "Loop detected at #{atEnd ? 'end' : 'start'} " + 501: "of task #{@property.fullId}", false) 502: skip = true 503: path.each do |t, e| 504: if t == @property && e == atEnd 505: skip = false 506: next 507: end 508: next if skip 509: info("loop_at_#{e ? 'end' : 'start'}", 510: "Loop ctnd. at #{e ? 'end' : 'start'} of task #{t.fullId}", t) 511: end 512: error('loop_end', "Aborting") 513: end 514: # Used for debugging only 515: if false 516: pathText = '' 517: path.each do |t, e| 518: pathText += "#{t.fullId}(#{e ? 'end' : 'start'}) -> " 519: end 520: pathText += "#{@property.fullId}(#{atEnd ? 'end' : 'start'})" 521: puts pathText 522: end 523: return if @deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)] 524: path << [ @property, atEnd ] 525: 526: # To find loops we have to traverse the graph in a certain order. When we 527: # enter a task we can either come from outside or inside. The following 528: # graph explains these definitions: 529: # 530: # | / \ | 531: # outside v / \ v outside 532: # +------------------------------+ 533: # | / Task \ | 534: # -->| o <--- ---> o |<-- 535: # |/ Start End \| 536: # /+------------------------------+\ 537: # / ^ ^ \ 538: # | inside | 539: # 540: # At the top we have the parent task. At the botton the child tasks. 541: # The horizontal arrors are start predecessors or end successors. 542: # As the graph is doubly-linked, we need to becareful to only find real 543: # loops. When coming from outside, we only continue to the inside and vice 544: # versa. Horizontal moves are only made when we are in a leaf task. 545: unless atEnd 546: if fromOutside 547: if @property.container? 548: # 549: # | 550: # v 551: # +-------- 552: # -->| o--+ 553: # +----|--- 554: # | 555: # V 556: # 557: @property.children.each do |child| 558: child.checkForLoops(@scenarioIdx, path, false, true) 559: end 560: else 561: # | 562: # v 563: # +-------- 564: # -->| o----> 565: # +-------- 566: # 567: checkForLoops(path, true, false) # if a('forward') 568: end 569: else 570: if a('startpreds').empty? 571: # 572: # ^ 573: # | 574: # +-|------ 575: # | o <-- 576: # +-------- 577: # ^ 578: # | 579: # 580: if @property.parent 581: @property.parent.checkForLoops(@scenarioIdx, path, false, false) 582: end 583: else 584: 585: # +-------- 586: # <---- o <-- 587: # +-------- 588: # ^ 589: # | 590: # 591: a('startpreds').each do |task, targetEnd| 592: task.checkForLoops(@scenarioIdx, path, targetEnd, true) 593: end 594: end 595: end 596: else 597: if fromOutside 598: if @property.container? 599: # 600: # | 601: # v 602: # --------+ 603: # +--o |<-- 604: # ---|----+ 605: # | 606: # v 607: # 608: @property.children.each do |child| 609: child.checkForLoops(@scenarioIdx, path, true, true) 610: end 611: else 612: # | 613: # v 614: # --------+ 615: # <----o |<-- 616: # --------+ 617: # 618: checkForLoops(path, false, false) # unless a('forward') 619: end 620: else 621: if a('endsuccs').empty? 622: # 623: # ^ 624: # | 625: # ------|-+ 626: # --> o | 627: # --------+ 628: # ^ 629: # | 630: # 631: if @property.parent 632: @property.parent.checkForLoops(@scenarioIdx, path, true, false) 633: end 634: else 635: # --------+ 636: # --> o----> 637: # --------+ 638: # ^ 639: # | 640: # 641: a('endsuccs').each do |task, targetEnd| 642: task.checkForLoops(@scenarioIdx, path, targetEnd, true) 643: end 644: end 645: end 646: end 647: 648: path.pop 649: @deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)] = true 650: # puts "Finished with #{@property.fullId} #{atEnd ? 'end' : 'start'} " + 651: # "#{fromOutside ? 'outside' : 'inside'}" 652: end
Return a list of intervals that lay within iv and are at least minDuration long and contain no working time.
# File lib/TaskScenario.rb, line 1332 1332: def collectTimeOffIntervals(iv, minDuration) 1333: if a('shifts') 1334: a('shifts').collectTimeOffIntervals(iv, minDuration) 1335: else 1336: [] 1337: end 1338: end
This function does some prep work for other functions like calcCriticalness. It compiles a list of all allocated leaf resources and stores it in @candidates. It also adds the allocated effort to the ‘alloctdeffort’ counter of each resource.
# File lib/TaskScenario.rb, line 672 672: def countResourceAllocations 673: return if @candidates.empty? || a('effort') <= 0 674: 675: avgEffort = a('effort') / @candidates.length 676: @candidates.each do |resource| 677: resource['alloctdeffort', @scenarioIdx] += avgEffort 678: end 679: end
Find the earliest possible start date for the task. This date must be after the end date of all the task that this task depends on. Dependencies may also require a minimum gap between the tasks.
# File lib/TaskScenario.rb, line 979 979: def earliestStart 980: # This is the date that we will return. 981: startDate = nil 982: a('depends').each do |dependency| 983: potentialStartDate = 984: dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 985: return nil if potentialStartDate.nil? 986: 987: # Determine the end date of a 'length' gap. 988: dateAfterLengthGap = potentialStartDate 989: gapLength = dependency.gapLength 990: while gapLength > 0 && dateAfterLengthGap < @project['end'] do 991: if @project.isWorkingTime(dateAfterLengthGap) 992: gapLength -= 1 993: end 994: dateAfterLengthGap += @project['scheduleGranularity'] 995: end 996: 997: # Determine the end date of a 'duration' gap. 998: if dateAfterLengthGap > potentialStartDate + dependency.gapDuration 999: potentialStartDate = dateAfterLengthGap 1000: else 1001: potentialStartDate += dependency.gapDuration 1002: end 1003: 1004: startDate = potentialStartDate if startDate.nil? || 1005: startDate < potentialStartDate 1006: end 1007: 1008: # If any of the parent tasks has an explicit start date, the task must 1009: # start at or after this date. 1010: task = @property 1011: while (task = task.parent) do 1012: if task['start', @scenarioIdx] && 1013: (startDate.nil? || task['start', @scenarioIdx] > startDate) 1014: startDate = task['start', @scenarioIdx] 1015: break 1016: end 1017: end 1018: 1019: # When the computed start date is after the already determined end date 1020: # of the task, the start dependencies were too weak. This happens when 1021: # task B depends on A and they are specified this way: 1022: # task A: | --> D- 1023: # task B: -D <-- | 1024: if a('end') && startDate > a('end') 1025: error('weak_start_dep', 1026: "Task #{@property.fullId} has a too weak start dependencies " + 1027: "to be scheduled properly.") 1028: end 1029: 1030: startDate 1031: end
When the actual scheduling process has been completed, this function must be called to do some more housekeeping. It computes some derived data based on the just scheduled values.
# File lib/TaskScenario.rb, line 326 326: def finishScheduling 327: calcCompletion 328: # This list is no longer needed, so let's save some memory. Set it to 329: # nil so we can detect accidental use. 330: @candidates = nil 331: end
Compute the total time resource or all resources are allocated during interval specified by startIdx and endIdx.
# File lib/TaskScenario.rb, line 1280 1280: def getAllocatedTime(startIdx, endIdx, resource = nil) 1281: return 0.0 if a('milestone') 1282: 1283: allocatedTime = 0.0 1284: if @property.container? 1285: @property.children.each do |task| 1286: allocatedTime += task.getAllocatedTime(@scenarioIdx, startIdx, endIdx, 1287: resource) 1288: end 1289: else 1290: if resource 1291: allocatedTime += resource.getAllocatedTime(@scenarioIdx, 1292: startIdx, endIdx, 1293: @property) 1294: else 1295: a('assignedresources').each do |r| 1296: allocatedTime += r.getAllocatedTime(@scenarioIdx, startIdx, endIdx, 1297: @property) 1298: end 1299: end 1300: end 1301: allocatedTime 1302: end
Compute the effective work a resource or all resources do during the interval specified by startIdx and endIdx. The effective work is the actual work multiplied by the efficiency of the resource.
# File lib/TaskScenario.rb, line 1307 1307: def getEffectiveWork(startIdx, endIdx, resource = nil) 1308: return 0.0 if a('milestone') 1309: 1310: workLoad = 0.0 1311: if @property.container? 1312: @property.children.each do |task| 1313: workLoad += task.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1314: resource) 1315: end 1316: else 1317: if resource 1318: workLoad += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1319: @property) 1320: else 1321: a('assignedresources').each do |r| 1322: workLoad += r.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1323: @property) 1324: end 1325: end 1326: end 1327: workLoad 1328: end
Return true of this Task has a dependency [ target, onEnd ] in the dependency category depType.
# File lib/TaskScenario.rb, line 127 127: def hasDependency?(depType, target, onEnd) 128: a(depType).include?([target, onEnd]) 129: end
Return true if the task has a effort, length or duration setting.
# File lib/TaskScenario.rb, line 972 972: def hasDurationSpec? 973: a('length') > 0 || a('duration') > 0 || a('effort') > 0 || a('milestone') 974: end
Returns true of the resource is assigned to this task or any of its children.
# File lib/TaskScenario.rb, line 1391 1391: def hasResourceAllocated?(interval, resource) 1392: if @property.leaf? 1393: return resource.allocated?(@scenarioIdx, interval, @property) 1394: else 1395: @property.children.each do |t| 1396: return true if t.hasResourceAllocated?(@scenarioIdx, interval, 1397: resource) 1398: end 1399: end 1400: false 1401: end
Check if the Task task depends on this task. depth specifies how many dependent task are traversed at max. A value of 0 means no limit. TODO: Change this to a non-recursive implementation.
# File lib/TaskScenario.rb, line 1343 1343: def isDependencyOf(task, depth) 1344: return true if task == @property 1345: 1346: # Check if any of the parent tasks is a dependency of _task_. 1347: t = @property.parent 1348: while t 1349: # If the parent is a dependency, than all childs are as well. 1350: return true if t.isDependencyOf(@scenarioIdx, task, depth) 1351: t = t.parent 1352: end 1353: 1354: 1355: a('startsuccs').each do |dep| 1356: unless dep[1] 1357: # must be a start->start dependency 1358: return true if dep[0].isDependencyOf(@scenarioIdx, task, depth) 1359: end 1360: end 1361: 1362: return false if depth == 1 1363: 1364: a('endsuccs').each do |dep| 1365: unless dep[1] 1366: # must be an end->start dependency 1367: return true if dep[0].isDependencyOf(@scenarioIdx, task, depth - 1) 1368: end 1369: end 1370: 1371: false 1372: end
If task or any of its sub-tasks depend on this task or any of its sub-tasks, we call this task a feature of task.
# File lib/TaskScenario.rb, line 1376 1376: def isFeatureOf(task) 1377: sources = @property.all 1378: destinations = task.all 1379: 1380: sources.each do |s| 1381: destinations.each do |d| 1382: return true if s.isDependencyOf(@scenarioIdx, d, 0) 1383: end 1384: end 1385: 1386: false 1387: end
Find the latest possible end date for the task. This date must be before the start date of all the task that this task precedes. Dependencies may also require a minimum gap between the tasks.
# File lib/TaskScenario.rb, line 1036 1036: def latestEnd 1037: # This is the date that we will return. 1038: endDate = nil 1039: a('precedes').each do |dependency| 1040: potentialEndDate = 1041: dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 1042: return nil if potentialEndDate.nil? 1043: 1044: # Determine the end date of a 'length' gap. 1045: dateBeforeLengthGap = potentialEndDate 1046: gapLength = dependency.gapLength 1047: while gapLength > 0 && dateBeforeLengthGap > @project['start'] do 1048: if @project.isWorkingTime(dateBeforeLengthGap - 1049: @project['scheduleGranularity']) 1050: gapLength -= 1 1051: end 1052: dateBeforeLengthGap -= @project['scheduleGranularity'] 1053: end 1054: 1055: # Determine the end date of a 'duration' gap. 1056: if dateBeforeLengthGap < potentialEndDate - dependency.gapDuration 1057: potentialEndDate = dateBeforeLengthGap 1058: else 1059: potentialEndDate -= dependency.gapDuration 1060: end 1061: 1062: endDate = potentialEndDate if endDate.nil? || endDate > potentialEndDate 1063: end 1064: 1065: # If any of the parent tasks has an explicit end date, the task must end 1066: # at or before this date. 1067: task = @property 1068: while (task = task.parent) do 1069: if task['end', @scenarioIdx] && 1070: (endDate.nil? || task['end', @scenarioIdx] < endDate) 1071: endDate = task['end', @scenarioIdx] 1072: break 1073: end 1074: end 1075: 1076: # When the computed end date is before the already determined start date 1077: # of the task, the end dependencies were too weak. This happens when 1078: # task A precedes B and they are specified this way: 1079: # task A: | --> D- 1080: # task B: -D <-- | 1081: if a('start') && endDate > a('start') 1082: error('weak_end_dep', 1083: "Task #{@property.fullId} has a too weak end dependencies " + 1084: "to be scheduled properly.") 1085: end 1086: 1087: endDate 1088: end
This function is not essential but does perform a large number of consistency checks. It should be called after the scheduling run has been finished.
# File lib/TaskScenario.rb, line 336 336: def postScheduleCheck 337: @errors = 0 338: @property.children.each do |task| 339: @errors += 1 unless task.postScheduleCheck(@scenarioIdx) 340: end 341: 342: # There is no point to check the parent if the child(s) have errors. 343: return false if @errors > 0 344: 345: # Same for runaway tasks. They have already been reported. 346: if @isRunAway 347: error('sched_runaway', "Some tasks did not fit into the project time " + 348: "frame.") 349: end 350: 351: # Make sure the task is marked complete 352: unless a('scheduled') 353: error('not_scheduled', 354: "Task #{@property.fullId} has not been marked as scheduled.") 355: end 356: 357: # If the task has a follower or predecessor that is a runaway this task 358: # is also incomplete. 359: (a('startsuccs') + a('endsuccs')).each do |task, onEnd| 360: return false if task.isRunAway(@scenarioIdx) 361: end 362: (a('startpreds') + a('endpreds')).each do |task, onEnd| 363: return false if task.isRunAway(@scenarioIdx) 364: end 365: 366: # Check if the start time is ok 367: if a('start').nil? 368: error('task_start_undef', 369: "Task #{@property.fullId} has undefined start time") 370: end 371: if a('start') < @project['start'] || a('start') > @project['end'] 372: error('task_start_range', 373: "The start time (#{a('start')}) of task #{@property.fullId} " + 374: "is outside the project interval (#{@project['start']} - " + 375: "#{@project['end']})") 376: end 377: if !a('minstart').nil? && a('start') < a('minstart') 378: warning('minstart', 379: "The start time (#{a('start')}) of task #{@property.fullId} " + 380: "is too early. Must be after #{a('minstart')}.") 381: end 382: if !a('maxstart').nil? && a('start') > a('maxstart') 383: warning('maxstart', 384: "The start time (#{a('start')}) of task #{@property.fullId} " + 385: "is too late. Must be before #{a('maxstart')}.") 386: end 387: # Check if the end time is ok 388: error('task_end_undef', 389: "Task #{@property.fullId} has undefined end time") if a('end').nil? 390: if a('end') < @project['start'] || a('end') > @project['end'] 391: error('task_end_range', 392: "The end time (#{a('end')}) of task #{@property.fullId} " + 393: "is outside the project interval (#{@project['start']} - " + 394: "#{@project['end']})") 395: end 396: if !a('minend').nil? && a('end') < a('minend') 397: warning('minend', 398: "The end time (#{a('end')}) of task #{@property.fullId} " + 399: "is too early. Must be after #{a('minend')}.") 400: end 401: if !a('maxend').nil? && a('end') > a('maxend') 402: warning('maxend', 403: "The end time (#{a('end')}) of task #{@property.fullId} " + 404: "is too late. Must be before #{a('maxend')}.") 405: end 406: # Make sure the start is before the end 407: if a('start') > a('end') 408: error('start_after_end', 409: "The start time (#{a('start')}) of task #{@property.fullId} " + 410: "is after the end time (#{a('end')}).") 411: end 412: 413: 414: # Check that tasks fits into parent task. 415: unless (parent = @property.parent).nil? || 416: parent['start', @scenarioIdx].nil? || 417: parent['end', @scenarioIdx].nil? 418: if a('start') < parent['start', @scenarioIdx] 419: error('task_start_in_parent', 420: "The start date (#{a('start')}) of task #{@property.fullId} " + 421: "is before the start date of the enclosing task " + 422: "#{parent['start', @scenarioIdx]}. ") 423: end 424: if a('end') > parent['end', @scenarioIdx] 425: error('task_end_in_parent', 426: "The end date (#{a('end')}) of task #{@property.fullId} " + 427: "is after the end date of the enclosing task " + 428: "#{parent['end', @scenarioIdx]}. ") 429: end 430: end 431: 432: # Check that all preceding tasks start/end before this task. 433: @property['depends', @scenarioIdx].each do |dependency| 434: task = dependency.task 435: limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 436: next if limit.nil? 437: if limit > a('start') 438: error('task_pred_before', 439: "Task #{@property.fullId} must start after " + 440: "#{dependency.onEnd ? 'end' : 'start'} of task " + 441: "#{task.fullId}.") 442: end 443: end 444: 445: # Check that all following tasks end before this task 446: @property['precedes', @scenarioIdx].each do |dependency| 447: task = dependency.task 448: limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 449: next if limit.nil? 450: if limit < a('end') 451: error('task_succ_after', 452: "Task #{@property.fullId} must end before " + 453: "#{dependency.onEnd ? 'end' : 'start'} of task #{task.fullId}.") 454: end 455: end 456: 457: if a('milestone') && a('start') != a('end') 458: error('milestone_times_equal', 459: "Milestone #{@property.fullId} must have identical start and " + 460: "end date.") 461: end 462: 463: if a('fail') || a('warn') 464: queryAttrs = { 'project' => @project, 465: 'scenarioIdx' => @scenarioIdx, 466: 'property' => @property, 467: 'scopeProperty' => nil, 468: 'start' => @project['start'], 469: 'end' => @project['end'], 470: 'loadUnit' => :days, 471: 'numberFormat' => @project['numberFormat'], 472: 'timeFormat' => @project['timeFormat'], 473: 'currencyFormat' => @project['currencyFormat'] } 474: query = Query.new(queryAttrs) 475: if a('fail') && a('fail').eval(query) 476: error('task_fail_check', 477: "User defined check failed for task #{@property.fullId} \n" + 478: "Condition: #{a('fail').to_s}\n" + 479: "Result: #{a('fail').to_s(query)}") 480: end 481: if a('warn') && a('warn').eval(query) 482: warning('task_warn_check', 483: "User defined warning triggered for task " + 484: "#{@property.fullId} \n" + 485: "Condition: #{a('warn').to_s}\n" + 486: "Result: #{a('warn').to_s(query)}") 487: end 488: end 489: 490: @errors == 0 491: end
Before the actual scheduling work can be started, we need to do a few consistency checks on the task.
# File lib/TaskScenario.rb, line 153 153: def preScheduleCheck 154: # Accounts can have sub accounts added after being used in a chargetset. 155: # So we need to re-test here. 156: a('chargeset').each do |chargeset| 157: chargeset.each do |account, share| 158: unless account.leaf? 159: error('account_no_leaf', 160: "Chargesets may not include group account #{account.fullId}.") 161: end 162: end 163: end 164: 165: # Leaf tasks can be turned into containers after bookings have been added. 166: # We need to check for this. 167: unless @property.leaf? || a('booking').empty? 168: error('container_booking', 169: "Container task #{@property.fullId} may not have bookings.") 170: end 171: 172: # Milestones may not have bookings. 173: if a('milestone') && !a('booking').empty? 174: error('milestone_booking', 175: "Milestone #{@property.fullId} may not have bookings.") 176: end 177: 178: # All 'scheduled' tasks must have a fixed start and end date. 179: if a('scheduled') && (a('start').nil? || a('end').nil?) 180: error('not_scheduled', 181: "Task #{@property.fullId} is marked as scheduled but does not " + 182: 'have a fixed start and end date.') 183: end 184: 185: # If an effort has been specified resources must be allocated as well. 186: if a('effort') > 0 && a('allocate').empty? 187: error('effort_no_allocations', 188: "Task #{@property.fullId} has an effort but no allocations.") 189: end 190: 191: durationSpecs = 0 192: durationSpecs += 1 if a('effort') > 0 193: durationSpecs += 1 if a('length') > 0 194: durationSpecs += 1 if a('duration') > 0 195: durationSpecs += 1 if a('milestone') 196: 197: # The rest of this function performs a number of plausibility tests with 198: # regards to task start and end critiria. To explain the various cases, 199: # the following symbols are used: 200: # 201: # |: fixed start or end date 202: # -: no fixed start or end date 203: # M: Milestone 204: # D: start or end dependency 205: # x->: ASAP task with duration criteria 206: # <-x: ALAP task with duration criteria 207: # -->: ASAP task without duration criteria 208: # <--: ALAP task without duration criteria 209: 210: if @property.container? 211: if durationSpecs > 0 212: error('container_duration', 213: "Container task #{@property.fullId} may not have a duration " + 214: "or be marked as milestones.") 215: end 216: elsif a('milestone') 217: if durationSpecs > 1 218: error('milestone_duration', 219: "Milestone task #{@property.fullId} may not have a duration.") 220: end 221: # Milestones can have the following cases: 222: # 223: # | M - ok |D M - ok - M - err1 -D M - ok 224: # | M | err2 |D M | err2 - M | ok -D M | ok 225: # | M -D ok |D M -D ok - M -D ok -D M -D ok 226: # | M |D err2 |D M |D err2 - M |D ok -D M |D ok 227: 228: # err1: no start and end 229: # already handled by 'start_undetermed' or 'end_undetermed' 230: 231: # err2: differnt start and end dates 232: if a('start') && a('end') && a('start') != a('end') 233: error('milestone_start_end', 234: "Start (#{a('start')}) and end (#{a('end')}) dates of " + 235: "milestone task #{@property.fullId} must be identical.") 236: end 237: else 238: # Error table for non-container, non-milestone tasks: 239: # AMP: Automatic milestone promotion for underspecified tasks when 240: # no bookings or allocations are present. 241: # AMPi: Automatic milestone promotion when no bookings or 242: # allocations are present. When no bookings but allocations are 243: # present the task inherits start and end date. 244: # Ref. implicitXref()| 245: # inhS: Inherit start date from parent task or project 246: # inhE: Inherit end date from parent task or project 247: # 248: # | x-> - ok |D x-> - ok - x-> - inhS -D x-> - ok 249: # | x-> | err1 |D x-> | err1 - x-> | inhS -D x-> | err1 250: # | x-> -D ok |D x-> -D ok - x-> -D inhS -D x-> -D ok 251: # | x-> |D err1 |D x-> |D err1 - x-> |D inhS -D x-> |D err1 252: # | --> - AMP |D --> - AMP - --> - AMPi -D --> - AMP 253: # | --> | ok |D --> | ok - --> | inhS -D --> | ok 254: # | --> -D ok |D --> -D ok - --> -D inhS -D --> -D ok 255: # | --> |D ok |D --> |D ok - --> |D inhS -D --> |D ok 256: # | <-x - inhE |D <-x - inhE - <-x - inhE -D <-x - inhE 257: # | <-x | err1 |D <-x | err1 - <-x | ok -D <-x | ok 258: # | <-x -D err1 |D <-x -D err1 - <-x -D ok -D <-x -D ok 259: # | <-x |D err1 |D <-x |D err1 - <-x |D ok -D <-x |D ok 260: # | <-- - inhE |D <-- - inhE - <-- - AMP -D <-- - inhE 261: # | <-- | ok |D <-- | ok - <-- | AMP -D <-- | ok 262: # | <-- -D ok |D <-- -D ok - <-- -D AMP -D <-- -D ok 263: # | <-- |D ok |D <-- |D ok - <-- |D AMP -D <-- |D ok 264: 265: # These cases are normally autopromoted to milestones or inherit their 266: # start or end dates. But this only works for tasks that have no 267: # allocations or bookings. 268: # - --> - 269: # | --> - 270: # |D --> - 271: # -D --> - 272: # - <-- - 273: # - <-- | 274: # - <-- -D 275: # - <-- |D 276: if durationSpecs == 0 && 277: ((a('forward') && a('end').nil? && !hasDependencies(true)) || 278: (!a('forward') && a('start').nil? && !hasDependencies(false))) 279: error('task_underspecified', 280: "Task #{@property.fullId} has too few specifations to be " + 281: "scheduled.") 282: end 283: 284: # err1: Overspecified (12 cases) 285: # | x-> | 286: # | <-x | 287: # | x-> |D 288: # | <-x |D 289: # |D x-> | 290: # |D <-x | 291: # |D <-x |D 292: # |D x-> |D 293: # -D x-> | 294: # -D x-> |D 295: # |D <-x -D 296: # | <-x -D 297: if durationSpecs > 1 298: error('multiple_durations', 299: "Tasks may only have either a duration, length or effort or " + 300: "be a milestone.") 301: end 302: startSpeced = @property.provided('start', @scenarioIdx) 303: endSpeced = @property.provided('end', @scenarioIdx) 304: if ((startSpeced && endSpeced) || 305: (hasDependencies(false) && a('forward') && endSpeced) || 306: (hasDependencies(true) && !a('forward') && startSpeced)) && 307: durationSpecs > 0 && !@property.provided('scheduled', @scenarioIdx) 308: error('task_overspecified', 309: "Task #{@property.fullId} has a start, an end and a " + 310: 'duration specification.') 311: end 312: end 313: 314: if !a('booking').empty? && !a('forward') && !a('scheduled') 315: error('alap_booking', 316: 'A task scheduled in ALAP mode may only have bookings if it ' + 317: 'has been marked as fully scheduled. Keep in mind that ' + 318: 'certain attributes like \end\ or \precedes\ automatically ' + 319: 'switch the task to ALAP mode.') 320: end 321: end
Call this function to reset all scheduling related data prior to scheduling.
# File lib/TaskScenario.rb, line 31 31: def prepareScheduling 32: @property['startpreds', @scenarioIdx] = [] 33: @property['startsuccs', @scenarioIdx] =[] 34: @property['endpreds', @scenarioIdx] = [] 35: @property['endsuccs', @scenarioIdx] = [] 36: 37: @isRunAway = false 38: 39: # The following variables are only used during scheduling 40: @lastSlot = nil 41: # The 'done' variables count scheduled values in number of time slots. 42: @doneDuration = 0 43: @doneLength = 0 44: # Due to the 'efficiency' factor the effort slots must be a float. 45: @doneEffort = 0.0 46: 47: @startIsDetermed = nil 48: @endIsDetermed = nil 49: @tentativeStart = @tentativeEnd = nil 50: 51: # To avoid multiple calls to propagateDate() we use these flags to know 52: # when we've done it already. 53: @startPropagated = false 54: @endPropagated = false 55: 56: # Inheriting start or end values is a bit tricky. This should really only 57: # happen if the task is a leaf task and scheduled away from the specified 58: # date. Since the default meachanism inherites all values, we have to 59: # delete the wrong ones again. 60: unless @property.provided('start', @scenarioIdx) 61: @property['start', @scenarioIdx] = nil 62: end 63: unless @property.provided('end', @scenarioIdx) 64: @property['end', @scenarioIdx] = nil 65: end 66: 67: # Milestones may only have start or end date even when the 'scheduled' 68: # attribute is set. For further processing, we need to add the missing 69: # date. 70: if a('milestone') && a('scheduled') 71: @property['end', @scenarioIdx] = a('start') if a('start') && !a('end') 72: @property['start', @scenarioIdx] = a('end') if !a('start') && a('end') 73: end 74: 75: # Collect the limits of this task and all parent tasks into a single 76: # Array. 77: @limits = [] 78: task = @property 79: # Reset the counters of all limits of this task. 80: task['limits', @scenarioIdx].reset if task['limits', @scenarioIdx] 81: until task.nil? 82: if task['limits', @scenarioIdx] 83: @limits << task['limits', @scenarioIdx] 84: end 85: task = task.parent 86: end 87: 88: # Collect the mandatory allocations. 89: @mandatories = [] 90: a('allocate').each do |allocation| 91: @mandatories << allocation if allocation.mandatory 92: end 93: 94: bookBookings 95: markMilestone 96: end
Set a new start or end date and propagate the value to all other task ends that have a direct dependency to this end of the task.
# File lib/TaskScenario.rb, line 822 822: def propagateDate(date, atEnd) 823: thisEnd = atEnd ? 'end' : 'start' 824: otherEnd = atEnd ? 'start' : 'end' 825: 826: # These flags are just used to avoid duplicate calls of this function 827: # during propagateInitialValues(). 828: if atEnd 829: @endPropagated = true 830: else 831: @startPropagated = true 832: end 833: 834: # For leaf tasks, propagate start may set the date. Container task dates 835: # are only set in scheduleContainer(). 836: @property[thisEnd, @scenarioIdx] = date if @property.leaf? 837: 838: if a('milestone') 839: # Start and end date of a milestone are identical. 840: @property['scheduled', @scenarioIdx] = true 841: if a(otherEnd).nil? 842: propagateDate(a(thisEnd), !atEnd) 843: end 844: Log << "Milestone #{@property.fullId}: #{a('start')} -> #{a('end')}" 845: elsif !a('scheduled') && a('start') && a('end') && 846: !(a('length') == 0 && a('duration') == 0 && a('effort') == 0 && 847: !a('allocate').empty?) 848: @property['scheduled', @scenarioIdx] = true 849: end 850: 851: # Propagate date to all dependent tasks. 852: a(thisEnd + 'preds').each do |task, onEnd| 853: propagateDateToDep(task, onEnd) 854: end 855: a(thisEnd + 'succs').each do |task, onEnd| 856: propagateDateToDep(task, onEnd) 857: end 858: 859: # Propagate date to sub tasks which have only an implicit 860: # dependency on the parent task and no other criteria for this end of 861: # the task. 862: @property.children.each do |task| 863: if task.canInheritDate?(@scenarioIdx, atEnd) 864: task.propagateDate(@scenarioIdx, date, atEnd) 865: end 866: end 867: 868: # The date propagation might have completed the date set of the enclosing 869: # containter task. If so, we can schedule it as well. 870: @property.parent.scheduleContainer(@scenarioIdx) if !@property.parent.nil? 871: end
# File lib/TaskScenario.rb, line 131 131: def propagateInitialValues 132: unless @startPropagated 133: if a('start') 134: propagateDate(a('start'), false) 135: elsif @property.parent.nil? && 136: @property.canInheritDate?(@scenarioIdx, false) 137: propagateDate(@project['start'], false) 138: end 139: end 140: 141: unless @endPropagated 142: if a('end') 143: propagateDate(a('end'), true) 144: elsif @property.parent.nil? && 145: @property.canInheritDate?(@scenarioIdx, true) 146: propagateDate(@project['end'], true) 147: end 148: end 149: end
# File lib/TaskScenario.rb, line 1100 1100: def query_complete(query) 1101: if @property.leaf? 1102: query.sortable = query.numerical = complete = a('complete').to_i 1103: query.string = "#{complete}%" 1104: else 1105: query.string = '' 1106: end 1107: end
Compute the cost generated by this Task for a given Account during a given interval. If a Resource is provided as scopeProperty only the cost directly generated by the resource is taken into account.
# File lib/TaskScenario.rb, line 1112 1112: def query_cost(query) 1113: if query.costAccount 1114: query.sortable = query.numerical = cost = 1115: turnover(query.startIdx, query.endIdx, query.costAccount, 1116: query.scopeProperty) 1117: query.string = query.currencyFormat.format(cost) 1118: else 1119: query.string = 'No cost account' 1120: end 1121: end
The duration of the task. After scheduling, it can be determined for all tasks. Also for those who did not have a ‘duration’ attribute.
# File lib/TaskScenario.rb, line 1125 1125: def query_duration(query) 1126: query.sortable = query.numerical = duration = 1127: (a('end') - a('start')) / (60 * 60 * 24) 1128: query.string = query.scaleDuration(duration) 1129: end
The effort allocated for the task in the specified interval. In case a Resource is given as scope property only the effort allocated for this resource is taken into account.
# File lib/TaskScenario.rb, line 1161 1161: def query_effort(query) 1162: query.sortable = query.numerical = work = 1163: getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty) 1164: query.string = query.scaleLoad(work) 1165: end
The completed (as of ‘now’) effort allocated for the task in the specified interval. In case a Resource is given as scope property only the effort allocated for this resource is taken into account.
# File lib/TaskScenario.rb, line 1134 1134: def query_effortdone(query) 1135: # For this query, we always override the query period. 1136: query.sortable = query.numerical = effort = 1137: getEffectiveWork(@project.dateToIdx(@project['start']), 1138: @project.dateToIdx(@project['now']), 1139: query.scopeProperty) 1140: query.string = query.scaleLoad(effort) 1141: end
The remaining (as of ‘now’) effort allocated for the task in the specified interval. In case a Resource is given as scope property only the effort allocated for this resource is taken into account.
# File lib/TaskScenario.rb, line 1147 1147: def query_effortleft(query) 1148: # For this query, we always override the query period. 1149: query.start = @project['now'] 1150: query.end = @project['end'] 1151: query.sortable = query.numerical = effort = 1152: getEffectiveWork(@project.dateToIdx(@project['now']), 1153: @project.dateToIdx(@project['end']), 1154: query.scopeProperty) 1155: query.string = query.scaleLoad(effort) 1156: end
# File lib/TaskScenario.rb, line 1167 1167: def query_followers(query) 1168: str = '' 1169: 1170: # First gather the task that depend on the start of this task. 1171: a('startsuccs').each do |task, onEnd| 1172: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1173: if onEnd 1174: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1175: str += "[->] #{taskEnd}" 1176: else 1177: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1178: str += "[->[ #{taskStart}" 1179: end 1180: str += "\n" 1181: end 1182: # Than add the tasks that depend on the end of this task. 1183: a('endsuccs').each do |task, onEnd| 1184: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1185: if onEnd 1186: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1187: str += "]->] #{taskEnd}" 1188: else 1189: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1190: str += "]->[ #{taskStart}" 1191: end 1192: str += "\n" 1193: end 1194: 1195: rText = RichText.new(str) 1196: query.rti = rText.generateIntermediateFormat 1197: end
# File lib/TaskScenario.rb, line 1199 1199: def query_precursors(query) 1200: str = '' 1201: 1202: # First gather the task that depend on the start of this task. 1203: a('startpreds').each do |task, onEnd| 1204: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1205: if onEnd 1206: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1207: str += "]->] #{taskEnd}" 1208: else 1209: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1210: str += "[->[ #{taskStart}" 1211: end 1212: str += "\n" 1213: end 1214: # Than add the tasks that depend on the end of this task. 1215: a('endpreds').each do |task, onEnd| 1216: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1217: if onEnd 1218: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1219: str += "[->] #{taskEnd}" 1220: else 1221: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1222: str += "]->[ #{taskStart}" 1223: end 1224: str += "\n" 1225: end 1226: 1227: rText = RichText.new(str) 1228: query.rti = rText.generateIntermediateFormat 1229: end
A list of the resources that have been allocated to work on the task in the report time frame.
# File lib/TaskScenario.rb, line 1233 1233: def query_resources(query) 1234: list = '' 1235: a('assignedresources').each do |resource| 1236: if getAllocatedTime(query.startIdx, query.endIdx, resource) > 0.0 1237: list += ', ' unless list.empty? 1238: list += "#{resource.name} (#{resource.fullId})" 1239: end 1240: end 1241: query.sortable = query.string = list 1242: rText = RichText.new(list) 1243: query.rti = rText.generateIntermediateFormat 1244: end
Compute the revenue generated by this Task for a given Account during a given interval. If a Resource is provided as scopeProperty only the revenue directly generated by the resource is taken into account.
# File lib/TaskScenario.rb, line 1249 1249: def query_revenue(query) 1250: if query.revenueAccount 1251: query.sortable = query.numerical = revenue = 1252: turnover(query.startIdx, query.endIdx, query.revenueAccount, 1253: query.scopeProperty) 1254: query.string = query.currencyFormat.format(revenue) 1255: else 1256: query.string = 'No revenue account' 1257: end 1258: end
# File lib/TaskScenario.rb, line 1260 1260: def query_targets(query) 1261: targetList = PropertyList.new(@project.tasks, false) 1262: targets(targetList) 1263: targetList.delete(@property) 1264: targetList.setSorting([['start', true, @scenarioIdx], 1265: ['seqno', true, 1 ]]) 1266: targetList.sort! 1267: 1268: res = '' 1269: targetList.each do |task| 1270: date = task['start', @scenarioIdx].to_s(@property.project['timeFormat']) 1271: res += "# #{task.name} (#{task.fullId}) #{date}\n" 1272: end 1273: rText = RichText.new(res) 1274: query.rti = rText.generateIntermediateFormat 1275: end
Check if the task is ready to be scheduled. For this it needs to have at least one specified end date and a duration criteria or the other end date.
# File lib/TaskScenario.rb, line 772 772: def readyForScheduling? 773: return false if a('scheduled') || @isRunAway 774: 775: if a('forward') 776: return true if a('start') && (hasDurationSpec? || a('end')) 777: else 778: return true if a('end') && (hasDurationSpec? || a('start')) 779: end 780: 781: false 782: end
# File lib/TaskScenario.rb, line 493 493: def resetLoopFlags 494: @deadEndFlags = Array.new(4, false) 495: end
This function is the entry point for the core scheduling algorithm. It schedules the task to completion. The function returns true if a start or end date has been determined and other tasks may be ready for scheduling now.
# File lib/TaskScenario.rb, line 788 788: def schedule 789: # Is the task scheduled from start to end or vice versa? 790: forward = a('forward') 791: # The task may not excede the project interval. 792: limit = @project[forward ? 'end' : 'start'] 793: # Number of seconds per time slot. 794: slotDuration = @project['scheduleGranularity'] 795: slot = nextSlot(slotDuration) 796: 797: # Schedule all time slots from slot in the scheduling direction until 798: # the task is completed or a problem has been found. 799: while !scheduleSlot(slot, slotDuration) 800: if forward 801: # The task is scheduled from start to end. 802: slot += slotDuration 803: if slot > limit 804: markAsRunaway 805: return false 806: end 807: else 808: # The task is scheduled from end to start. 809: slot -= slotDuration 810: if slot < limit 811: markAsRunaway 812: return false 813: end 814: end 815: end 816: 817: true 818: end
Find the smallest possible interval that encloses all child tasks. Abort the operation if any of the child tasks are not yet scheduled.
# File lib/TaskScenario.rb, line 931 931: def scheduleContainer 932: return if a('scheduled') || !@property.container? 933: 934: nStart = nil 935: nEnd = nil 936: 937: @property.children.each do |task| 938: # Abort if a child has not yet been scheduled. 939: return unless task['scheduled', @scenarioIdx] 940: 941: if nStart.nil? || task['start', @scenarioIdx] < nStart 942: nStart = task['start', @scenarioIdx] 943: end 944: if nEnd.nil? || task['end', @scenarioIdx] > nEnd 945: nEnd = task['end', @scenarioIdx] 946: end 947: end 948: 949: startSet = endSet = false 950: # Propagate the dates to other dependent tasks. 951: if a('start').nil? || a('start') > nStart 952: @property['start', @scenarioIdx] = nStart 953: startSet = true 954: end 955: if a('end').nil? || a('end') < nEnd 956: @property['end', @scenarioIdx] = nEnd 957: endSet = true 958: end 959: unless a('start') && a('end') 960: raise "Start (#{a('start')}) and end (#{a('end')}) must be set" 961: end 962: @property['scheduled', @scenarioIdx] = true 963: Log << "Container task #{@property.fullId}: #{a('start')} -> #{a('end')}" 964: 965: # If we have modified the start or end date, we need to communicate this 966: # new date to surrounding tasks. 967: propagateDate(nStart, false) if startSet 968: propagateDate(nEnd, true) if endSet 969: end
Register the user provided bookings with the Resource scoreboards. A booking describes the assignment of a Resource to a certain Task for a specified Interval.
# File lib/TaskScenario.rb, line 1647 1647: def bookBookings 1648: scheduled = a('scheduled') 1649: a('booking').each do |booking| 1650: unless booking.resource.leaf? 1651: error('booking_resource_not_leaf', 1652: "Booked resources may not be group resources", true, 1653: booking.sourceFileInfo) 1654: end 1655: unless a('forward') || scheduled 1656: error('booking_forward_only', 1657: "Only forward scheduled tasks may have booking statements.") 1658: end 1659: slotDuration = @project['scheduleGranularity'] 1660: booking.intervals.each do |interval| 1661: startIdx = @project.dateToIdx(interval.start) 1662: date = interval.start 1663: endIdx = @project.dateToIdx(interval.end) 1664: tEnd = nil 1665: startIdx.upto(endIdx - 1) do |idx| 1666: tEnd = date + slotDuration 1667: if booking.resource.bookBooking(@scenarioIdx, idx, booking) 1668: # Booking was successful for this time slot. 1669: @doneEffort += booking.resource['efficiency', @scenarioIdx] 1670: 1671: # Set start and lastSlot if appropriate. The task start will be 1672: # set to the begining of the first booked slot. The lastSlot 1673: # will be set to the last booked slot 1674: @lastSlot = date if @lastSlot.nil? || date > @lastSlot 1675: @tentativeEnd = tEnd if @tentativeEnd.nil? || 1676: @tentativeEnd < tEnd 1677: if !scheduled && (a('start').nil? || date < a('start')) 1678: @property['start', @scenarioIdx] = date 1679: end 1680: 1681: unless a('assignedresources').include?(booking.resource) 1682: @property['assignedresources', @scenarioIdx] << booking.resource 1683: end 1684: end 1685: if a('length') > 0 && @project.isWorkingTime(date, tEnd) 1686: # For tasks with a 'length' we track the covered work time and 1687: # set the task to 'scheduled' when we have enough length. 1688: @doneLength += 1 1689: if !scheduled && @doneLength >= a('length') 1690: @property['end', @scenarioIdx] = tEnd 1691: @property['scheduled', @scenarioIdx] = true 1692: end 1693: end 1694: date = tEnd 1695: end 1696: if a('duration') > 0 && @tentativeEnd 1697: @doneDuration = ((@tentativeEnd - a('start')) / 1698: @project['scheduleGranularity']).to_i 1699: if !scheduled && @doneDuration >= a('duration') 1700: @property['end', @scenarioIdx] = @tentativeEnd 1701: @property['scheduled', @scenarioIdx] = true 1702: end 1703: end 1704: end 1705: end 1706: end
# File lib/TaskScenario.rb, line 1593 1593: def bookResource(resource, sbIdx, date) 1594: booked = false 1595: resource.allLeaves.each do |r| 1596: # Prevent overbooking when multiple resources are allocated and 1597: # available. If the task has allocation limits we need to make sure 1598: # that none of them is already exceeded. 1599: break if a('effort') > 0 && @doneEffort >= a('effort') || 1600: !limitsOk?(date, resource) 1601: 1602: if r.book(@scenarioIdx, sbIdx, @property) 1603: 1604: if a('assignedresources').empty? 1605: if a('forward') 1606: @property['start', @scenarioIdx] = @project.idxToDate(sbIdx) 1607: else 1608: @property['end', @scenarioIdx] = @project.idxToDate(sbIdx + 1) 1609: end 1610: end 1611: 1612: @tentativeStart = @project.idxToDate(sbIdx) 1613: @tentativeEnd = @project.idxToDate(sbIdx + 1) 1614: 1615: @doneEffort += r['efficiency', @scenarioIdx] 1616: # Limits do not take efficiency into account. Limits are usage limits, 1617: # not effort limits. 1618: @limits.each do |limit| 1619: limit.inc(date, resource) 1620: end 1621: 1622: unless a('assignedresources').include?(r) 1623: @property['assignedresources', @scenarioIdx] << r 1624: end 1625: booked = true 1626: end 1627: end 1628: 1629: booked 1630: end
# File lib/TaskScenario.rb, line 1511 1511: def bookResources(date, slotDuration) 1512: # If there are no allocations defined, we can't do any bookings. 1513: # In projection mode we do not allow bookings prior to the current date 1514: # for any task (in strict mode) or tasks which have user specified 1515: # bookings (sloppy mode). 1516: if a('allocate').empty? || 1517: (@project.scenario(@scenarioIdx).get('projection') && 1518: date < @project['now'] && 1519: (@project.scenario(@scenarioIdx).get('strict') || 1520: a('assignedresources').empty?)) 1521: return 1522: end 1523: 1524: # If the task has shifts to limit the allocations, we check that we are 1525: # within a defined shift interval. If yes, we need to be on shift to 1526: # continue. 1527: if (shifts = a('shifts')) && shifts.assigned?(date) 1528: return if !shifts.onShift?(date) 1529: end 1530: 1531: # If the task has resource independent allocation limits we need to make 1532: # sure that none of them is already exceeded. 1533: return unless limitsOk?(date) 1534: 1535: sbIdx = @project.dateToIdx(date) 1536: 1537: # We first have to make sure that if there are mandatory resources 1538: # that these are all available for the time slot. 1539: takenMandatories = [] 1540: @mandatories.each do |allocation| 1541: return unless allocation.onShift?(date) 1542: 1543: # For mandatory allocations with alternatives at least one of the 1544: # alternatives must be available. 1545: found = false 1546: allocation.candidates(@scenarioIdx).each do |candidate| 1547: # When a resource group is marked mandatory, all members of the 1548: # group must be available. 1549: allAvailable = true 1550: candidate.allLeaves.each do |resource| 1551: if !limitsOk?(date, resource) || 1552: !resource.available?(@scenarioIdx, sbIdx) || 1553: takenMandatories.include?(resource) 1554: # We've found a mandatory resource that is not available for 1555: # the slot. 1556: allAvailable = false 1557: break 1558: else 1559: takenMandatories << resource 1560: end 1561: end 1562: if allAvailable 1563: found = true 1564: break 1565: end 1566: end 1567: 1568: # At least one mandatory resource is not available. We cannot continue. 1569: return unless found 1570: end 1571: 1572: iv = Interval.new(date, date + slotDuration) 1573: a('allocate').each do |allocation| 1574: next unless allocation.onShift?(date) 1575: 1576: # In case we have a persistent allocation we need to check if there is 1577: # already a locked resource and use it. 1578: if allocation.persistent && !allocation.lockedResource.nil? 1579: bookResource(allocation.lockedResource, sbIdx, date) 1580: else 1581: # If not, we create a list of candidates in the proper order and 1582: # assign the first one available. 1583: allocation.candidates(@scenarioIdx).each do |candidate| 1584: if bookResource(candidate, sbIdx, date) 1585: allocation.lockedResource = candidate 1586: break 1587: end 1588: end 1589: end 1590: end 1591: end
Calculate the current completion degree for tasks that have no user specified completion value.
# File lib/TaskScenario.rb, line 1865 1865: def calcCompletion 1866: # If the user provided a completion degree we are not touching it. 1867: if @property.provided('complete', @scenarioIdx) 1868: calcStatus 1869: return 1870: end 1871: 1872: if a('start').nil? || a('end').nil? 1873: @property['complete', @scenarioIdx] = 0.0 1874: @property['status', @scenarioIdx] = 'unknown' 1875: return 1876: end 1877: 1878: if a('milestone') 1879: @property['complete', @scenarioIdx] = 1880: @property['end', @scenarioIdx] <= @project['now'] ? 100.0 : 0.0 1881: @property['status', @scenarioIdx] = 1882: a('end') <= @project['now'] ? 'done' : 'not reached' 1883: else 1884: completion = 0.0 1885: if a('end') <= @project['now'] 1886: # The task has ended already. It's 100% complete. 1887: completion = 100.0 1888: elsif @project['now'] <= a('start') 1889: # The task has not started yet. Its' 0% complete. 1890: completion = 0.0 1891: else 1892: # The task is in progress. Calculate the current completion 1893: # degree. 1894: if @property.leaf? && a('effort') > 0 1895: # Effort based leaf tasks. The completion degree is the percantage 1896: # of effort that has been done already. 1897: done = getEffectiveWork(@project.dateToIdx(a('start')), 1898: @project.dateToIdx(@project['now'])) 1899: total = @project.convertToDailyLoad( 1900: a('effort') * @project['scheduleGranularity']) 1901: completion = done / total * 100.0 1902: else 1903: # Container tasks and length/duration leaf tasks. There is no way 1904: # we can compute the completion degree of a container task with a 1905: # mix of effort and duration task in a meaningful way. So, we 1906: # just go by duration. 1907: completion = ((@project['now'] - a('start')) / 1908: (a('end') - a('start'))) * 100.0 1909: end 1910: end 1911: @property['complete', @scenarioIdx] = completion 1912: calcStatus 1913: end 1914: end
This is a helper function for calcPathCriticalness(). It computes the larges criticalness of the pathes through the end-successors of this task and all its parent tasks.
# File lib/TaskScenario.rb, line 1841 1841: def calcPathCriticalnessEndSuccs 1842: maxCriticalness = 0.0 1843: # Gather a list of all end-successors of this task and its parent task. 1844: tList = [] 1845: p = @property 1846: while (p) 1847: p['endsuccs', @scenarioIdx].each do |task, onEnd| 1848: tList << [ task, onEnd ] unless tList.include?([ task, onEnd ]) 1849: end 1850: p = p.parent 1851: end 1852: 1853: tList.each do |task, onEnd| 1854: if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) > 1855: maxCriticalness 1856: maxCriticalness = criticalness 1857: end 1858: end 1859: 1860: maxCriticalness 1861: end
Calculate the status of the task based on the ‘complete’ attribute.
# File lib/TaskScenario.rb, line 1917 1917: def calcStatus 1918: @property['status', @scenarioIdx] = 1919: if a('complete') == 0.0 1920: 'not started' 1921: elsif a('complete') >= 100.0 1922: 'done' 1923: else 1924: 'in progress' 1925: end 1926: end
# File lib/TaskScenario.rb, line 1759 1759: def checkDependency(dependency, depType) 1760: if (depTask = dependency.resolve(@project)).nil? 1761: # Remove the broken dependency. It could cause trouble later on. 1762: @property[depType, @scenarioIdx].delete(dependency) 1763: error('task_depend_unknown', 1764: "Task #{@property.fullId} has unknown #{depType} " + 1765: "#{dependency.taskId}") 1766: end 1767: 1768: if depTask == @property 1769: # Remove the broken dependency. It could cause trouble later on. 1770: @property[depType, @scenarioIdx].delete(dependency) 1771: error('task_depend_self', "Task #{@property.fullId} cannot " + 1772: "depend on self") 1773: end 1774: 1775: if depTask.isChildOf?(@property) 1776: # Remove the broken dependency. It could cause trouble later on. 1777: @property[depType, @scenarioIdx].delete(dependency) 1778: error('task_depend_child', 1779: "Task #{@property.fullId} cannot depend on child " + 1780: "#{depTask.fullId}") 1781: end 1782: 1783: if @property.isChildOf?(depTask) 1784: # Remove the broken dependency. It could cause trouble later on. 1785: @property[depType, @scenarioIdx].delete(dependency) 1786: error('task_depend_parent', 1787: "Task #{@property.fullId} cannot depend on parent " + 1788: "#{depTask.fullId}") 1789: end 1790: 1791: @property[depType, @scenarioIdx].each do |dep| 1792: if dep.task == depTask && dep != dependency 1793: # Remove the broken dependency. It could cause trouble later on. 1794: @property[depType, @scenarioIdx].delete(dependency) 1795: error('task_depend_multi', 1796: "No need to specify dependency #{depTask.fullId} multiple " + 1797: "times for task #{@property.fullId}.") 1798: end 1799: end 1800: 1801: depTask 1802: end
This function checks if the task has a dependency on another task or fixed date for a certain end. If atEnd is true, the task end will be checked. Otherwise the start.
# File lib/TaskScenario.rb, line 1711 1711: def hasDependencies(atEnd) 1712: thisEnd = atEnd ? 'end' : 'start' 1713: !a(thisEnd + 'succs').empty? || !a(thisEnd + 'preds').empty? 1714: end
Return true if this task or any of its parent tasks has at least one sucessor task.
# File lib/TaskScenario.rb, line 1718 1718: def hasSuccessors 1719: t = @property 1720: while t 1721: return true unless t['endsuccs', @scenarioIdx].empty? 1722: t = t.parent 1723: end 1724: 1725: false 1726: end
Check if all of the task limits are not exceded at the given date. If a resource is provided, the limit for that particular resource is checked. If no resource is provided, only non-resource-specific limits are checked.
# File lib/TaskScenario.rb, line 1636 1636: def limitsOk?(date, resource = nil) 1637: @limits.each do |limit| 1638: return false if !limit.ok?(date, true, resource) 1639: end 1640: true 1641: end
# File lib/TaskScenario.rb, line 1728 1728: def markAsRunaway 1729: warning('runaway', "Task #{@property.fullId} does not fit into " + 1730: "project time frame") 1731: 1732: @isRunAway = true 1733: end
This function determines if a task is really a milestones and marks them accordingly.
# File lib/TaskScenario.rb, line 1737 1737: def markMilestone 1738: return if @property.container? || hasDurationSpec? || 1739: !a('booking').empty? || !a('allocate').empty? 1740: 1741: # The following cases qualify for an automatic milestone promotion. 1742: # - --> - 1743: # | --> - 1744: # |D --> - 1745: # -D --> - 1746: # - <-- - 1747: # - <-- | 1748: # - <-- -D 1749: # - <-- |D 1750: hasStartSpec = !a('start').nil? || !a('depends').empty? 1751: hasEndSpec = !a('end').nil? || !a('precedes').empty? 1752: 1753: @property['milestone', @scenarioIdx] = 1754: (hasStartSpec && a('forward') && !hasEndSpec) || 1755: (!hasStartSpec && !a('forward') && hasEndSpec) || 1756: (!hasStartSpec && !hasEndSpec) 1757: end
Return the date of the next slot this task wants to have scheduled. This must either be the first slot ever or it must be directly adjecent to the previous slot. If this task has not yet been scheduled at all, @lastSlot is still nil. Otherwise it contains the date of the last scheduled slot.
# File lib/TaskScenario.rb, line 1501 1501: def nextSlot(slotDuration) 1502: return nil if a('scheduled') || @isRunAway 1503: 1504: if a('forward') 1505: @lastSlot.nil? ? a('start') : @lastSlot + slotDuration 1506: else 1507: @lastSlot.nil? ? a('end') - slotDuration : @lastSlot - slotDuration 1508: end 1509: end
This function is called to propagate the start or end date of the current task to a dependend Task task. If atEnd is true, the date should be propagated to the end of the task, otherwise to the start.
# File lib/TaskScenario.rb, line 1813 1813: def propagateDateToDep(task, atEnd) 1814: #puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId}" 1815: # Don't propagate if the task is already completely scheduled or is a 1816: # container. 1817: return if task['scheduled', @scenarioIdx] || task.container? 1818: 1819: # Don't propagate if the task already has a date for that end. 1820: return unless task[atEnd ? 'end' : 'start', @scenarioIdx].nil? 1821: 1822: # Don't propagate if the task has a duration or is a milestone and the 1823: # task end to set is in the scheduling direction. 1824: return if task.hasDurationSpec?(@scenarioIdx) && 1825: !(atEnd ^ task['forward', @scenarioIdx]) 1826: 1827: # Check if all other dependencies for that task end have been determined 1828: # already and use the latest or earliest possible date. Don't propagate 1829: # if we don't have all dates yet. 1830: return if (nDate = (atEnd ? task.latestEnd(@scenarioIdx) : 1831: task.earliestStart(@scenarioIdx))).nil? 1832: 1833: # Looks like it is ok to propagate the date. 1834: task.propagateDate(@scenarioIdx, nDate, atEnd) 1835: # puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId} done" 1836: end
# File lib/TaskScenario.rb, line 1404 1404: def scheduleSlot(slot, slotDuration) 1405: # Tasks must always be scheduled in a single contigous fashion. @lastSlot 1406: # indicates the slot that was used for the previous call. Depending on the 1407: # scheduling direction the next slot must be scheduled either right before 1408: # or after this slot. If the current slot is not directly aligned, we'll 1409: # wait for another call with a proper slot. The function returns true 1410: # only if a slot could be scheduled. 1411: if a('forward') 1412: # On first call, the @lastSlot is not set yet. We set it to the slot 1413: # before the start slot. 1414: if @lastSlot.nil? 1415: @lastSlot = a('start') - slotDuration 1416: @tentativeEnd = slot + slotDuration 1417: end 1418: 1419: return false unless slot == @lastSlot + slotDuration 1420: else 1421: # On first call, the @lastSlot is not set yet. We set it to the slot 1422: # to the end slot. 1423: if @lastSlot.nil? 1424: @lastSlot = a('end') 1425: @tentativeStart = slot 1426: end 1427: 1428: return false unless slot == @lastSlot - slotDuration 1429: end 1430: @lastSlot = slot 1431: 1432: if a('length') > 0 || a('duration') > 0 1433: # The doneDuration counts the number of scheduled slots. It is increased 1434: # by one with every scheduled slot. The doneLength is only increased for 1435: # global working time slots. 1436: bookResources(slot, slotDuration) 1437: @doneDuration += 1 1438: if @project.isWorkingTime(slot, slot + slotDuration) 1439: @doneLength += 1 1440: end 1441: 1442: # If we have reached the specified duration or lengths, we set the end 1443: # or start date and propagate the value to neighbouring tasks. 1444: if (a('length') > 0 && @doneLength >= a('length')) || 1445: (a('duration') > 0 && @doneDuration >= a('duration')) 1446: if a('forward') 1447: propagateDate(slot + slotDuration, true) 1448: else 1449: propagateDate(slot, false) 1450: end 1451: return true 1452: end 1453: elsif a('effort') > 0 1454: bookResources(slot, slotDuration) if @doneEffort < a('effort') 1455: if @doneEffort >= a('effort') 1456: # The specified effort has been reached. The has been fully scheduled 1457: # now. 1458: if a('forward') 1459: propagateDate(@tentativeEnd, true) 1460: else 1461: propagateDate(@tentativeStart, false) 1462: end 1463: return true 1464: end 1465: elsif a('milestone') 1466: if a('forward') 1467: propagateDate(a('start'), true) 1468: else 1469: propagateDate(a('end'), false) 1470: end 1471: return true 1472: elsif a('start') && a('end') 1473: # Task with start and end date but no duration criteria 1474: if a('allocate').empty? 1475: # For start-end-tasks without allocation, we don't have to do 1476: # anything but to set the 'scheduled' flag. 1477: @property['scheduled', @scenarioIdx] = true 1478: @property.parent.scheduleContainer(@scenarioIdx) if @property.parent 1479: return true 1480: end 1481: 1482: bookResources(slot, slotDuration) 1483: 1484: # Depending on the scheduling direction we can mark the task as 1485: # scheduled once we have reached the other end. 1486: if (a('forward') && slot + slotDuration >= a('end')) || 1487: (!a('forward') && slot <= a('start')) 1488: @property['scheduled', @scenarioIdx] = true 1489: @property.parent.scheduleContainer(@scenarioIdx) if @property.parent 1490: return true 1491: end 1492: end 1493: 1494: false 1495: end
Set @startIsDetermed or @endIsDetermed (depending on _setStart) to value.
# File lib/TaskScenario.rb, line 1806 1806: def setDetermination(setStart, value) 1807: setStart ? @startIsDetermed = value : @endIsDetermed = value 1808: end
Recursively compile a list of Task properties which depend on the current task.
# File lib/TaskScenario.rb, line 1930 1930: def targets(list) 1931: # A target must be a leaf function that has no direct or indirect 1932: # (through parent) following tasks. 1933: if @property.leaf? && !hasSuccessors && !list.include?(@property) 1934: list << @property 1935: return 1936: end 1937: 1938: a('endsuccs').each do |t, onEnd| 1939: t.targets(@scenarioIdx, list) 1940: end 1941: 1942: # Check of indirect followers. 1943: @property.parent.targets(@scenarioIdx, list) if @property.parent 1944: end
Compute the turnover generated by this Task for a given Account account during the interval specified by startIdx and endIdx. These can either be TjTime values or Scoreboard indexes. If a Resource resource is given, only the turnover directly generated by the resource is taken into account.
# File lib/TaskScenario.rb, line 1951 1951: def turnover(startIdx, endIdx, account, resource = nil) 1952: amount = 0.0 1953: if @property.container? 1954: @property.children.each do |child| 1955: amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, 1956: resource) 1957: end 1958: end 1959: 1960: # If there are no chargeset defined for this task, we don't need to 1961: # compute the resource related or other cost. 1962: unless a('chargeset').empty? 1963: resourceCost = 0.0 1964: otherCost = 0.0 1965: 1966: # Container tasks don't have resource cost. 1967: unless @property.container? 1968: if resource 1969: resourceCost = resource.cost(@scenarioIdx, startIdx, endIdx, 1970: @property) 1971: else 1972: a('assignedresources').each do |r| 1973: resourceCost += r.cost(@scenarioIdx, startIdx, endIdx, @property) 1974: end 1975: end 1976: end 1977: 1978: unless a('charge').empty? 1979: # Add one-time and periodic charges to the amount. 1980: startDate = startIdx.is_a?(TjTime) ? startIdx : 1981: @project.idxToDate(startIdx) 1982: endDate = endIdx.is_a?(TjTime) ? endIdx : 1983: @project.idxToDate(endIdx) 1984: iv = Interval.new(startDate, endDate) 1985: a('charge').each do |charge| 1986: otherCost += charge.turnover(iv) 1987: end 1988: end 1989: 1990: totalCost = resourceCost + otherCost 1991: # Now weight the total cost by the share of the account 1992: a('chargeset').each do |set| 1993: set.each do |accnt, share| 1994: if share > 0.0 && (accnt == account || accnt.isChildOf?(account)) 1995: amount += totalCost * share 1996: end 1997: end 1998: end 1999: end 2000: 2001: amount 2002: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.