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 1084 1084: def addBooking(booking) 1085: if a('booking').empty? 1086: # For the first item use the assignment form so that the 'provided' 1087: # attribute is set properly. 1088: @property['booking', @scenarioIdx] = [ booking ] 1089: else 1090: @property['booking', @scenarioIdx] << booking 1091: end 1092: 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 872 872: def canInheritDate?(atEnd) 873: # Inheriting a start or end date from the enclosing task or the project 874: # is allowed for the following scenarios: 875: # - --> - inherit start and end when no bookings but allocations 876: # present 877: # - <-- - dito 878: # 879: # - x-> - inhS 880: # - x-> | inhS 881: # - x-> -D inhS 882: # - x-> |D inhS 883: # - --> | inhS 884: # - --> -D inhS 885: # - --> |D inhS 886: # - <-- | inhS 887: # | --> - inhE 888: # | <-x - inhE 889: # |D <-x - inhE 890: # - <-x - inhE 891: # -D <-x - inhE 892: # | <-- - inhE 893: # |D <-- - inhE 894: # -D <-- - inhE 895: # Return false if we already have a date or if we have a dependency for 896: # this end. 897: thisEnd = atEnd ? 'end' : 'start' 898: hasThisDeps = !a(thisEnd + 'preds').empty? || !a(thisEnd + 'succs').empty? 899: hasThisSpec = a(thisEnd) || hasThisDeps 900: return false if hasThisSpec 901: 902: # Containter task can inherit the date if they have no dependencies. 903: return true if @property.container? 904: 905: thatEnd = atEnd ? 'start' : 'end' 906: hasThatDeps = !a(thatEnd + 'preds').empty? || !a(thatEnd + 'succs').empty? 907: hasThatSpec = a(thatEnd) || hasThatDeps 908: 909: # Check for tasks that have no start and end spec, no duration spec but 910: # allocates. They can inherit the start and end date. 911: return true if hasThatSpec && !hasDurationSpec? && !a('allocate').empty? 912: 913: if a('forward') ^ atEnd 914: # the scheduling direction is pointing away from this end 915: return true if hasDurationSpec? || !a('booking').empty? 916: 917: return hasThatSpec 918: else 919: # the scheduling direction is pointing towards this end 920: return a(thatEnd) && !hasDurationSpec? && 921: a('booking').empty? #&& a('allocate').empty? 922: end 923: 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 1326 1326: def collectTimeOffIntervals(iv, minDuration) 1327: if a('shifts') 1328: a('shifts').collectTimeOffIntervals(iv, minDuration) 1329: else 1330: [] 1331: end 1332: 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 973 973: def earliestStart 974: # This is the date that we will return. 975: startDate = nil 976: a('depends').each do |dependency| 977: potentialStartDate = 978: dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 979: return nil if potentialStartDate.nil? 980: 981: # Determine the end date of a 'length' gap. 982: dateAfterLengthGap = potentialStartDate 983: gapLength = dependency.gapLength 984: while gapLength > 0 && dateAfterLengthGap < @project['end'] do 985: if @project.isWorkingTime(dateAfterLengthGap) 986: gapLength -= 1 987: end 988: dateAfterLengthGap += @project['scheduleGranularity'] 989: end 990: 991: # Determine the end date of a 'duration' gap. 992: if dateAfterLengthGap > potentialStartDate + dependency.gapDuration 993: potentialStartDate = dateAfterLengthGap 994: else 995: potentialStartDate += dependency.gapDuration 996: end 997: 998: startDate = potentialStartDate if startDate.nil? || 999: startDate < potentialStartDate 1000: end 1001: 1002: # If any of the parent tasks has an explicit start date, the task must 1003: # start at or after this date. 1004: task = @property 1005: while (task = task.parent) do 1006: if task['start', @scenarioIdx] && 1007: (startDate.nil? || task['start', @scenarioIdx] > startDate) 1008: startDate = task['start', @scenarioIdx] 1009: break 1010: end 1011: end 1012: 1013: # When the computed start date is after the already determined end date 1014: # of the task, the start dependencies were too weak. This happens when 1015: # task B depends on A and they are specified this way: 1016: # task A: | --> D- 1017: # task B: -D <-- | 1018: if a('end') && startDate > a('end') 1019: error('weak_start_dep', 1020: "Task #{@property.fullId} has a too weak start dependencies " + 1021: "to be scheduled properly.") 1022: end 1023: 1024: startDate 1025: 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 1274 1274: def getAllocatedTime(startIdx, endIdx, resource = nil) 1275: return 0.0 if a('milestone') 1276: 1277: allocatedTime = 0.0 1278: if @property.container? 1279: @property.children.each do |task| 1280: allocatedTime += task.getAllocatedTime(@scenarioIdx, startIdx, endIdx, 1281: resource) 1282: end 1283: else 1284: if resource 1285: allocatedTime += resource.getAllocatedTime(@scenarioIdx, 1286: startIdx, endIdx, 1287: @property) 1288: else 1289: a('assignedresources').each do |r| 1290: allocatedTime += r.getAllocatedTime(@scenarioIdx, startIdx, endIdx, 1291: @property) 1292: end 1293: end 1294: end 1295: allocatedTime 1296: 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 1301 1301: def getEffectiveWork(startIdx, endIdx, resource = nil) 1302: return 0.0 if a('milestone') 1303: 1304: workLoad = 0.0 1305: if @property.container? 1306: @property.children.each do |task| 1307: workLoad += task.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1308: resource) 1309: end 1310: else 1311: if resource 1312: workLoad += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1313: @property) 1314: else 1315: a('assignedresources').each do |r| 1316: workLoad += r.getEffectiveWork(@scenarioIdx, startIdx, endIdx, 1317: @property) 1318: end 1319: end 1320: end 1321: workLoad 1322: 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 966 966: def hasDurationSpec? 967: a('length') > 0 || a('duration') > 0 || a('effort') > 0 || a('milestone') 968: end
Returns true of the resource is assigned to this task or any of its children.
# File lib/TaskScenario.rb, line 1369 1369: def hasResourceAllocated?(interval, resource) 1370: if @property.leaf? 1371: return resource.allocated?(@scenarioIdx, interval, @property) 1372: else 1373: @property.children.each do |t| 1374: return true if t.hasResourceAllocated?(@scenarioIdx, interval, 1375: resource) 1376: end 1377: end 1378: false 1379: 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.
# File lib/TaskScenario.rb, line 1336 1336: def isDependencyOf(task, depth) 1337: return true if task == @property 1338: 1339: # Check if any of the parent tasks is a dependency of _task_. 1340: t = @property.parent 1341: while t 1342: # If the parent is a dependency, than all childs are as well. 1343: return true if t.isDependencyOf(@scenarioIdx, task, depth) 1344: t = t.parent 1345: end 1346: 1347: 1348: a('startsuccs').each do |dep| 1349: unless dep[1] 1350: # must be a start->start dependency 1351: return true if dep[0].isDependencyOf(@scenarioIdx, task, depth) 1352: end 1353: end 1354: 1355: return false if depth == 1 1356: 1357: a('endsuccs').each do |dep| 1358: unless dep[1] 1359: # must be an end->start dependency 1360: return true if dep[0].isDependencyOf(@scenarioIdx, task, depth - 1) 1361: end 1362: end 1363: 1364: false 1365: 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 1030 1030: def latestEnd 1031: # This is the date that we will return. 1032: endDate = nil 1033: a('precedes').each do |dependency| 1034: potentialEndDate = 1035: dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] 1036: return nil if potentialEndDate.nil? 1037: 1038: # Determine the end date of a 'length' gap. 1039: dateBeforeLengthGap = potentialEndDate 1040: gapLength = dependency.gapLength 1041: while gapLength > 0 && dateBeforeLengthGap > @project['start'] do 1042: if @project.isWorkingTime(dateBeforeLengthGap - 1043: @project['scheduleGranularity']) 1044: gapLength -= 1 1045: end 1046: dateBeforeLengthGap -= @project['scheduleGranularity'] 1047: end 1048: 1049: # Determine the end date of a 'duration' gap. 1050: if dateBeforeLengthGap < potentialEndDate - dependency.gapDuration 1051: potentialEndDate = dateBeforeLengthGap 1052: else 1053: potentialEndDate -= dependency.gapDuration 1054: end 1055: 1056: endDate = potentialEndDate if endDate.nil? || endDate > potentialEndDate 1057: end 1058: 1059: # If any of the parent tasks has an explicit end date, the task must end 1060: # at or before this date. 1061: task = @property 1062: while (task = task.parent) do 1063: if task['end', @scenarioIdx] && 1064: (endDate.nil? || task['end', @scenarioIdx] < endDate) 1065: endDate = task['end', @scenarioIdx] 1066: break 1067: end 1068: end 1069: 1070: # When the computed end date is before the already determined start date 1071: # of the task, the end dependencies were too weak. This happens when 1072: # task A precedes B and they are specified this way: 1073: # task A: | --> D- 1074: # task B: -D <-- | 1075: if a('start') && endDate > a('start') 1076: error('weak_end_dep', 1077: "Task #{@property.fullId} has a too weak end dependencies " + 1078: "to be scheduled properly.") 1079: end 1080: 1081: endDate 1082: 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 && !a('scheduled') 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: end 846: 847: # Propagate date to all dependent tasks. 848: a(thisEnd + 'preds').each do |task, onEnd| 849: propagateDateToDep(task, onEnd) 850: end 851: a(thisEnd + 'succs').each do |task, onEnd| 852: propagateDateToDep(task, onEnd) 853: end 854: 855: # Propagate date to sub tasks which have only an implicit 856: # dependency on the parent task and no other criteria for this end of 857: # the task. 858: @property.children.each do |task| 859: if task.canInheritDate?(@scenarioIdx, atEnd) 860: task.propagateDate(@scenarioIdx, date, atEnd) 861: end 862: end 863: 864: # The date propagation might have completed the date set of the enclosing 865: # containter task. If so, we can schedule it as well. 866: @property.parent.scheduleContainer(@scenarioIdx) if !@property.parent.nil? 867: 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 1094 1094: def query_complete(query) 1095: if @property.leaf? 1096: query.sortable = query.numerical = complete = a('complete').to_i 1097: query.string = "#{complete}%" 1098: else 1099: query.string = '' 1100: end 1101: 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 1106 1106: def query_cost(query) 1107: if query.costAccount 1108: query.sortable = query.numerical = cost = 1109: turnover(query.startIdx, query.endIdx, query.costAccount, 1110: query.scopeProperty) 1111: query.string = query.currencyFormat.format(cost) 1112: else 1113: query.string = 'No cost account' 1114: end 1115: 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 1119 1119: def query_duration(query) 1120: query.sortable = query.numerical = duration = 1121: (a('end') - a('start')) / (60 * 60 * 24) 1122: query.string = query.scaleDuration(duration) 1123: 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 1155 1155: def query_effort(query) 1156: query.sortable = query.numerical = work = 1157: getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty) 1158: query.string = query.scaleLoad(work) 1159: 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 1128 1128: def query_effortdone(query) 1129: # For this query, we always override the query period. 1130: query.sortable = query.numerical = effort = 1131: getEffectiveWork(@project.dateToIdx(@project['start']), 1132: @project.dateToIdx(@project['now']), 1133: query.scopeProperty) 1134: query.string = query.scaleLoad(effort) 1135: 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 1141 1141: def query_effortleft(query) 1142: # For this query, we always override the query period. 1143: query.start = @project['now'] 1144: query.end = @project['end'] 1145: query.sortable = query.numerical = effort = 1146: getEffectiveWork(@project.dateToIdx(@project['now']), 1147: @project.dateToIdx(@project['end']), 1148: query.scopeProperty) 1149: query.string = query.scaleLoad(effort) 1150: end
# File lib/TaskScenario.rb, line 1161 1161: def query_followers(query) 1162: str = '' 1163: 1164: # First gather the task that depend on the start of this task. 1165: a('startsuccs').each do |task, onEnd| 1166: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1167: if onEnd 1168: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1169: str += "[->[ #{taskEnd}" 1170: else 1171: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1172: str += "[->] #{taskStart}" 1173: end 1174: str += "\n" 1175: end 1176: # Than add the tasks that depend on the end of this task. 1177: a('endsuccs').each do |task, onEnd| 1178: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1179: if onEnd 1180: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1181: str += "]->[ #{taskEnd}" 1182: else 1183: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1184: str += "]->] #{taskStart}" 1185: end 1186: str += "\n" 1187: end 1188: 1189: rText = RichText.new(str) 1190: query.rti = rText.generateIntermediateFormat 1191: end
# File lib/TaskScenario.rb, line 1193 1193: def query_precursors(query) 1194: str = '' 1195: 1196: # First gather the task that depend on the start of this task. 1197: a('startpreds').each do |task, onEnd| 1198: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1199: if onEnd 1200: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1201: str += "]->[ #{taskEnd}" 1202: else 1203: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1204: str += "[->[ #{taskStart}" 1205: end 1206: str += "\n" 1207: end 1208: # Than add the tasks that depend on the end of this task. 1209: a('endpreds').each do |task, onEnd| 1210: str += "* <nowiki>#{task.name}</nowiki> (#{task.fullId}) " 1211: if onEnd 1212: taskEnd = task['end', query.scenarioIdx].to_s(query.timeFormat) 1213: str += "[->] #{taskEnd}" 1214: else 1215: taskStart = task['start', query.scenarioIdx].to_s(query.timeFormat) 1216: str += "]->] #{taskStart}" 1217: end 1218: str += "\n" 1219: end 1220: 1221: rText = RichText.new(str) 1222: query.rti = rText.generateIntermediateFormat 1223: 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 1227 1227: def query_resources(query) 1228: list = '' 1229: a('assignedresources').each do |resource| 1230: if getAllocatedTime(query.startIdx, query.endIdx, resource) > 0.0 1231: list += ', ' unless list.empty? 1232: list += "#{resource.name} (#{resource.fullId})" 1233: end 1234: end 1235: query.sortable = query.string = list 1236: rText = RichText.new(list) 1237: query.rti = rText.generateIntermediateFormat 1238: 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 1243 1243: def query_revenue(query) 1244: if query.revenueAccount 1245: query.sortable = query.numerical = revenue = 1246: turnover(query.startIdx, query.endIdx, query.revenueAccount, 1247: query.scopeProperty) 1248: query.string = query.currencyFormat.format(revenue) 1249: else 1250: query.string = 'No revenue account' 1251: end 1252: end
# File lib/TaskScenario.rb, line 1254 1254: def query_targets(query) 1255: targetList = PropertyList.new(@project.tasks, false) 1256: targets(targetList) 1257: targetList.delete(@property) 1258: targetList.setSorting([['start', true, @scenarioIdx], 1259: ['seqno', true, 1 ]]) 1260: targetList.sort! 1261: 1262: res = '' 1263: targetList.each do |task| 1264: date = task['start', @scenarioIdx].to_s(@property.project['timeFormat']) 1265: res += "# #{task.name} (#{task.fullId}) #{date}\n" 1266: end 1267: rText = RichText.new(res) 1268: query.rti = rText.generateIntermediateFormat 1269: 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 927 927: def scheduleContainer 928: return if a('scheduled') || !@property.container? 929: 930: nStart = nil 931: nEnd = nil 932: 933: @property.children.each do |task| 934: # Abort if a child has not yet been scheduled. 935: return unless task['scheduled', @scenarioIdx] 936: 937: if nStart.nil? || task['start', @scenarioIdx] < nStart 938: nStart = task['start', @scenarioIdx] 939: end 940: if nEnd.nil? || task['end', @scenarioIdx] > nEnd 941: nEnd = task['end', @scenarioIdx] 942: end 943: end 944: 945: @property['scheduled', @scenarioIdx] = true 946: 947: startSet = endSet = false 948: # Propagate the dates to other dependent tasks. 949: if a('start').nil? || a('start') > nStart 950: @property['start', @scenarioIdx] = nStart 951: startSet = true 952: end 953: if a('end').nil? || a('end') < nEnd 954: @property['end', @scenarioIdx] = nEnd 955: endSet = true 956: end 957: Log << "Container task #{@property.fullId}: #{a('start')} -> #{a('end')}" 958: 959: # If we have modified the start or end date, we need to communicate this 960: # new date to surrounding tasks. 961: propagateDate(nStart, false) if startSet 962: propagateDate(nEnd, true) if endSet 963: 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 1612 1612: def bookBookings 1613: scheduled = a('scheduled') 1614: a('booking').each do |booking| 1615: unless booking.resource.leaf? 1616: error('booking_resource_not_leaf', 1617: "Booked resources may not be group resources", true, 1618: booking.sourceFileInfo) 1619: end 1620: unless a('forward') || a('scheduled') 1621: error('booking_forward_only', 1622: "Only forward scheduled tasks may have booking statements.") 1623: end 1624: slotDuration = @project['scheduleGranularity'] 1625: booking.intervals.each do |interval| 1626: startIdx = @project.dateToIdx(interval.start) 1627: date = interval.start 1628: endIdx = @project.dateToIdx(interval.end) 1629: tEnd = nil 1630: startIdx.upto(endIdx - 1) do |idx| 1631: tEnd = date + slotDuration 1632: if booking.resource.bookBooking(@scenarioIdx, idx, booking) 1633: # Booking was successful for this time slot. 1634: @doneEffort += booking.resource['efficiency', @scenarioIdx] 1635: 1636: # Set start and lastSlot if appropriate. The task start will be 1637: # set to the begining of the first booked slot. The lastSlot 1638: # will be set to the last booked slot 1639: @lastSlot = date if @lastSlot.nil? || date > @lastSlot 1640: @tentativeEnd = tEnd if @tentativeEnd.nil? || 1641: @tentativeEnd < tEnd 1642: if !a('scheduled') && (a('start').nil? || date < a('start')) 1643: @property['start', @scenarioIdx] = date 1644: end 1645: 1646: unless a('assignedresources').include?(booking.resource) 1647: @property['assignedresources', @scenarioIdx] << booking.resource 1648: end 1649: end 1650: if a('length') > 0 && @project.isWorkingTime(date, tEnd) 1651: # For tasks with a 'length' we track the covered work time and 1652: # set the task to 'scheduled' when we have enough length. 1653: @doneLength += 1 1654: if !scheduled && @doneLength >= a('length') 1655: @property['end', @scenarioIdx] = tEnd 1656: @property['scheduled', @scenarioIdx] = true 1657: end 1658: end 1659: date = tEnd 1660: end 1661: if a('duration') > 0 && @tentativeEnd 1662: @doneDuration = ((@tentativeEnd - a('start')) / 1663: @project['scheduleGranularity']).to_i 1664: if !scheduled && @doneDuration >= a('duration') 1665: @property['end', @scenarioIdx] = @tentativeEnd 1666: @property['scheduled', @scenarioIdx] = true 1667: end 1668: end 1669: end 1670: end 1671: end
# File lib/TaskScenario.rb, line 1572 1572: def bookResource(resource, sbIdx, date) 1573: booked = false 1574: resource.allLeaves.each do |r| 1575: # Prevent overbooking when multiple resources are allocated and 1576: # available. 1577: break if a('effort') > 0 && @doneEffort >= a('effort') 1578: 1579: if r.book(@scenarioIdx, sbIdx, @property) 1580: 1581: if a('assignedresources').empty? 1582: if a('forward') 1583: @property['start', @scenarioIdx] = @project.idxToDate(sbIdx) 1584: else 1585: @property['end', @scenarioIdx] = @project.idxToDate(sbIdx + 1) 1586: end 1587: end 1588: 1589: @tentativeStart = @project.idxToDate(sbIdx) 1590: @tentativeEnd = @project.idxToDate(sbIdx + 1) 1591: 1592: @doneEffort += r['efficiency', @scenarioIdx] 1593: # Limits do not take efficiency into account. Limits are usage limits, 1594: # not effort limits. 1595: @limits.each do |limit| 1596: limit.inc(date) 1597: end 1598: 1599: unless a('assignedresources').include?(r) 1600: @property['assignedresources', @scenarioIdx] << r 1601: end 1602: booked = true 1603: end 1604: end 1605: 1606: booked 1607: end
# File lib/TaskScenario.rb, line 1491 1491: def bookResources(date, slotDuration) 1492: # If there are no allocations defined, we can't do any bookings. 1493: # In projection mode we do not allow bookings prior to the current date 1494: # for any task (in strict mode) or tasks which have user specified 1495: # bookings (sloppy mode). 1496: if a('allocate').empty? || 1497: (@project.scenario(@scenarioIdx).get('projection') && 1498: date < @project['now'] && 1499: (@project.scenario(@scenarioIdx).get('strict') || 1500: a('assignedresources').empty?)) 1501: return 1502: end 1503: 1504: # If the task has shifts to limit the allocations, we check that we are 1505: # within a defined shift interval. If yes, we need to be on shift to 1506: # continue. 1507: if (shifts = a('shifts')) && shifts.assigned?(date) 1508: return if !shifts.onShift?(date) 1509: end 1510: 1511: # If the task has allocation limits we need to make sure that none of them 1512: # is already exceeded. 1513: @limits.each do |limit| 1514: return if !limit.ok?(date) 1515: end 1516: 1517: sbIdx = @project.dateToIdx(date) 1518: 1519: # We first have to make sure that if there are mandatory resources 1520: # that these are all available for the time slot. 1521: takenMandatories = [] 1522: @mandatories.each do |allocation| 1523: return unless allocation.onShift?(date) 1524: 1525: # For mandatory allocations with alternatives at least one of the 1526: # alternatives must be available. 1527: found = false 1528: allocation.candidates(@scenarioIdx).each do |candidate| 1529: # When a resource group is marked mandatory, all members of the 1530: # group must be available. 1531: allAvailable = true 1532: candidate.allLeaves.each do |resource| 1533: if !resource.available?(@scenarioIdx, sbIdx) || 1534: takenMandatories.include?(resource) 1535: allAvailable = false 1536: break 1537: else 1538: takenMandatories << resource 1539: end 1540: end 1541: if allAvailable 1542: found = true 1543: break 1544: end 1545: end 1546: 1547: # At least one mandatory resource is not available. We cannot continue. 1548: return unless found 1549: end 1550: 1551: iv = Interval.new(date, date + slotDuration) 1552: a('allocate').each do |allocation| 1553: next unless allocation.onShift?(date) 1554: 1555: # In case we have a persistent allocation we need to check if there is 1556: # already a locked resource and use it. 1557: if allocation.persistent && !allocation.lockedResource.nil? 1558: bookResource(allocation.lockedResource, sbIdx, date) 1559: else 1560: # If not, we create a list of candidates in the proper order and 1561: # assign the first one available. 1562: allocation.candidates(@scenarioIdx).each do |candidate| 1563: if bookResource(candidate, sbIdx, date) 1564: allocation.lockedResource = candidate 1565: break 1566: end 1567: end 1568: end 1569: end 1570: end
Calculate the current completion degree for tasks that have no user specified completion value.
# File lib/TaskScenario.rb, line 1830 1830: def calcCompletion 1831: # If the user provided a completion degree we are not touching it. 1832: if @property.provided('complete', @scenarioIdx) 1833: calcStatus 1834: return 1835: end 1836: 1837: if a('start').nil? || a('end').nil? 1838: @property['complete', @scenarioIdx] = 0.0 1839: @property['status', @scenarioIdx] = 'unknown' 1840: return 1841: end 1842: 1843: if a('milestone') 1844: @property['complete', @scenarioIdx] = 1845: @property['end', @scenarioIdx] <= @project['now'] ? 100.0 : 0.0 1846: @property['status', @scenarioIdx] = 1847: a('end') <= @project['now'] ? 'done' : 'not reached' 1848: else 1849: completion = 0.0 1850: if a('end') <= @project['now'] 1851: # The task has ended already. It's 100% complete. 1852: completion = 100.0 1853: elsif @project['now'] <= a('start') 1854: # The task has not started yet. Its' 0% complete. 1855: completion = 0.0 1856: else 1857: # The task is in progress. Calculate the current completion 1858: # degree. 1859: if @property.leaf? && a('effort') > 0 1860: # Effort based leaf tasks. The completion degree is the percantage 1861: # of effort that has been done already. 1862: done = getEffectiveWork(@project.dateToIdx(a('start')), 1863: @project.dateToIdx(@project['now'])) 1864: total = @project.convertToDailyLoad( 1865: a('effort') * @project['scheduleGranularity']) 1866: completion = done / total * 100.0 1867: else 1868: # Container tasks and length/duration leaf tasks. There is no way 1869: # we can compute the completion degree of a container task with a 1870: # mix of effort and duration task in a meaningful way. So, we 1871: # just go by duration. 1872: completion = ((@project['now'] - a('start')) / 1873: (a('end') - a('start'))) * 100.0 1874: end 1875: end 1876: @property['complete', @scenarioIdx] = completion 1877: calcStatus 1878: end 1879: 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 1806 1806: def calcPathCriticalnessEndSuccs 1807: maxCriticalness = 0.0 1808: # Gather a list of all end-successors of this task and its parent task. 1809: tList = [] 1810: p = @property 1811: while (p) 1812: p['endsuccs', @scenarioIdx].each do |task, onEnd| 1813: tList << [ task, onEnd ] unless tList.include?([ task, onEnd ]) 1814: end 1815: p = p.parent 1816: end 1817: 1818: tList.each do |task, onEnd| 1819: if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) > 1820: maxCriticalness 1821: maxCriticalness = criticalness 1822: end 1823: end 1824: 1825: maxCriticalness 1826: end
Calculate the status of the task based on the ‘complete’ attribute.
# File lib/TaskScenario.rb, line 1882 1882: def calcStatus 1883: @property['status', @scenarioIdx] = 1884: if a('complete') == 0.0 1885: 'not started' 1886: elsif a('complete') >= 100.0 1887: 'done' 1888: else 1889: 'in progress' 1890: end 1891: end
# File lib/TaskScenario.rb, line 1724 1724: def checkDependency(dependency, depType) 1725: if (depTask = dependency.resolve(@project)).nil? 1726: # Remove the broken dependency. It could cause trouble later on. 1727: @property[depType, @scenarioIdx].delete(dependency) 1728: error('task_depend_unknown', 1729: "Task #{@property.fullId} has unknown #{depType} " + 1730: "#{dependency.taskId}") 1731: end 1732: 1733: if depTask == @property 1734: # Remove the broken dependency. It could cause trouble later on. 1735: @property[depType, @scenarioIdx].delete(dependency) 1736: error('task_depend_self', "Task #{@property.fullId} cannot " + 1737: "depend on self") 1738: end 1739: 1740: if depTask.isChildOf?(@property) 1741: # Remove the broken dependency. It could cause trouble later on. 1742: @property[depType, @scenarioIdx].delete(dependency) 1743: error('task_depend_child', 1744: "Task #{@property.fullId} cannot depend on child " + 1745: "#{depTask.fullId}") 1746: end 1747: 1748: if @property.isChildOf?(depTask) 1749: # Remove the broken dependency. It could cause trouble later on. 1750: @property[depType, @scenarioIdx].delete(dependency) 1751: error('task_depend_parent', 1752: "Task #{@property.fullId} cannot depend on parent " + 1753: "#{depTask.fullId}") 1754: end 1755: 1756: @property[depType, @scenarioIdx].each do |dep| 1757: if dep.task == depTask && dep != dependency 1758: # Remove the broken dependency. It could cause trouble later on. 1759: @property[depType, @scenarioIdx].delete(dependency) 1760: error('task_depend_multi', 1761: "No need to specify dependency #{depTask.fullId} multiple " + 1762: "times for task #{@property.fullId}.") 1763: end 1764: end 1765: 1766: depTask 1767: 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 1676 1676: def hasDependencies(atEnd) 1677: thisEnd = atEnd ? 'end' : 'start' 1678: !a(thisEnd + 'succs').empty? || !a(thisEnd + 'preds').empty? 1679: end
Return true if this task or any of its parent tasks has at least one sucessor task.
# File lib/TaskScenario.rb, line 1683 1683: def hasSuccessors 1684: t = @property 1685: while t 1686: return true unless t['endsuccs', @scenarioIdx].empty? 1687: t = t.parent 1688: end 1689: 1690: false 1691: end
# File lib/TaskScenario.rb, line 1693 1693: def markAsRunaway 1694: warning('runaway', "Task #{@property.fullId} does not fit into " + 1695: "project time frame") 1696: 1697: @isRunAway = true 1698: end
This function determines if a task is really a milestones and marks them accordingly.
# File lib/TaskScenario.rb, line 1702 1702: def markMilestone 1703: return if @property.container? || hasDurationSpec? || 1704: !a('booking').empty? || !a('allocate').empty? 1705: 1706: # The following cases qualify for an automatic milestone promotion. 1707: # - --> - 1708: # | --> - 1709: # |D --> - 1710: # -D --> - 1711: # - <-- - 1712: # - <-- | 1713: # - <-- -D 1714: # - <-- |D 1715: hasStartSpec = !a('start').nil? || !a('depends').empty? 1716: hasEndSpec = !a('end').nil? || !a('precedes').empty? 1717: 1718: @property['milestone', @scenarioIdx] = 1719: (hasStartSpec && a('forward') && !hasEndSpec) || 1720: (!hasStartSpec && !a('forward') && hasEndSpec) || 1721: (!hasStartSpec && !hasEndSpec) 1722: 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 1481 1481: def nextSlot(slotDuration) 1482: return nil if a('scheduled') || @isRunAway 1483: 1484: if a('forward') 1485: @lastSlot.nil? ? a('start') : @lastSlot + slotDuration 1486: else 1487: @lastSlot.nil? ? a('end') - slotDuration : @lastSlot - slotDuration 1488: end 1489: 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 1778 1778: def propagateDateToDep(task, atEnd) 1779: #puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId}" 1780: # Don't propagate if the task is already completely scheduled or is a 1781: # container. 1782: return if task['scheduled', @scenarioIdx] || task.container? 1783: 1784: # Don't propagate if the task already has a date for that end. 1785: return unless task[atEnd ? 'end' : 'start', @scenarioIdx].nil? 1786: 1787: # Don't propagate if the task has a duration or is a milestone and the 1788: # task end to set is in the scheduling direction. 1789: return if task.hasDurationSpec?(@scenarioIdx) && 1790: !(atEnd ^ task['forward', @scenarioIdx]) 1791: 1792: # Check if all other dependencies for that task end have been determined 1793: # already and use the latest or earliest possible date. Don't propagate 1794: # if we don't have all dates yet. 1795: return if (nDate = (atEnd ? task.latestEnd(@scenarioIdx) : 1796: task.earliestStart(@scenarioIdx))).nil? 1797: 1798: # Looks like it is ok to propagate the date. 1799: task.propagateDate(@scenarioIdx, nDate, atEnd) 1800: # puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId} done" 1801: end
# File lib/TaskScenario.rb, line 1382 1382: def scheduleSlot(slot, slotDuration) 1383: # Tasks must always be scheduled in a single contigous fashion. @lastSlot 1384: # indicates the slot that was used for the previous call. Depending on the 1385: # scheduling direction the next slot must be scheduled either right before 1386: # or after this slot. If the current slot is not directly aligned, we'll 1387: # wait for another call with a proper slot. The function returns true 1388: # only if a slot could be scheduled. 1389: if a('forward') 1390: # On first call, the @lastSlot is not set yet. We set it to the slot 1391: # before the start slot. 1392: if @lastSlot.nil? 1393: @lastSlot = a('start') - slotDuration 1394: @tentativeEnd = slot + slotDuration 1395: end 1396: 1397: return false unless slot == @lastSlot + slotDuration 1398: else 1399: # On first call, the @lastSlot is not set yet. We set it to the slot 1400: # to the end slot. 1401: if @lastSlot.nil? 1402: @lastSlot = a('end') 1403: @tentativeStart = slot 1404: end 1405: 1406: return false unless slot == @lastSlot - slotDuration 1407: end 1408: @lastSlot = slot 1409: 1410: if a('length') > 0 || a('duration') > 0 1411: # The doneDuration counts the number of scheduled slots. It is increased 1412: # by one with every scheduled slot. The doneLength is only increased for 1413: # global working time slots. 1414: bookResources(slot, slotDuration) 1415: @doneDuration += 1 1416: if @project.isWorkingTime(slot, slot + slotDuration) 1417: @doneLength += 1 1418: end 1419: 1420: # If we have reached the specified duration or lengths, we set the end 1421: # or start date and propagate the value to neighbouring tasks. 1422: if (a('length') > 0 && @doneLength >= a('length')) || 1423: (a('duration') > 0 && @doneDuration >= a('duration')) 1424: @property['scheduled', @scenarioIdx] = true 1425: if a('forward') 1426: propagateDate(slot + slotDuration, true) 1427: else 1428: propagateDate(slot, false) 1429: end 1430: return true 1431: end 1432: elsif a('effort') > 0 1433: bookResources(slot, slotDuration) if @doneEffort < a('effort') 1434: if @doneEffort >= a('effort') 1435: # The specified effort has been reached. The has been fully scheduled 1436: # now. 1437: @property['scheduled', @scenarioIdx] = true 1438: if a('forward') 1439: propagateDate(@tentativeEnd, true) 1440: else 1441: propagateDate(@tentativeStart, false) 1442: end 1443: return true 1444: end 1445: elsif a('milestone') 1446: if a('forward') 1447: propagateDate(a('start'), true) 1448: else 1449: propagateDate(a('end'), false) 1450: end 1451: return true 1452: elsif a('start') && a('end') 1453: # Task with start and end date but no duration criteria 1454: if a('allocate').empty? 1455: # For start-end-tasks without allocation, we don't have to do 1456: # anything but to set the 'scheduled' flag. 1457: @property['scheduled', @scenarioIdx] = true 1458: @property.parent.scheduleContainer(@scenarioIdx) if @property.parent 1459: return true 1460: end 1461: 1462: bookResources(slot, slotDuration) 1463: 1464: # Depending on the scheduling direction we can mark the task as 1465: # scheduled once we have reached the other end. 1466: if (a('forward') && slot + slotDuration >= a('end')) || 1467: (!a('forward') && slot <= a('start')) 1468: @property['scheduled', @scenarioIdx] = true 1469: @property.parent.scheduleContainer(@scenarioIdx) if @property.parent 1470: return true 1471: end 1472: end 1473: 1474: false 1475: end
Set @startIsDetermed or @endIsDetermed (depending on _setStart) to value.
# File lib/TaskScenario.rb, line 1771 1771: def setDetermination(setStart, value) 1772: setStart ? @startIsDetermed = value : @endIsDetermed = value 1773: end
Recursively compile a list of Task properties which depend on the current task.
# File lib/TaskScenario.rb, line 1895 1895: def targets(list) 1896: # A target must be a leaf function that has no direct or indirect 1897: # (through parent) following tasks. 1898: if @property.leaf? && !hasSuccessors && !list.include?(@property) 1899: list << @property 1900: return 1901: end 1902: 1903: a('endsuccs').each do |t, onEnd| 1904: t.targets(@scenarioIdx, list) 1905: end 1906: 1907: # Check of indirect followers. 1908: @property.parent.targets(@scenarioIdx, list) if @property.parent 1909: 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 1916 1916: def turnover(startIdx, endIdx, account, resource = nil) 1917: amount = 0.0 1918: if @property.container? 1919: @property.children.each do |child| 1920: amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, 1921: resource) 1922: end 1923: end 1924: 1925: # If there are no chargeset defined for this task, we don't need to 1926: # compute the resource related or other cost. 1927: unless a('chargeset').empty? 1928: resourceCost = 0.0 1929: otherCost = 0.0 1930: 1931: # Container tasks don't have resource cost. 1932: unless @property.container? 1933: if resource 1934: resourceCost = resource.cost(@scenarioIdx, startIdx, endIdx, 1935: @property) 1936: else 1937: a('assignedresources').each do |r| 1938: resourceCost += r.cost(@scenarioIdx, startIdx, endIdx, @property) 1939: end 1940: end 1941: end 1942: 1943: unless a('charge').empty? 1944: # Add one-time and periodic charges to the amount. 1945: startDate = startIdx.is_a?(TjTime) ? startIdx : 1946: @project.idxToDate(startIdx) 1947: endDate = endIdx.is_a?(TjTime) ? endIdx : 1948: @project.idxToDate(endIdx) 1949: iv = Interval.new(startDate, endDate) 1950: a('charge').each do |charge| 1951: otherCost += charge.turnover(iv) 1952: end 1953: end 1954: 1955: totalCost = resourceCost + otherCost 1956: # Now weight the total cost by the share of the account 1957: a('chargeset').each do |set| 1958: set.each do |accnt, share| 1959: if share > 0.0 && (accnt == account || accnt.isChildOf?(account)) 1960: amount += totalCost * share 1961: end 1962: end 1963: end 1964: end 1965: 1966: amount 1967: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.