taskjuggler 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +280 -0
- data/README +31 -0
- data/Rakefile +20 -0
- data/benchmarks/UTF-8-Strings.rb +58 -0
- data/benchmarks/allocate.tjp +30 -0
- data/benchmarks/booking.tjp +62 -0
- data/benchmarks/depends.tjp +112 -0
- data/benchmarks/htmltaskreport.tjp +45 -0
- data/benchmarks/runbench.rb +24 -0
- data/bin/tj3 +3 -0
- data/bin/tj3man +3 -0
- data/doc/classes/AppConfig.html +808 -0
- data/doc/classes/Arguments.html +226 -0
- data/doc/classes/String.html +395 -0
- data/doc/classes/TaskJuggler.html +1358 -0
- data/doc/classes/TaskJuggler/Account.html +257 -0
- data/doc/classes/TaskJuggler/AccountScenario.html +218 -0
- data/doc/classes/TaskJuggler/Allocation.html +419 -0
- data/doc/classes/TaskJuggler/AllocationAttribute.html +291 -0
- data/doc/classes/TaskJuggler/AttributeBase.html +608 -0
- data/doc/classes/TaskJuggler/AttributeDefinition.html +259 -0
- data/doc/classes/TaskJuggler/Booking.html +307 -0
- data/doc/classes/TaskJuggler/BookingListAttribute.html +263 -0
- data/doc/classes/TaskJuggler/BooleanAttribute.html +261 -0
- data/doc/classes/TaskJuggler/CSVFile.html +325 -0
- data/doc/classes/TaskJuggler/Charge.html +279 -0
- data/doc/classes/TaskJuggler/ChargeListAttribute.html +229 -0
- data/doc/classes/TaskJuggler/ChargeSet.html +440 -0
- data/doc/classes/TaskJuggler/ChargeSetListAttribute.html +276 -0
- data/doc/classes/TaskJuggler/ColumnTable.html +260 -0
- data/doc/classes/TaskJuggler/DateAttribute.html +194 -0
- data/doc/classes/TaskJuggler/DependencyListAttribute.html +267 -0
- data/doc/classes/TaskJuggler/DurationAttribute.html +229 -0
- data/doc/classes/TaskJuggler/FixnumAttribute.html +194 -0
- data/doc/classes/TaskJuggler/FlagListAttribute.html +263 -0
- data/doc/classes/TaskJuggler/FloatAttribute.html +229 -0
- data/doc/classes/TaskJuggler/GanttChart.html +667 -0
- data/doc/classes/TaskJuggler/GanttContainer.html +441 -0
- data/doc/classes/TaskJuggler/GanttHeader.html +280 -0
- data/doc/classes/TaskJuggler/GanttHeaderScaleItem.html +245 -0
- data/doc/classes/TaskJuggler/GanttLine.html +398 -0
- data/doc/classes/TaskJuggler/GanttLoadStack.html +327 -0
- data/doc/classes/TaskJuggler/GanttMilestone.html +415 -0
- data/doc/classes/TaskJuggler/GanttRouter.html +425 -0
- data/doc/classes/TaskJuggler/GanttTaskBar.html +429 -0
- data/doc/classes/TaskJuggler/HTMLDocument.html +240 -0
- data/doc/classes/TaskJuggler/HTMLGraphics.html +231 -0
- data/doc/classes/TaskJuggler/Interval.html +552 -0
- data/doc/classes/TaskJuggler/IntervalListAttribute.html +267 -0
- data/doc/classes/TaskJuggler/KeywordDocumentation.html +796 -0
- data/doc/classes/TaskJuggler/Limits.html +416 -0
- data/doc/classes/TaskJuggler/Limits/Limit.html +381 -0
- data/doc/classes/TaskJuggler/LimitsAttribute.html +261 -0
- data/doc/classes/TaskJuggler/Log.html +613 -0
- data/doc/classes/TaskJuggler/LogicalAttribute.html +226 -0
- data/doc/classes/TaskJuggler/LogicalExpression.html +251 -0
- data/doc/classes/TaskJuggler/LogicalFlag.html +229 -0
- data/doc/classes/TaskJuggler/LogicalFunction.html +324 -0
- data/doc/classes/TaskJuggler/LogicalOperation.html +299 -0
- data/doc/classes/TaskJuggler/Macro.html +194 -0
- data/doc/classes/TaskJuggler/MacroParser.html +360 -0
- data/doc/classes/TaskJuggler/MacroTable.html +366 -0
- data/doc/classes/TaskJuggler/Message.html +281 -0
- data/doc/classes/TaskJuggler/MessageHandler.html +215 -0
- data/doc/classes/TaskJuggler/Project.html +1606 -0
- data/doc/classes/TaskJuggler/ProjectFileParser.html +412 -0
- data/doc/classes/TaskJuggler/PropertyList.html +597 -0
- data/doc/classes/TaskJuggler/PropertySet.html +1200 -0
- data/doc/classes/TaskJuggler/PropertyTreeNode.html +1449 -0
- data/doc/classes/TaskJuggler/Query.html +600 -0
- data/doc/classes/TaskJuggler/RealFormat.html +252 -0
- data/doc/classes/TaskJuggler/ReferenceAttribute.html +194 -0
- data/doc/classes/TaskJuggler/Report.html +528 -0
- data/doc/classes/TaskJuggler/ReportElement.html +1070 -0
- data/doc/classes/TaskJuggler/ReportTable.html +497 -0
- data/doc/classes/TaskJuggler/ReportTableCell.html +518 -0
- data/doc/classes/TaskJuggler/ReportTableColumn.html +364 -0
- data/doc/classes/TaskJuggler/ReportTableElement.html +644 -0
- data/doc/classes/TaskJuggler/ReportTableLegend.html +343 -0
- data/doc/classes/TaskJuggler/ReportTableLine.html +431 -0
- data/doc/classes/TaskJuggler/Resource.html +211 -0
- data/doc/classes/TaskJuggler/ResourceListAttribute.html +267 -0
- data/doc/classes/TaskJuggler/ResourceListRE.html +249 -0
- data/doc/classes/TaskJuggler/ResourceScenario.html +1137 -0
- data/doc/classes/TaskJuggler/RichText.html +537 -0
- data/doc/classes/TaskJuggler/RichTextAttribute.html +229 -0
- data/doc/classes/TaskJuggler/RichTextDocument.html +418 -0
- data/doc/classes/TaskJuggler/RichTextElement.html +829 -0
- data/doc/classes/TaskJuggler/RichTextException.html +212 -0
- data/doc/classes/TaskJuggler/RichTextParser.html +317 -0
- data/doc/classes/TaskJuggler/RichTextProtocolExample.html +303 -0
- data/doc/classes/TaskJuggler/RichTextProtocolHandler.html +194 -0
- data/doc/classes/TaskJuggler/RichTextScanner.html +561 -0
- data/doc/classes/TaskJuggler/RichTextSnip.html +364 -0
- data/doc/classes/TaskJuggler/RichTextSyntaxRules.html +883 -0
- data/doc/classes/TaskJuggler/Scenario.html +163 -0
- data/doc/classes/TaskJuggler/ScenarioData.html +354 -0
- data/doc/classes/TaskJuggler/Scoreboard.html +638 -0
- data/doc/classes/TaskJuggler/Shift.html +255 -0
- data/doc/classes/TaskJuggler/ShiftAssignment.html +488 -0
- data/doc/classes/TaskJuggler/ShiftAssignments.html +715 -0
- data/doc/classes/TaskJuggler/ShiftAssignmentsAttribute.html +261 -0
- data/doc/classes/TaskJuggler/ShiftScenario.html +282 -0
- data/doc/classes/TaskJuggler/SourceFileInfo.html +247 -0
- data/doc/classes/TaskJuggler/StringAttribute.html +229 -0
- data/doc/classes/TaskJuggler/SymbolAttribute.html +194 -0
- data/doc/classes/TaskJuggler/SyntaxReference.html +516 -0
- data/doc/classes/TaskJuggler/TOCEntry.html +242 -0
- data/doc/classes/TaskJuggler/TableColumnDefinition.html +273 -0
- data/doc/classes/TaskJuggler/TableOfContents.html +256 -0
- data/doc/classes/TaskJuggler/Task.html +203 -0
- data/doc/classes/TaskJuggler/TaskDependency.html +251 -0
- data/doc/classes/TaskJuggler/TaskListAttribute.html +267 -0
- data/doc/classes/TaskJuggler/TaskListRE.html +250 -0
- data/doc/classes/TaskJuggler/TaskScenario.html +2206 -0
- data/doc/classes/TaskJuggler/TextParser.html +670 -0
- data/doc/classes/TaskJuggler/TextParser/Pattern.html +923 -0
- data/doc/classes/TaskJuggler/TextParser/Rule.html +779 -0
- data/doc/classes/TaskJuggler/TextParser/StackElement.html +267 -0
- data/doc/classes/TaskJuggler/TextParser/TextParserResultArray.html +212 -0
- data/doc/classes/TaskJuggler/TextParser/TokenDoc.html +221 -0
- data/doc/classes/TaskJuggler/TextScanner.html +708 -0
- data/doc/classes/TaskJuggler/TextScanner/BufferStreamHandle.html +355 -0
- data/doc/classes/TaskJuggler/TextScanner/FileStreamHandle.html +341 -0
- data/doc/classes/TaskJuggler/TextScanner/StreamHandle.html +260 -0
- data/doc/classes/TaskJuggler/TjException.html +185 -0
- data/doc/classes/TaskJuggler/TjTime.html +1845 -0
- data/doc/classes/TaskJuggler/TjpExample.html +310 -0
- data/doc/classes/TaskJuggler/TjpExportRE.html +329 -0
- data/doc/classes/TaskJuggler/TjpSyntaxRules.html +8928 -0
- data/doc/classes/TaskJuggler/UserManual.html +606 -0
- data/doc/classes/TaskJuggler/WorkingHours.html +582 -0
- data/doc/classes/TaskJuggler/WorkingHoursAttribute.html +284 -0
- data/doc/classes/TaskJuggler/XMLBlob.html +207 -0
- data/doc/classes/TaskJuggler/XMLComment.html +206 -0
- data/doc/classes/TaskJuggler/XMLDocument.html +293 -0
- data/doc/classes/TaskJuggler/XMLElement.html +341 -0
- data/doc/classes/TaskJuggler/XMLNamedText.html +174 -0
- data/doc/classes/TaskJuggler/XMLText.html +221 -0
- data/doc/files/COPYING.html +448 -0
- data/doc/files/README.html +134 -0
- data/doc/files/lib/AccountScenario_rb.html +116 -0
- data/doc/files/lib/Account_rb.html +118 -0
- data/doc/files/lib/Allocation_rb.html +118 -0
- data/doc/files/lib/AppConfig_rb.html +116 -0
- data/doc/files/lib/AttributeBase_rb.html +106 -0
- data/doc/files/lib/AttributeDefinition_rb.html +106 -0
- data/doc/files/lib/Attributes_rb.html +130 -0
- data/doc/files/lib/Booking_rb.html +106 -0
- data/doc/files/lib/ChargeSet_rb.html +116 -0
- data/doc/files/lib/Charge_rb.html +116 -0
- data/doc/files/lib/HTMLDocument_rb.html +116 -0
- data/doc/files/lib/Interval_rb.html +116 -0
- data/doc/files/lib/KeywordDocumentation_rb.html +122 -0
- data/doc/files/lib/Limits_rb.html +116 -0
- data/doc/files/lib/Log_rb.html +116 -0
- data/doc/files/lib/LogicalExpression_rb.html +122 -0
- data/doc/files/lib/LogicalFlag_rb.html +116 -0
- data/doc/files/lib/LogicalFunction_rb.html +116 -0
- data/doc/files/lib/LogicalOperation_rb.html +116 -0
- data/doc/files/lib/MacroParser_rb.html +118 -0
- data/doc/files/lib/MacroTable_rb.html +122 -0
- data/doc/files/lib/MessageHandler_rb.html +106 -0
- data/doc/files/lib/Message_rb.html +116 -0
- data/doc/files/lib/ProjectFileParser_rb.html +122 -0
- data/doc/files/lib/Project_rb.html +148 -0
- data/doc/files/lib/PropertyList_rb.html +106 -0
- data/doc/files/lib/PropertySet_rb.html +118 -0
- data/doc/files/lib/PropertyTreeNode_rb.html +106 -0
- data/doc/files/lib/Query_rb.html +116 -0
- data/doc/files/lib/RealFormat_rb.html +106 -0
- data/doc/files/lib/ResourceScenario_rb.html +116 -0
- data/doc/files/lib/Resource_rb.html +118 -0
- data/doc/files/lib/RichTextDocument_rb.html +120 -0
- data/doc/files/lib/RichTextElement_rb.html +120 -0
- data/doc/files/lib/RichTextParser_rb.html +120 -0
- data/doc/files/lib/RichTextProtocolExample_rb.html +120 -0
- data/doc/files/lib/RichTextProtocolHandler_rb.html +106 -0
- data/doc/files/lib/RichTextScanner_rb.html +116 -0
- data/doc/files/lib/RichTextSnip_rb.html +118 -0
- data/doc/files/lib/RichTextSyntaxRules_rb.html +106 -0
- data/doc/files/lib/RichText_rb.html +118 -0
- data/doc/files/lib/ScenarioData_rb.html +118 -0
- data/doc/files/lib/Scenario_rb.html +116 -0
- data/doc/files/lib/Scoreboard_rb.html +106 -0
- data/doc/files/lib/ShiftAssignments_rb.html +116 -0
- data/doc/files/lib/ShiftScenario_rb.html +116 -0
- data/doc/files/lib/Shift_rb.html +118 -0
- data/doc/files/lib/SourceFileInfo_rb.html +106 -0
- data/doc/files/lib/SyntaxReference_rb.html +122 -0
- data/doc/files/lib/TOCEntry_rb.html +118 -0
- data/doc/files/lib/TableColumnDefinition_rb.html +106 -0
- data/doc/files/lib/TableOfContents_rb.html +118 -0
- data/doc/files/lib/TaskDependency_rb.html +106 -0
- data/doc/files/lib/TaskJuggler_rb.html +120 -0
- data/doc/files/lib/TaskScenario_rb.html +116 -0
- data/doc/files/lib/Task_rb.html +118 -0
- data/doc/files/lib/TextParser/Pattern_rb.html +116 -0
- data/doc/files/lib/TextParser/Rule_rb.html +106 -0
- data/doc/files/lib/TextParser/StackElement_rb.html +106 -0
- data/doc/files/lib/TextParser/TokenDoc_rb.html +106 -0
- data/doc/files/lib/TextParser_rb.html +124 -0
- data/doc/files/lib/TextScanner_rb.html +128 -0
- data/doc/files/lib/Tj3Config_rb.html +118 -0
- data/doc/files/lib/TjException_rb.html +106 -0
- data/doc/files/lib/TjTime_rb.html +118 -0
- data/doc/files/lib/TjpExample_rb.html +116 -0
- data/doc/files/lib/TjpSyntaxRules_rb.html +106 -0
- data/doc/files/lib/UTF8String_rb.html +132 -0
- data/doc/files/lib/UserManual_rb.html +124 -0
- data/doc/files/lib/WorkingHours_rb.html +116 -0
- data/doc/files/lib/XMLDocument_rb.html +116 -0
- data/doc/files/lib/XMLElement_rb.html +116 -0
- data/doc/files/lib/reports/CSVFile_rb.html +116 -0
- data/doc/files/lib/reports/ColumnTable_rb.html +116 -0
- data/doc/files/lib/reports/GanttChart_rb.html +122 -0
- data/doc/files/lib/reports/GanttContainer_rb.html +116 -0
- data/doc/files/lib/reports/GanttHeaderScaleItem_rb.html +106 -0
- data/doc/files/lib/reports/GanttHeader_rb.html +116 -0
- data/doc/files/lib/reports/GanttLine_rb.html +126 -0
- data/doc/files/lib/reports/GanttLoadStack_rb.html +116 -0
- data/doc/files/lib/reports/GanttMilestone_rb.html +116 -0
- data/doc/files/lib/reports/GanttRouter_rb.html +106 -0
- data/doc/files/lib/reports/GanttTaskBar_rb.html +116 -0
- data/doc/files/lib/reports/HTMLGraphics_rb.html +106 -0
- data/doc/files/lib/reports/ReportElement_rb.html +118 -0
- data/doc/files/lib/reports/ReportTableCell_rb.html +106 -0
- data/doc/files/lib/reports/ReportTableColumn_rb.html +106 -0
- data/doc/files/lib/reports/ReportTableElement_rb.html +122 -0
- data/doc/files/lib/reports/ReportTableLegend_rb.html +106 -0
- data/doc/files/lib/reports/ReportTableLine_rb.html +116 -0
- data/doc/files/lib/reports/ReportTable_rb.html +118 -0
- data/doc/files/lib/reports/Report_rb.html +126 -0
- data/doc/files/lib/reports/ResourceListRE_rb.html +122 -0
- data/doc/files/lib/reports/TaskListRE_rb.html +122 -0
- data/doc/files/lib/reports/TjpExportRE_rb.html +116 -0
- data/doc/files/lib/taskjuggler3_rb.html +276 -0
- data/doc/files/lib/tj3man_rb.html +189 -0
- data/doc/fr_class_index.html +285 -0
- data/doc/fr_file_index.html +223 -0
- data/doc/fr_method_index.html +1953 -0
- data/doc/index.html +21 -0
- data/doc/rdoc-style.css +299 -0
- data/examples/tutorial.tjp +361 -0
- data/gem_spec.rb +30 -0
- data/lib/Account.rb +50 -0
- data/lib/AccountScenario.rb +39 -0
- data/lib/Allocation.rb +102 -0
- data/lib/AppConfig.rb +134 -0
- data/lib/AttributeBase.rb +131 -0
- data/lib/AttributeDefinition.rb +47 -0
- data/lib/Attributes.rb +478 -0
- data/lib/BatchProcessor.rb +209 -0
- data/lib/Booking.rb +59 -0
- data/lib/Charge.rb +71 -0
- data/lib/ChargeSet.rb +126 -0
- data/lib/HTMLDocument.rb +59 -0
- data/lib/Interval.rb +127 -0
- data/lib/KeywordDocumentation.rb +560 -0
- data/lib/Limits.rb +219 -0
- data/lib/Log.rb +160 -0
- data/lib/LogicalExpression.rb +71 -0
- data/lib/LogicalFlag.rb +34 -0
- data/lib/LogicalFunction.rb +102 -0
- data/lib/LogicalOperation.rb +118 -0
- data/lib/MacroParser.rb +77 -0
- data/lib/MacroTable.rb +84 -0
- data/lib/Message.rb +56 -0
- data/lib/MessageHandler.rb +34 -0
- data/lib/Project.rb +662 -0
- data/lib/ProjectFileParser.rb +333 -0
- data/lib/PropertyList.rb +181 -0
- data/lib/PropertySet.rb +304 -0
- data/lib/PropertyTreeNode.rb +461 -0
- data/lib/Query.rb +227 -0
- data/lib/RealFormat.rb +73 -0
- data/lib/Resource.rb +42 -0
- data/lib/ResourceScenario.rb +511 -0
- data/lib/RichText.rb +147 -0
- data/lib/RichTextDocument.rb +139 -0
- data/lib/RichTextElement.rb +391 -0
- data/lib/RichTextParser.rb +66 -0
- data/lib/RichTextProtocolExample.rb +65 -0
- data/lib/RichTextProtocolHandler.rb +35 -0
- data/lib/RichTextScanner.rb +390 -0
- data/lib/RichTextSnip.rb +104 -0
- data/lib/RichTextSyntaxRules.rb +265 -0
- data/lib/Scenario.rb +27 -0
- data/lib/ScenarioData.rb +65 -0
- data/lib/Scoreboard.rb +141 -0
- data/lib/Shift.rb +48 -0
- data/lib/ShiftAssignments.rb +291 -0
- data/lib/ShiftScenario.rb +46 -0
- data/lib/SourceFileInfo.rb +37 -0
- data/lib/SyntaxReference.rb +284 -0
- data/lib/TOCEntry.rb +76 -0
- data/lib/TableColumnDefinition.rb +54 -0
- data/lib/TableOfContents.rb +46 -0
- data/lib/Task.rb +37 -0
- data/lib/TaskDependency.rb +39 -0
- data/lib/TaskJuggler.rb +84 -0
- data/lib/TaskScenario.rb +1622 -0
- data/lib/TextParser.rb +416 -0
- data/lib/TextParser/Pattern.rb +263 -0
- data/lib/TextParser/Rule.rb +171 -0
- data/lib/TextParser/StackElement.rb +45 -0
- data/lib/TextParser/TokenDoc.rb +38 -0
- data/lib/TextScanner.rb +682 -0
- data/lib/Tj3Config.rb +27 -0
- data/lib/TjException.rb +27 -0
- data/lib/TjTime.rb +395 -0
- data/lib/TjpExample.rb +119 -0
- data/lib/TjpSyntaxRules.rb +4022 -0
- data/lib/UTF8String.rb +100 -0
- data/lib/UserManual.rb +282 -0
- data/lib/WorkingHours.rb +323 -0
- data/lib/XMLDocument.rb +54 -0
- data/lib/XMLElement.rb +175 -0
- data/lib/reports/CSVFile.rb +146 -0
- data/lib/reports/ColumnTable.rb +66 -0
- data/lib/reports/GanttChart.rb +308 -0
- data/lib/reports/GanttContainer.rb +107 -0
- data/lib/reports/GanttHeader.rb +141 -0
- data/lib/reports/GanttHeaderScaleItem.rb +42 -0
- data/lib/reports/GanttLine.rb +329 -0
- data/lib/reports/GanttLoadStack.rb +113 -0
- data/lib/reports/GanttMilestone.rb +80 -0
- data/lib/reports/GanttRouter.rb +375 -0
- data/lib/reports/GanttTaskBar.rb +95 -0
- data/lib/reports/HTMLGraphics.rb +65 -0
- data/lib/reports/Report.rb +344 -0
- data/lib/reports/ReportElement.rb +427 -0
- data/lib/reports/ReportTable.rb +144 -0
- data/lib/reports/ReportTableCell.rb +142 -0
- data/lib/reports/ReportTableColumn.rb +82 -0
- data/lib/reports/ReportTableElement.rb +852 -0
- data/lib/reports/ReportTableLegend.rb +167 -0
- data/lib/reports/ReportTableLine.rb +87 -0
- data/lib/reports/ResourceListRE.rb +72 -0
- data/lib/reports/TaskListRE.rb +73 -0
- data/lib/reports/TjpExportRE.rb +394 -0
- data/lib/taskjuggler3.rb +106 -0
- data/lib/tj3man.rb +88 -0
- data/manual/Day_To_Day_Juggling +168 -0
- data/manual/Getting_Started +61 -0
- data/manual/How_To_Contribute +185 -0
- data/manual/Installation +68 -0
- data/manual/Intro +102 -0
- data/manual/Reporting_Bugs +26 -0
- data/manual/Rich_Text_Attributes +90 -0
- data/manual/TaskJuggler_2x_Migration +40 -0
- data/manual/Tutorial +579 -0
- data/manual/fdl +450 -0
- data/prj_cfg.rb +43 -0
- data/setup.rb +1585 -0
- data/tasks/csts.rake +72 -0
- data/tasks/gem.rake +14 -0
- data/tasks/manual.rake +10 -0
- data/tasks/missing.rake +21 -0
- data/tasks/rcov.rake +14 -0
- data/tasks/rdoc.rake +17 -0
- data/tasks/rexml_fix.rb +16 -0
- data/tasks/rexml_fix_19.rb +49 -0
- data/tasks/show.rake +21 -0
- data/tasks/stats.rake +25 -0
- data/tasks/test.rake +11 -0
- data/test/MessageChecker.rb +53 -0
- data/test/TestSuite/CSV-Reports/celltext-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/celltext.tjp +7 -0
- data/test/TestSuite/CSV-Reports/genrefs +6 -0
- data/test/TestSuite/CSV-Reports/project-1.tji +57 -0
- data/test/TestSuite/CSV-Reports/resourcereport-Reference.csv +4 -0
- data/test/TestSuite/CSV-Reports/resourcereport.tjp +10 -0
- data/test/TestSuite/CSV-Reports/resourcereport_with_tasks-Reference.csv +22 -0
- data/test/TestSuite/CSV-Reports/resourcereport_with_tasks.tjp +11 -0
- data/test/TestSuite/CSV-Reports/sortByTree-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/sortByTree.tjp +8 -0
- data/test/TestSuite/CSV-Reports/sortBy_duration.down-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/sortBy_duration.down.tjp +10 -0
- data/test/TestSuite/CSV-Reports/sortBy_effort.up-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/sortBy_effort.up.tjp +10 -0
- data/test/TestSuite/CSV-Reports/sortBy_plan.start.down-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/sortBy_plan.start.down.tjp +10 -0
- data/test/TestSuite/CSV-Reports/taskreport-Reference.csv +14 -0
- data/test/TestSuite/CSV-Reports/taskreport.tjp +10 -0
- data/test/TestSuite/CSV-Reports/taskreport_with_resources-Reference.csv +24 -0
- data/test/TestSuite/CSV-Reports/taskreport_with_resources.tjp +11 -0
- data/test/TestSuite/Scheduler/Correct/Allocate.tjp +86 -0
- data/test/TestSuite/Scheduler/Correct/AutomaticMilestones.tjp +63 -0
- data/test/TestSuite/Scheduler/Correct/Booking.tjp +161 -0
- data/test/TestSuite/Scheduler/Correct/Depends.tjp +50 -0
- data/test/TestSuite/Scheduler/Correct/Duration.tjp +34 -0
- data/test/TestSuite/Scheduler/Correct/InheritStartEnd.tjp +129 -0
- data/test/TestSuite/Scheduler/Correct/Limits.tjp +81 -0
- data/test/TestSuite/Scheduler/Correct/MultipleMandatories.tjp +43 -0
- data/test/TestSuite/Scheduler/Correct/Optimize-1.tjp +28 -0
- data/test/TestSuite/Scheduler/Correct/Optimize-2.tjp +33 -0
- data/test/TestSuite/Scheduler/Correct/Optimize-3.tjp +33 -0
- data/test/TestSuite/Scheduler/Correct/Optimize-4.tjp +34 -0
- data/test/TestSuite/Scheduler/Correct/Optimize-5.tjp +62 -0
- data/test/TestSuite/Scheduler/Correct/Precedes.tjp +50 -0
- data/test/TestSuite/Scheduler/Correct/Shift.tjp +102 -0
- data/test/TestSuite/Scheduler/Errors/account_no_leaf.tjp +13 -0
- data/test/TestSuite/Scheduler/Errors/booking_conflict.tjp +10 -0
- data/test/TestSuite/Scheduler/Errors/booking_no_duty.tjp +9 -0
- data/test/TestSuite/Scheduler/Errors/booking_on_vacation.tjp +9 -0
- data/test/TestSuite/Scheduler/Errors/container_booking.tjp +14 -0
- data/test/TestSuite/Scheduler/Errors/container_duration.tjp +11 -0
- data/test/TestSuite/Scheduler/Errors/effort_no_allocations.tjp +7 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_1.tjp +19 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_10.tjp +36 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_11.tjp +27 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_12.tjp +20 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_13.tjp +27 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_14.tjp +26 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_2.tjp +24 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_3.tjp +18 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_4.tjp +36 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_5.tjp +37 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_6.tjp +35 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_7.tjp +46 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_8.tjp +51 -0
- data/test/TestSuite/Scheduler/Errors/loop_detected_9.tjp +20 -0
- data/test/TestSuite/Scheduler/Errors/maxend.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/maxstart.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/milestone_booking.tjp +10 -0
- data/test/TestSuite/Scheduler/Errors/milestone_duration.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/milestone_start_end.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/minend.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/minstart.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/multiple_durations.tjp +11 -0
- data/test/TestSuite/Scheduler/Errors/no_tasks.tjp +6 -0
- data/test/TestSuite/Scheduler/Errors/not_scheduled.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/task_depend_child.tjp +10 -0
- data/test/TestSuite/Scheduler/Errors/task_depend_multi.tjp +13 -0
- data/test/TestSuite/Scheduler/Errors/task_depend_parent.tjp +11 -0
- data/test/TestSuite/Scheduler/Errors/task_depend_self.tjp +10 -0
- data/test/TestSuite/Scheduler/Errors/task_depend_unknown.tjp +10 -0
- data/test/TestSuite/Scheduler/Errors/task_overspecified_1.tjp +9 -0
- data/test/TestSuite/Scheduler/Errors/task_overspecified_2.tjp +14 -0
- data/test/TestSuite/Scheduler/Errors/task_overspecified_3.tjp +14 -0
- data/test/TestSuite/Scheduler/Errors/task_underspecified_1.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/task_underspecified_2.tjp +8 -0
- data/test/TestSuite/Scheduler/Errors/task_underspecified_3.tjp +9 -0
- data/test/TestSuite/Syntax/Correct/Account.tjp +53 -0
- data/test/TestSuite/Syntax/Correct/Allocate-1.tjp +24 -0
- data/test/TestSuite/Syntax/Correct/Alternative.tjp +13 -0
- data/test/TestSuite/Syntax/Correct/AutoMacros.tjp +14 -0
- data/test/TestSuite/Syntax/Correct/Booking.tjp +26 -0
- data/test/TestSuite/Syntax/Correct/Caption.tjp +33 -0
- data/test/TestSuite/Syntax/Correct/Celltext.tjp +28 -0
- data/test/TestSuite/Syntax/Correct/Comments.tjp +29 -0
- data/test/TestSuite/Syntax/Correct/Complete.tjp +16 -0
- data/test/TestSuite/Syntax/Correct/CompletedWork.tji +20 -0
- data/test/TestSuite/Syntax/Correct/CriticalPath.tjp +31 -0
- data/test/TestSuite/Syntax/Correct/Currencyformat.tjp +12 -0
- data/test/TestSuite/Syntax/Correct/CustomAttributes.tjp +14 -0
- data/test/TestSuite/Syntax/Correct/Depends1.tjp +22 -0
- data/test/TestSuite/Syntax/Correct/Durations.tjp +29 -0
- data/test/TestSuite/Syntax/Correct/Efficiency.tjp +19 -0
- data/test/TestSuite/Syntax/Correct/Export.tjp +40 -0
- data/test/TestSuite/Syntax/Correct/Flags.tjp +32 -0
- data/test/TestSuite/Syntax/Correct/Freeze.tjp +28 -0
- data/test/TestSuite/Syntax/Correct/Gap.tjp +15 -0
- data/test/TestSuite/Syntax/Correct/HtmlTaskReport.tjp +33 -0
- data/test/TestSuite/Syntax/Correct/Limits-1.tjp +71 -0
- data/test/TestSuite/Syntax/Correct/LoadUnits.tjp +31 -0
- data/test/TestSuite/Syntax/Correct/Macro-1.tjp +19 -0
- data/test/TestSuite/Syntax/Correct/Mandatory.tjp +17 -0
- data/test/TestSuite/Syntax/Correct/Milestone.tjp +12 -0
- data/test/TestSuite/Syntax/Correct/MinMax.tjp +17 -0
- data/test/TestSuite/Syntax/Correct/Numberformat.tjp +12 -0
- data/test/TestSuite/Syntax/Correct/Period.tjp +16 -0
- data/test/TestSuite/Syntax/Correct/Persistent.tjp +11 -0
- data/test/TestSuite/Syntax/Correct/Precedes1.tjp +17 -0
- data/test/TestSuite/Syntax/Correct/Priority.tjp +30 -0
- data/test/TestSuite/Syntax/Correct/Project.tjp +21 -0
- data/test/TestSuite/Syntax/Correct/ProjectIDs.tjp +23 -0
- data/test/TestSuite/Syntax/Correct/RawHTML.tjp +29 -0
- data/test/TestSuite/Syntax/Correct/Reports.tjp +54 -0
- data/test/TestSuite/Syntax/Correct/Resource.tjp +20 -0
- data/test/TestSuite/Syntax/Correct/Responsible.tjp +16 -0
- data/test/TestSuite/Syntax/Correct/Scenario.tjp +15 -0
- data/test/TestSuite/Syntax/Correct/Scheduling.tjp +26 -0
- data/test/TestSuite/Syntax/Correct/Select.tjp +27 -0
- data/test/TestSuite/Syntax/Correct/Shift.tjp +55 -0
- data/test/TestSuite/Syntax/Correct/Simple.tjp +25 -0
- data/test/TestSuite/Syntax/Correct/String.tjp +12 -0
- data/test/TestSuite/Syntax/Correct/Supplement.tjp +24 -0
- data/test/TestSuite/Syntax/Correct/TaskRoot.tjp +34 -0
- data/test/TestSuite/Syntax/Correct/TimeFrame.tjp +19 -0
- data/test/TestSuite/Syntax/Correct/Timezone.tjp +8 -0
- data/test/TestSuite/Syntax/Correct/Vacation.tjp +33 -0
- data/test/TestSuite/Syntax/Correct/csvtest +16 -0
- data/test/TestSuite/Syntax/Correct/manual2example.rb +24 -0
- data/test/TestSuite/Syntax/Correct/tutorial.tjp +485 -0
- data/test/TestSuite/Syntax/Errors/bad_include.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/booking_group.tjp +14 -0
- data/test/TestSuite/Syntax/Errors/booking_milestone.tjp +13 -0
- data/test/TestSuite/Syntax/Errors/booking_no_leaf.tjp +13 -0
- data/test/TestSuite/Syntax/Errors/chargeset.tjp +14 -0
- data/test/TestSuite/Syntax/Errors/chargeset_master.tjp +15 -0
- data/test/TestSuite/Syntax/Errors/container_attribute.tjp +13 -0
- data/test/TestSuite/Syntax/Errors/cost_acct_no_top.tjp +24 -0
- data/test/TestSuite/Syntax/Errors/cost_rev_same.tjp +24 -0
- data/test/TestSuite/Syntax/Errors/date_in_range.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/effort_zero.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/empty.tjp +1 -0
- data/test/TestSuite/Syntax/Errors/export_bad_extn.tjp +9 -0
- data/test/TestSuite/Syntax/Errors/extend_id_cap.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/interval_end_in_range.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/interval_start_in_range.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/leaf_resource_id_expected.tjp +12 -0
- data/test/TestSuite/Syntax/Errors/misaligned_date.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/no_csv_suffix.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/no_html_suffix.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/operand_attribute.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/operand_unkn_flag.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/operand_unkn_scen.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/overtime_range.tjp +13 -0
- data/test/TestSuite/Syntax/Errors/purge_no_list.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/purge_unknown_id.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/report_end.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/report_redifinition.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/report_start.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/resource_exists.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/resource_id_expected.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/rev_acct_no_top.tjp +24 -0
- data/test/TestSuite/Syntax/Errors/scenario_exists.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/shift_assignment_overlap.tjp +15 -0
- data/test/TestSuite/Syntax/Errors/shift_exists.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/shift_id_expected.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/sloppy_range.tjp +13 -0
- data/test/TestSuite/Syntax/Errors/sort_direction.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/sort_unknown_scen.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/sorting_crit_exptd1.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/sorting_crit_exptd2.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/sorting_wbs.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/start_before_end1.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/start_before_end2.tjp +6 -0
- data/test/TestSuite/Syntax/Errors/task_complete.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/task_exists.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/task_priority.tjp +8 -0
- data/test/TestSuite/Syntax/Errors/task_without_chargeset.tjp +9 -0
- data/test/TestSuite/Syntax/Errors/time_interval.tjp +12 -0
- data/test/TestSuite/Syntax/Errors/too_many_bangs.tjp +10 -0
- data/test/TestSuite/Syntax/Errors/undecl_flag.tjp +6 -0
- data/test/TestSuite/Syntax/Errors/unknown_projectid.tjp +7 -0
- data/test/TestSuite/Syntax/Errors/unknown_scenario_id.tjp +6 -0
- data/test/TestSuite/Syntax/Errors/unknown_scenario_idx.tjp +11 -0
- data/test/TestSuite/Syntax/Errors/unknown_task.tjp +10 -0
- data/test/all.rb +31 -0
- data/test/test_BatchProcessor.rb +54 -0
- data/test/test_CSV-Reports.rb +101 -0
- data/test/test_Limits.rb +104 -0
- data/test/test_LogicalExpression.rb +110 -0
- data/test/test_MacroTable.rb +51 -0
- data/test/test_Project.rb +57 -0
- data/test/test_PropertySet.rb +71 -0
- data/test/test_Query.rb +83 -0
- data/test/test_RealFormat.rb +83 -0
- data/test/test_RichText.rb +869 -0
- data/test/test_Scheduler.rb +42 -0
- data/test/test_ShiftAssignments.rb +77 -0
- data/test/test_Syntax.rb +41 -0
- data/test/test_TextScanner.rb +95 -0
- data/test/test_TjTime.rb +114 -0
- data/test/test_TjpExample.rb +169 -0
- data/test/test_UTF8String.rb +84 -0
- data/test/test_WorkingHours.rb +56 -0
- metadata +649 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env ruby -w
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
#
|
|
4
|
+
# = TableOfContents.rb -- The TaskJuggler III Project Management Software
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org>
|
|
7
|
+
#
|
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
|
10
|
+
# published by the Free Software Foundation.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
require 'XMLElement'
|
|
14
|
+
require 'TOCEntry'
|
|
15
|
+
|
|
16
|
+
class TaskJuggler
|
|
17
|
+
|
|
18
|
+
# This class can be used to store a table of contents. It's just an Array of
|
|
19
|
+
# TOCEntry objects. Each TOCEntry objects represents the title of a section.
|
|
20
|
+
class TableOfContents
|
|
21
|
+
|
|
22
|
+
# Create an empty TableOfContents object.
|
|
23
|
+
def initialize
|
|
24
|
+
@entries = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# This method must be used to add new TOCEntry objects to the
|
|
28
|
+
# TableOfContents. _entry_ must be a TOCEntry object reference.
|
|
29
|
+
def addEntry(entry)
|
|
30
|
+
@entries << entry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Return HTML elements that represent the content of the TableOfContents
|
|
34
|
+
# object. The result is a tree of XMLElement objects.
|
|
35
|
+
def to_html
|
|
36
|
+
div = XMLElement.new('div', 'style' => 'margin-left:15%; margin-right:15%;')
|
|
37
|
+
div << (table = XMLElement.new('table'))
|
|
38
|
+
@entries.each { |e| table << e.to_html }
|
|
39
|
+
|
|
40
|
+
div
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
data/lib/Task.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby -w
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
#
|
|
4
|
+
# = Task.rb -- The TaskJuggler III Project Management Software
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org>
|
|
7
|
+
#
|
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
|
10
|
+
# published by the Free Software Foundation.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
require 'PropertyTreeNode'
|
|
14
|
+
require 'TaskScenario'
|
|
15
|
+
|
|
16
|
+
class TaskJuggler
|
|
17
|
+
|
|
18
|
+
class Task < PropertyTreeNode
|
|
19
|
+
|
|
20
|
+
def initialize(project, id, name, parent)
|
|
21
|
+
super(project.tasks, id, name, parent)
|
|
22
|
+
project.addTask(self)
|
|
23
|
+
|
|
24
|
+
@data = Array.new(@project.scenarioCount, nil)
|
|
25
|
+
@project.scenarioCount.times do |i|
|
|
26
|
+
@data[i] = TaskScenario.new(self, i, @scenarioAttributes[i])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def readyForScheduling?(scenarioIdx)
|
|
31
|
+
@data[scenarioIdx].readyForScheduling?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby -w
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
#
|
|
4
|
+
# = TaskDependency.rb -- The TaskJuggler III Project Management Software
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org>
|
|
7
|
+
#
|
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
|
10
|
+
# published by the Free Software Foundation.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
class TaskJuggler
|
|
14
|
+
|
|
15
|
+
class TaskDependency
|
|
16
|
+
|
|
17
|
+
attr_accessor :onEnd, :gapDuration, :gapLength
|
|
18
|
+
attr_reader :taskId, :task
|
|
19
|
+
|
|
20
|
+
def initialize(taskId, onEnd)
|
|
21
|
+
@taskId = taskId
|
|
22
|
+
@task = nil
|
|
23
|
+
# Specifies whether the dependency is relative to the start or the
|
|
24
|
+
# end of the dependent task.
|
|
25
|
+
@onEnd = onEnd
|
|
26
|
+
# The gap duration is stored in seconds of calendar time.
|
|
27
|
+
@gapDuration = 0
|
|
28
|
+
# The gap length is stored in number of scheduling slots.
|
|
29
|
+
@gapLength = 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve(project)
|
|
33
|
+
@task = project.task(@taskId)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
|
data/lib/TaskJuggler.rb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env ruby -w
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
#
|
|
4
|
+
# = TaskJuggler.rb -- The TaskJuggler III Project Management Software
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org>
|
|
7
|
+
#
|
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
|
10
|
+
# published by the Free Software Foundation.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
require 'Project'
|
|
14
|
+
require 'MessageHandler'
|
|
15
|
+
require 'Log'
|
|
16
|
+
|
|
17
|
+
# The TaskJuggler class models the object that provides access to the
|
|
18
|
+
# fundamental features of the TaskJuggler software. It can read project
|
|
19
|
+
# files, schedule them and generate the reports.
|
|
20
|
+
class TaskJuggler
|
|
21
|
+
|
|
22
|
+
attr_reader :messageHandler
|
|
23
|
+
attr_accessor :maxCpuCores
|
|
24
|
+
|
|
25
|
+
# Create a new TaskJuggler object. _console_ is a boolean that determines
|
|
26
|
+
# whether or not messsages can be written to $stderr.
|
|
27
|
+
def initialize(console)
|
|
28
|
+
@project = nil
|
|
29
|
+
@messageHandler = MessageHandler.new(console)
|
|
30
|
+
@maxCpuCores = 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Read in the files passed as file names in _files_, parse them and
|
|
34
|
+
# construct a Project object. In case of success true is returned.
|
|
35
|
+
# Otherwise false.
|
|
36
|
+
def parse(files)
|
|
37
|
+
Log.enter('parser', 'Parsing files ...')
|
|
38
|
+
master = true
|
|
39
|
+
@project = nil
|
|
40
|
+
|
|
41
|
+
parser = ProjectFileParser.new(@messageHandler)
|
|
42
|
+
files.each do |file|
|
|
43
|
+
begin
|
|
44
|
+
parser.open(file)
|
|
45
|
+
rescue TjException
|
|
46
|
+
Log.exit('parser')
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
if master
|
|
50
|
+
@project = parser.parse('project')
|
|
51
|
+
master = false
|
|
52
|
+
else
|
|
53
|
+
parser.setGlobalMacros
|
|
54
|
+
parser.parse('properties')
|
|
55
|
+
end
|
|
56
|
+
parser.close
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Log.exit('parser')
|
|
60
|
+
@messageHandler.messages.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Schedule all scenarios in the project. Return true if no error was
|
|
64
|
+
# detected, false otherwise.
|
|
65
|
+
def schedule
|
|
66
|
+
Log.enter('scheduler', 'Scheduling project ...')
|
|
67
|
+
#puts @project.to_s
|
|
68
|
+
res = @project.schedule
|
|
69
|
+
Log.exit('scheduler')
|
|
70
|
+
res
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Generate all specified reports. The project must have be scheduled before
|
|
74
|
+
# this method can be called. It returns true if no error occured, false
|
|
75
|
+
# otherwise.
|
|
76
|
+
def generateReports
|
|
77
|
+
Log.enter('reports', 'Generating reports ...')
|
|
78
|
+
res = @project.generateReports(@maxCpuCores)
|
|
79
|
+
Log.exit('reports')
|
|
80
|
+
res
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
data/lib/TaskScenario.rb
ADDED
|
@@ -0,0 +1,1622 @@
|
|
|
1
|
+
#!/usr/bin/env ruby -w
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
#
|
|
4
|
+
# = TaskScenario.rb -- The TaskJuggler III Project Management Software
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org>
|
|
7
|
+
#
|
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
|
10
|
+
# published by the Free Software Foundation.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
require 'ScenarioData'
|
|
14
|
+
|
|
15
|
+
class TaskJuggler
|
|
16
|
+
|
|
17
|
+
class TaskScenario < ScenarioData
|
|
18
|
+
|
|
19
|
+
attr_reader :isRunAway
|
|
20
|
+
|
|
21
|
+
# Create a new TaskScenario object.
|
|
22
|
+
def initialize(task, scenarioIdx, attributes)
|
|
23
|
+
super
|
|
24
|
+
|
|
25
|
+
# A list of all allocated leaf resources.
|
|
26
|
+
@candidates = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Call this function to reset all scheduling related data prior to
|
|
30
|
+
# scheduling.
|
|
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
|
+
# Collect the limits of this task and all parent tasks into a single
|
|
68
|
+
# Array.
|
|
69
|
+
@limits = []
|
|
70
|
+
task = @property
|
|
71
|
+
# Reset the counters of all limits of this task.
|
|
72
|
+
task['limits', @scenarioIdx].reset if task['limits', @scenarioIdx]
|
|
73
|
+
until task.nil?
|
|
74
|
+
if task['limits', @scenarioIdx]
|
|
75
|
+
@limits << task['limits', @scenarioIdx]
|
|
76
|
+
end
|
|
77
|
+
task = task.parent
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Collect the mandatory allocations.
|
|
81
|
+
@mandatories = []
|
|
82
|
+
a('allocate').each do |allocation|
|
|
83
|
+
@mandatories << allocation if allocation.mandatory
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
bookBookings
|
|
87
|
+
markMilestone
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The parser only stores the full task IDs for each of the dependencies. This
|
|
91
|
+
# function resolves them to task references and checks them. In addition
|
|
92
|
+
# to the 'depends' and 'precedes' property lists we also keep 4 additional
|
|
93
|
+
# lists.
|
|
94
|
+
# startpreds: All precedessors to the start of this task
|
|
95
|
+
# startsuccs: All successors to the start of this task
|
|
96
|
+
# endpreds: All predecessors to the end of this task
|
|
97
|
+
# endsuccs: All successors to the end of this task
|
|
98
|
+
# Each list element consists of a reference/boolean pair. The reference
|
|
99
|
+
# points to the dependent task and the boolean specifies whether the
|
|
100
|
+
# dependency originates from the end of the task or not.
|
|
101
|
+
def Xref
|
|
102
|
+
@property['depends', @scenarioIdx].each do |dependency|
|
|
103
|
+
depTask = checkDependency(dependency, 'depends')
|
|
104
|
+
a('startpreds').push([ depTask, dependency.onEnd ])
|
|
105
|
+
depTask[dependency.onEnd ? 'endsuccs' : 'startsuccs', @scenarioIdx].
|
|
106
|
+
push([ @property, false ])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@property['precedes', @scenarioIdx].each do |dependency|
|
|
110
|
+
predTask = checkDependency(dependency, 'precedes')
|
|
111
|
+
a('endsuccs').push([ predTask, dependency.onEnd ])
|
|
112
|
+
predTask[dependency.onEnd ? 'endpreds' : 'startpreds', @scenarioIdx].
|
|
113
|
+
push([@property, true ])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Return true of this Task has a dependency [ _target_, _onEnd_ ] in the
|
|
118
|
+
# dependency category _depType_.
|
|
119
|
+
def hasDependency?(depType, target, onEnd)
|
|
120
|
+
a(depType).include?([target, onEnd])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def propagateInitialValues
|
|
124
|
+
unless @startPropagated
|
|
125
|
+
if a('start')
|
|
126
|
+
propagateDate(a('start'), false)
|
|
127
|
+
elsif @property.parent.nil? &&
|
|
128
|
+
@property.canInheritDate?(@scenarioIdx, false)
|
|
129
|
+
propagateDate(@project['start'], false)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless @endPropagated
|
|
134
|
+
if a('end')
|
|
135
|
+
propagateDate(a('end'), true)
|
|
136
|
+
elsif @property.parent.nil? &&
|
|
137
|
+
@property.canInheritDate?(@scenarioIdx, true)
|
|
138
|
+
propagateDate(@project['end'], true)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Before the actual scheduling work can be started, we need to do a few
|
|
144
|
+
# consistency checks on the task.
|
|
145
|
+
def preScheduleCheck
|
|
146
|
+
# Accounts can have sub accounts added after being used in a chargetset.
|
|
147
|
+
# So we need to re-test here.
|
|
148
|
+
a('chargeset').each do |chargeset|
|
|
149
|
+
chargeset.each do |account, share|
|
|
150
|
+
unless account.leaf?
|
|
151
|
+
error('account_no_leaf',
|
|
152
|
+
"Chargesets may not include group account #{account.fullId}.")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Leaf tasks can be turned into containers after bookings have been added.
|
|
158
|
+
# We need to check for this.
|
|
159
|
+
unless @property.leaf? || a('booking').empty?
|
|
160
|
+
error('container_booking',
|
|
161
|
+
"Container task #{@property.fullId} may not have bookings.")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Milestones may not have bookings.
|
|
165
|
+
if a('milestone') && !a('booking').empty?
|
|
166
|
+
error('milestone_booking',
|
|
167
|
+
"Milestone #{@property.fullId} may not have bookings.")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# All 'scheduled' tasks must have a fixed start and end date.
|
|
171
|
+
if a('scheduled') && (a('start').nil? || a('end').nil?)
|
|
172
|
+
error('not_scheduled',
|
|
173
|
+
"Task #{@property.fullId} is marked as scheduled but does not " +
|
|
174
|
+
'have a fixed start and end date.')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# If an effort has been specified resources must be allocated as well.
|
|
178
|
+
if a('effort') > 0 && a('allocate').empty?
|
|
179
|
+
error('effort_no_allocations',
|
|
180
|
+
"Task #{@property.fullId} has an effort but no allocations.")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
durationSpecs = 0
|
|
184
|
+
durationSpecs += 1 if a('effort') > 0
|
|
185
|
+
durationSpecs += 1 if a('length') > 0
|
|
186
|
+
durationSpecs += 1 if a('duration') > 0
|
|
187
|
+
durationSpecs += 1 if a('milestone')
|
|
188
|
+
|
|
189
|
+
# The rest of this function performs a number of plausibility tests with
|
|
190
|
+
# regards to task start and end critiria. To explain the various cases,
|
|
191
|
+
# the following symbols are used:
|
|
192
|
+
#
|
|
193
|
+
# |: fixed start or end date
|
|
194
|
+
# -: no fixed start or end date
|
|
195
|
+
# M: Milestone
|
|
196
|
+
# D: start or end dependency
|
|
197
|
+
# x->: ASAP task with duration criteria
|
|
198
|
+
# <-x: ALAP task with duration criteria
|
|
199
|
+
# -->: ASAP task without duration criteria
|
|
200
|
+
# <--: ALAP task without duration criteria
|
|
201
|
+
|
|
202
|
+
if @property.container?
|
|
203
|
+
if durationSpecs > 0
|
|
204
|
+
error('container_duration',
|
|
205
|
+
"Container task #{@property.fullId} may not have a duration " +
|
|
206
|
+
"or be marked as milestones.")
|
|
207
|
+
end
|
|
208
|
+
elsif a('milestone')
|
|
209
|
+
if durationSpecs > 1
|
|
210
|
+
error('milestone_duration',
|
|
211
|
+
"Milestone task #{@property.fullId} may not have a duration.")
|
|
212
|
+
end
|
|
213
|
+
# Milestones can have the following cases:
|
|
214
|
+
#
|
|
215
|
+
# | M - ok |D M - ok - M - err1 -D M - ok
|
|
216
|
+
# | M | err2 |D M | err2 - M | ok -D M | ok
|
|
217
|
+
# | M -D ok |D M -D ok - M -D ok -D M -D ok
|
|
218
|
+
# | M |D err2 |D M |D err2 - M |D ok -D M |D ok
|
|
219
|
+
|
|
220
|
+
# err1: no start and end
|
|
221
|
+
# already handled by 'start_undetermed' or 'end_undetermed'
|
|
222
|
+
|
|
223
|
+
# err2: differnt start and end dates
|
|
224
|
+
if a('start') && a('end') && a('start') != a('end')
|
|
225
|
+
error('milestone_start_end',
|
|
226
|
+
"Start (#{a('start')}) and end (#{a('end')}) dates of " +
|
|
227
|
+
"milestone task #{@property.fullId} must be identical.")
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
# Error table for non-container, non-milestone tasks:
|
|
231
|
+
# AMP: Automatic milestone promotion for underspecified tasks when
|
|
232
|
+
# no bookings or allocations are present.
|
|
233
|
+
# AMPi: Automatic milestone promotion when no bookings or
|
|
234
|
+
# allocations are present. When no bookings but allocations are
|
|
235
|
+
# present the task inherits start and end date.
|
|
236
|
+
# Ref. implicitXref()|
|
|
237
|
+
# inhS: Inherit start date from parent task or project
|
|
238
|
+
# inhE: Inherit end date from parent task or project
|
|
239
|
+
#
|
|
240
|
+
# | x-> - ok |D x-> - ok - x-> - inhS -D x-> - ok
|
|
241
|
+
# | x-> | err1 |D x-> | err1 - x-> | inhS -D x-> | err1
|
|
242
|
+
# | x-> -D ok |D x-> -D ok - x-> -D inhS -D x-> -D ok
|
|
243
|
+
# | x-> |D err1 |D x-> |D err1 - x-> |D inhS -D x-> |D err1
|
|
244
|
+
# | --> - AMP |D --> - AMP - --> - AMPi -D --> - AMP
|
|
245
|
+
# | --> | ok |D --> | ok - --> | inhS -D --> | ok
|
|
246
|
+
# | --> -D ok |D --> -D ok - --> -D inhS -D --> -D ok
|
|
247
|
+
# | --> |D ok |D --> |D ok - --> |D inhS -D --> |D ok
|
|
248
|
+
# | <-x - inhE |D <-x - inhE - <-x - inhE -D <-x - inhE
|
|
249
|
+
# | <-x | err1 |D <-x | err1 - <-x | ok -D <-x | ok
|
|
250
|
+
# | <-x -D err1 |D <-x -D err1 - <-x -D ok -D <-x -D ok
|
|
251
|
+
# | <-x |D err1 |D <-x |D err1 - <-x |D ok -D <-x |D ok
|
|
252
|
+
# | <-- - inhE |D <-- - inhE - <-- - AMP -D <-- - inhE
|
|
253
|
+
# | <-- | ok |D <-- | ok - <-- | AMP -D <-- | ok
|
|
254
|
+
# | <-- -D ok |D <-- -D ok - <-- -D AMP -D <-- -D ok
|
|
255
|
+
# | <-- |D ok |D <-- |D ok - <-- |D AMP -D <-- |D ok
|
|
256
|
+
|
|
257
|
+
# These cases are normally autopromoted to milestones or inherit their
|
|
258
|
+
# start or end dates. But this only works for tasks that have no
|
|
259
|
+
# allocations or bookings.
|
|
260
|
+
# - --> -
|
|
261
|
+
# | --> -
|
|
262
|
+
# |D --> -
|
|
263
|
+
# -D --> -
|
|
264
|
+
# - <-- -
|
|
265
|
+
# - <-- |
|
|
266
|
+
# - <-- -D
|
|
267
|
+
# - <-- |D
|
|
268
|
+
if durationSpecs == 0 && ((a('forward') && a('end').nil?) ||
|
|
269
|
+
(!a('forward') && a('start').nil?))
|
|
270
|
+
error('task_underspecified',
|
|
271
|
+
"Task #{@property.fullId} has too few specifations to be " +
|
|
272
|
+
"scheduled.")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# err1: Overspecified (12 cases)
|
|
276
|
+
# | x-> |
|
|
277
|
+
# | <-x |
|
|
278
|
+
# | x-> |D
|
|
279
|
+
# | <-x |D
|
|
280
|
+
# |D x-> |
|
|
281
|
+
# |D <-x |
|
|
282
|
+
# |D <-x |D
|
|
283
|
+
# |D x-> |D
|
|
284
|
+
# -D x-> |
|
|
285
|
+
# -D x-> |D
|
|
286
|
+
# |D <-x -D
|
|
287
|
+
# | <-x -D
|
|
288
|
+
if durationSpecs > 1
|
|
289
|
+
error('multiple_durations',
|
|
290
|
+
"Tasks may only have either a duration, length or effort or " +
|
|
291
|
+
"be a milestone.")
|
|
292
|
+
end
|
|
293
|
+
startSpeced = @property.provided('start', @scenarioIdx)
|
|
294
|
+
endSpeced = @property.provided('end', @scenarioIdx)
|
|
295
|
+
if ((startSpeced && endSpeced) ||
|
|
296
|
+
(hasDependencies(false) && a('forward') && endSpeced) ||
|
|
297
|
+
(hasDependencies(true) && !a('forward') && startSpeced)) &&
|
|
298
|
+
durationSpecs > 0 && !a('scheduled')
|
|
299
|
+
error('task_overspecified',
|
|
300
|
+
"Task #{@property.fullId} has a start, an end and a " +
|
|
301
|
+
'duration specification.')
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if !a('booking').empty? && !a('forward') && !a('scheduled')
|
|
306
|
+
error('alap_booking',
|
|
307
|
+
'A task scheduled in ALAP mode may only have bookings if it ' +
|
|
308
|
+
'has been marked as fully scheduled. Keep in mind that ' +
|
|
309
|
+
'certain attributes like \'end\' or \'precedes\' automatically ' +
|
|
310
|
+
'switch the task to ALAP mode.')
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# When the actual scheduling process has been completed, this function must
|
|
315
|
+
# be called to do some more housekeeping. It computes some derived data
|
|
316
|
+
# based on the just scheduled values.
|
|
317
|
+
def finishScheduling
|
|
318
|
+
calcCompletion
|
|
319
|
+
# This list is no longer needed, so let's save some memory. Set it to
|
|
320
|
+
# nil so we can detect accidental use.
|
|
321
|
+
@candidates = nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# This function is not essential but does perform a large number of
|
|
325
|
+
# consistency checks. It should be called after the scheduling run has been
|
|
326
|
+
# finished.
|
|
327
|
+
def postScheduleCheck
|
|
328
|
+
@errors = 0
|
|
329
|
+
@property.children.each do |task|
|
|
330
|
+
@errors += 1 unless task.postScheduleCheck(@scenarioIdx)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# There is no point to check the parent if the child(s) have errors.
|
|
334
|
+
return false if @errors > 0
|
|
335
|
+
|
|
336
|
+
# Same for runaway tasks. They have already been reported.
|
|
337
|
+
if @isRunAway
|
|
338
|
+
error('sched_runaway', "Some tasks did not fit into the project time " +
|
|
339
|
+
"frame.")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Make sure the task is marked complete
|
|
343
|
+
unless a('scheduled')
|
|
344
|
+
error('not_scheduled',
|
|
345
|
+
"Task #{@property.fullId} has not been marked as scheduled.")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# If the task has a follower or predecessor that is a runaway this task
|
|
349
|
+
# is also incomplete.
|
|
350
|
+
(a('startsuccs') + a('endsuccs')).each do |task, onEnd|
|
|
351
|
+
return false if task.isRunAway(@scenarioIdx)
|
|
352
|
+
end
|
|
353
|
+
(a('startpreds') + a('endpreds')).each do |task, onEnd|
|
|
354
|
+
return false if task.isRunAway(@scenarioIdx)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Check if the start time is ok
|
|
358
|
+
if a('start').nil?
|
|
359
|
+
error('task_start_undef',
|
|
360
|
+
"Task #{@property.fullId} has undefined start time")
|
|
361
|
+
end
|
|
362
|
+
if a('start') < @project['start'] || a('start') > @project['end']
|
|
363
|
+
error('task_start_range',
|
|
364
|
+
"The start time (#{a('start')}) of task #{@property.fullId} " +
|
|
365
|
+
"is outside the project interval (#{@project['start']} - " +
|
|
366
|
+
"#{@project['end']})")
|
|
367
|
+
end
|
|
368
|
+
if !a('minstart').nil? && a('start') < a('minstart')
|
|
369
|
+
warning('minstart',
|
|
370
|
+
"The start time (#{a('start')}) of task #{@property.fullId} " +
|
|
371
|
+
"is too early. Must be after #{a('minstart')}.")
|
|
372
|
+
end
|
|
373
|
+
if !a('maxstart').nil? && a('start') > a('maxstart')
|
|
374
|
+
warning('maxstart',
|
|
375
|
+
"The start time (#{a('start')}) of task #{@property.fullId} " +
|
|
376
|
+
"is too late. Must be before #{a('maxstart')}.")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Check if the end time is ok
|
|
380
|
+
error('task_end_undef',
|
|
381
|
+
"Task #{@property.fullId} has undefined end time") if a('end').nil?
|
|
382
|
+
if a('end') < @project['start'] || a('end') > @project['end']
|
|
383
|
+
error('task_end_range',
|
|
384
|
+
"The end time (#{a('end')}) of task #{@property.fullId} " +
|
|
385
|
+
"is outside the project interval (#{@project['start']} - " +
|
|
386
|
+
"#{@project['end']})")
|
|
387
|
+
end
|
|
388
|
+
if !a('minend').nil? && a('end') < a('minend')
|
|
389
|
+
warning('minend',
|
|
390
|
+
"The end time (#{a('end')}) of task #{@property.fullId} " +
|
|
391
|
+
"is too early. Must be after #{a('minend')}.")
|
|
392
|
+
end
|
|
393
|
+
if !a('maxend').nil? && a('end') > a('maxend')
|
|
394
|
+
warning('maxend',
|
|
395
|
+
"The end time (#{a('end')}) of task #{@property.fullId} " +
|
|
396
|
+
"is too late. Must be before #{a('maxend')}.")
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Check that tasks fits into parent task.
|
|
400
|
+
unless (parent = @property.parent).nil? ||
|
|
401
|
+
parent['start', @scenarioIdx].nil? ||
|
|
402
|
+
parent['end', @scenarioIdx].nil?
|
|
403
|
+
if a('start') < parent['start', @scenarioIdx]
|
|
404
|
+
error('task_start_in_parent',
|
|
405
|
+
"The start date (#{a('start')}) of task #{@property.fullId} " +
|
|
406
|
+
"is before the start date of the enclosing task " +
|
|
407
|
+
"#{parent['start', @scenarioIdx]}. ")
|
|
408
|
+
end
|
|
409
|
+
if a('end') > parent['end', @scenarioIdx]
|
|
410
|
+
error('task_end_in_parent',
|
|
411
|
+
"The end date (#{a('end')}) of task #{@property.fullId} " +
|
|
412
|
+
"is after the end date of the enclosing task " +
|
|
413
|
+
"#{parent['end', @scenarioIdx]}. ")
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Check that all preceding tasks start/end before this task.
|
|
418
|
+
@property['depends', @scenarioIdx].each do |dependency|
|
|
419
|
+
task = dependency.task
|
|
420
|
+
limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx]
|
|
421
|
+
next if limit.nil?
|
|
422
|
+
if limit > a('start')
|
|
423
|
+
error('task_pred_before',
|
|
424
|
+
"Task #{@property.fullId} must start after " +
|
|
425
|
+
"#{dependency.onEnd ? 'end' : 'start'} of task " +
|
|
426
|
+
"#{@property.fullId}.")
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Check that all following tasks end before this task
|
|
431
|
+
@property['precedes', @scenarioIdx].each do |dependency|
|
|
432
|
+
task = dependency.task
|
|
433
|
+
limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx]
|
|
434
|
+
next if limit.nil?
|
|
435
|
+
if limit < a('end')
|
|
436
|
+
error('task_succ_after',
|
|
437
|
+
"Task #{@property.fullId} must end before " +
|
|
438
|
+
"#{dependency.onEnd ? 'end' : 'start'} of task #{task.fullId}.")
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
if a('milestone') && a('start') != a('end')
|
|
443
|
+
error('milestone_times_equal',
|
|
444
|
+
"Milestone #{@property.fullId} must have identical start and " +
|
|
445
|
+
"end date.")
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
@errors == 0
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def resetLoopFlags
|
|
452
|
+
@deadEndFlags = Array.new(4, false)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def checkForLoops(path, atEnd, fromOutside)
|
|
456
|
+
# Check if we have been here before on this path.
|
|
457
|
+
if path.include?([ @property, atEnd ])
|
|
458
|
+
error('loop_detected', "Loop detected at #{atEnd ? 'end' : 'start'} " +
|
|
459
|
+
"of task #{@property.fullId}", false)
|
|
460
|
+
skip = true
|
|
461
|
+
path.each do |t, e|
|
|
462
|
+
if t == @property && e == atEnd
|
|
463
|
+
skip = false
|
|
464
|
+
next
|
|
465
|
+
end
|
|
466
|
+
next if skip
|
|
467
|
+
info("loop_at_#{e ? 'end' : 'start'}",
|
|
468
|
+
"Loop ctnd. at #{e ? 'end' : 'start'} of task #{t.fullId}", t)
|
|
469
|
+
end
|
|
470
|
+
error('loop_end', "Aborting")
|
|
471
|
+
end
|
|
472
|
+
# Used for debugging only
|
|
473
|
+
if false
|
|
474
|
+
pathText = ''
|
|
475
|
+
path.each do |t, e|
|
|
476
|
+
pathText += "#{t.fullId}(#{e ? 'end' : 'start'}) -> "
|
|
477
|
+
end
|
|
478
|
+
pathText += "#{@property.fullId}(#{atEnd ? 'end' : 'start'})"
|
|
479
|
+
puts pathText
|
|
480
|
+
end
|
|
481
|
+
return if @deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)]
|
|
482
|
+
path << [ @property, atEnd ]
|
|
483
|
+
|
|
484
|
+
# To find loops we have to traverse the graph in a certain order. When we
|
|
485
|
+
# enter a task we can either come from outside or inside. The following
|
|
486
|
+
# graph explains these definitions:
|
|
487
|
+
#
|
|
488
|
+
# | / \ |
|
|
489
|
+
# outside v / \ v outside
|
|
490
|
+
# +------------------------------+
|
|
491
|
+
# | / Task \ |
|
|
492
|
+
# -->| o <--- ---> o |<--
|
|
493
|
+
# |/ Start End \|
|
|
494
|
+
# /+------------------------------+\
|
|
495
|
+
# / ^ ^ \
|
|
496
|
+
# | inside |
|
|
497
|
+
#
|
|
498
|
+
# At the top we have the parent task. At the botton the child tasks.
|
|
499
|
+
# The horizontal arrors are start predecessors or end successors.
|
|
500
|
+
# As the graph is doubly-linked, we need to becareful to only find real
|
|
501
|
+
# loops. When coming from outside, we only continue to the inside and vice
|
|
502
|
+
# versa. Horizontal moves are only made when we are in a leaf task.
|
|
503
|
+
unless atEnd
|
|
504
|
+
if fromOutside
|
|
505
|
+
if @property.container?
|
|
506
|
+
#
|
|
507
|
+
# |
|
|
508
|
+
# v
|
|
509
|
+
# +--------
|
|
510
|
+
# -->| o--+
|
|
511
|
+
# +----|---
|
|
512
|
+
# |
|
|
513
|
+
# V
|
|
514
|
+
#
|
|
515
|
+
@property.children.each do |child|
|
|
516
|
+
child.checkForLoops(@scenarioIdx, path, false, true)
|
|
517
|
+
end
|
|
518
|
+
else
|
|
519
|
+
# |
|
|
520
|
+
# v
|
|
521
|
+
# +--------
|
|
522
|
+
# -->| o---->
|
|
523
|
+
# +--------
|
|
524
|
+
#
|
|
525
|
+
checkForLoops(path, true, false) # if a('forward')
|
|
526
|
+
end
|
|
527
|
+
else
|
|
528
|
+
if a('startpreds').empty?
|
|
529
|
+
#
|
|
530
|
+
# ^
|
|
531
|
+
# |
|
|
532
|
+
# +-|------
|
|
533
|
+
# | o <--
|
|
534
|
+
# +--------
|
|
535
|
+
# ^
|
|
536
|
+
# |
|
|
537
|
+
#
|
|
538
|
+
if @property.parent
|
|
539
|
+
@property.parent.checkForLoops(@scenarioIdx, path, false, false)
|
|
540
|
+
end
|
|
541
|
+
else
|
|
542
|
+
|
|
543
|
+
# +--------
|
|
544
|
+
# <---- o <--
|
|
545
|
+
# +--------
|
|
546
|
+
# ^
|
|
547
|
+
# |
|
|
548
|
+
#
|
|
549
|
+
a('startpreds').each do |task, targetEnd|
|
|
550
|
+
task.checkForLoops(@scenarioIdx, path, targetEnd, true)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
else
|
|
555
|
+
if fromOutside
|
|
556
|
+
if @property.container?
|
|
557
|
+
#
|
|
558
|
+
# |
|
|
559
|
+
# v
|
|
560
|
+
# --------+
|
|
561
|
+
# +--o |<--
|
|
562
|
+
# ---|----+
|
|
563
|
+
# |
|
|
564
|
+
# v
|
|
565
|
+
#
|
|
566
|
+
@property.children.each do |child|
|
|
567
|
+
child.checkForLoops(@scenarioIdx, path, true, true)
|
|
568
|
+
end
|
|
569
|
+
else
|
|
570
|
+
# |
|
|
571
|
+
# v
|
|
572
|
+
# --------+
|
|
573
|
+
# <----o |<--
|
|
574
|
+
# --------+
|
|
575
|
+
#
|
|
576
|
+
checkForLoops(path, false, false) # unless a('forward')
|
|
577
|
+
end
|
|
578
|
+
else
|
|
579
|
+
if a('endsuccs').empty?
|
|
580
|
+
#
|
|
581
|
+
# ^
|
|
582
|
+
# |
|
|
583
|
+
# ------|-+
|
|
584
|
+
# --> o |
|
|
585
|
+
# --------+
|
|
586
|
+
# ^
|
|
587
|
+
# |
|
|
588
|
+
#
|
|
589
|
+
if @property.parent
|
|
590
|
+
@property.parent.checkForLoops(@scenarioIdx, path, true, false)
|
|
591
|
+
end
|
|
592
|
+
else
|
|
593
|
+
# --------+
|
|
594
|
+
# --> o---->
|
|
595
|
+
# --------+
|
|
596
|
+
# ^
|
|
597
|
+
# |
|
|
598
|
+
#
|
|
599
|
+
a('endsuccs').each do |task, targetEnd|
|
|
600
|
+
task.checkForLoops(@scenarioIdx, path, targetEnd, true)
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
path.pop
|
|
607
|
+
@deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)] = true
|
|
608
|
+
# puts "Finished with #{@property.fullId} #{atEnd ? 'end' : 'start'} " +
|
|
609
|
+
# "#{fromOutside ? 'outside' : 'inside'}"
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# This function must be called before prepareScheduling(). It compiles the
|
|
613
|
+
# list of leaf resources that are allocated to this task.
|
|
614
|
+
def candidates
|
|
615
|
+
@candidates = []
|
|
616
|
+
a('allocate').each do |allocation|
|
|
617
|
+
allocation.candidates.each do |candidate|
|
|
618
|
+
candidate.allLeaves.each do |resource|
|
|
619
|
+
@candidates << resource unless @candidates.include?(resource)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
@candidates
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# This function does some prep work for other functions like
|
|
627
|
+
# calcCriticalness. It compiles a list of all allocated leaf resources and
|
|
628
|
+
# stores it in @candidates. It also adds the allocated effort to
|
|
629
|
+
# the 'alloctdeffort' counter of each resource.
|
|
630
|
+
def countResourceAllocations
|
|
631
|
+
return if @candidates.empty? || a('effort') <= 0
|
|
632
|
+
|
|
633
|
+
avgEffort = a('effort') / @candidates.length
|
|
634
|
+
@candidates.each do |resource|
|
|
635
|
+
resource['alloctdeffort', @scenarioIdx] += avgEffort
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Determine the criticalness of the individual task. This is a measure for
|
|
640
|
+
# the likelyhood that this task will get the resources that it needs to
|
|
641
|
+
# complete the effort. Tasks without effort are not cricital. The only
|
|
642
|
+
# exception are milestones which get an arbitrary value of 1.
|
|
643
|
+
def calcCriticalness
|
|
644
|
+
@property['criticalness', @scenarioIdx] = 0.0
|
|
645
|
+
@property['pathcriticalness', @scenarioIdx] = nil
|
|
646
|
+
|
|
647
|
+
# Users feel that milestones are somewhat important. So we use an
|
|
648
|
+
# arbitrary value larger than 0 for them.
|
|
649
|
+
@property['criticalness', @scenarioIdx] = 1.0 if a('milestone')
|
|
650
|
+
|
|
651
|
+
# Task without efforts of allocations are not critical.
|
|
652
|
+
return if a('effort') <= 0 || @candidates.empty?
|
|
653
|
+
|
|
654
|
+
# Determine the average criticalness of all allocated resources.
|
|
655
|
+
criticalness = 0.0
|
|
656
|
+
@candidates.each do |resource|
|
|
657
|
+
criticalness += resource['criticalness', @scenarioIdx]
|
|
658
|
+
end
|
|
659
|
+
criticalness /= @candidates.length
|
|
660
|
+
|
|
661
|
+
# The task criticalness is the product of effort and average resource
|
|
662
|
+
# criticalness.
|
|
663
|
+
@property['criticalness', @scenarioIdx] = a('effort') * criticalness
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# The path criticalness is a measure for the overall criticalness of the
|
|
667
|
+
# task taking the dependencies into account. The fact that a task is part
|
|
668
|
+
# of a chain of effort-based task raises all the task in the chain to a
|
|
669
|
+
# higher criticalness level than the individual tasks. In fact, the path
|
|
670
|
+
# criticalness of this chain is equal to the sum of the individual
|
|
671
|
+
# criticalnesses of the tasks.
|
|
672
|
+
def calcPathCriticalness(atEnd = false)
|
|
673
|
+
# If we have computed this already, just return the value. If we are only
|
|
674
|
+
# at the end of the task, we do not include the criticalness of this task
|
|
675
|
+
# as it is not really part of the path.
|
|
676
|
+
if a('pathcriticalness')
|
|
677
|
+
return a('pathcriticalness') - (atEnd ? 0 : a('criticalness'))
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
maxCriticalness = 0.0
|
|
681
|
+
|
|
682
|
+
if atEnd
|
|
683
|
+
# At the end, we only care about pathes through the successors of this
|
|
684
|
+
# task or its parent tasks.
|
|
685
|
+
if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness
|
|
686
|
+
maxCriticalness = criticalness
|
|
687
|
+
end
|
|
688
|
+
else
|
|
689
|
+
# At the start of the task, we have two options.
|
|
690
|
+
if @property.container?
|
|
691
|
+
# For container tasks, we ignore all dependencies and check the pathes
|
|
692
|
+
# through all the children.
|
|
693
|
+
@property.children.each do |task|
|
|
694
|
+
if (criticalness = task.calcPathCriticalness(@scenarioIdx, false)) >
|
|
695
|
+
maxCriticalness
|
|
696
|
+
maxCriticalness = criticalness
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
else
|
|
700
|
+
# For leaf tasks, we check all pathes through the start successors and
|
|
701
|
+
# then the pathes through the end successors of this task and all its
|
|
702
|
+
# parent tasks.
|
|
703
|
+
a('startsuccs').each do |task, onEnd|
|
|
704
|
+
if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) >
|
|
705
|
+
maxCriticalness
|
|
706
|
+
maxCriticalness = criticalness
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness
|
|
711
|
+
maxCriticalness = criticalness
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
maxCriticalness += a('criticalness')
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
@property['pathcriticalness', @scenarioIdx] = maxCriticalness
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Check if the task is ready to be scheduled. For this it needs to have at
|
|
722
|
+
# least one specified end date and a duration criteria or the other end
|
|
723
|
+
# date.
|
|
724
|
+
def readyForScheduling?
|
|
725
|
+
return false if a('scheduled') || @isRunAway
|
|
726
|
+
|
|
727
|
+
if a('forward')
|
|
728
|
+
return true if a('start') && (hasDurationSpec? || a('end'))
|
|
729
|
+
else
|
|
730
|
+
return true if a('end') && (hasDurationSpec? || a('start'))
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
false
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# This function is the entry point for the core scheduling algorithm. It
|
|
737
|
+
# schedules the task to completion. The function returns true if a start
|
|
738
|
+
# or end date has been determined and other tasks may be ready for
|
|
739
|
+
# scheduling now.
|
|
740
|
+
def schedule
|
|
741
|
+
# Is the task scheduled from start to end or vice versa?
|
|
742
|
+
forward = a('forward')
|
|
743
|
+
# The task may not excede the project interval.
|
|
744
|
+
limit = @project[forward ? 'end' : 'start']
|
|
745
|
+
# Number of seconds per time slot.
|
|
746
|
+
slotDuration = @project['scheduleGranularity']
|
|
747
|
+
slot = nextSlot(slotDuration)
|
|
748
|
+
|
|
749
|
+
# Schedule all time slots from slot in the scheduling direction until
|
|
750
|
+
# the task is completed or a problem has been found.
|
|
751
|
+
while !scheduleSlot(slot, slotDuration)
|
|
752
|
+
if forward
|
|
753
|
+
# The task is scheduled from start to end.
|
|
754
|
+
slot += slotDuration
|
|
755
|
+
if slot > limit
|
|
756
|
+
markAsRunaway
|
|
757
|
+
return false
|
|
758
|
+
end
|
|
759
|
+
else
|
|
760
|
+
# The task is scheduled from end to start.
|
|
761
|
+
slot -= slotDuration
|
|
762
|
+
if slot < limit
|
|
763
|
+
markAsRunaway
|
|
764
|
+
return false
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
true
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Set a new start or end date and propagate the value to all other
|
|
773
|
+
# task ends that have a direct dependency to this end of the task.
|
|
774
|
+
def propagateDate(date, atEnd)
|
|
775
|
+
thisEnd = atEnd ? 'end' : 'start'
|
|
776
|
+
otherEnd = atEnd ? 'start' : 'end'
|
|
777
|
+
|
|
778
|
+
# These flags are just used to avoid duplicate calls of this function
|
|
779
|
+
# during propagateInitialValues().
|
|
780
|
+
if atEnd
|
|
781
|
+
@endPropagated = true
|
|
782
|
+
else
|
|
783
|
+
@startPropagated = true
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# For leaf tasks, propagate start may set the date. Container task dates
|
|
787
|
+
# are only set in scheduleContainer().
|
|
788
|
+
@property[thisEnd, @scenarioIdx] = date if @property.leaf?
|
|
789
|
+
|
|
790
|
+
if a('milestone')
|
|
791
|
+
# Start and end date of a milestone are identical.
|
|
792
|
+
@property['scheduled', @scenarioIdx] = true
|
|
793
|
+
if a(otherEnd).nil?
|
|
794
|
+
propagateDate(a(thisEnd), !atEnd)
|
|
795
|
+
end
|
|
796
|
+
Log << "Milestone #{@property.fullId}: #{a('start')} -> #{a('end')}"
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Propagate date to all dependent tasks.
|
|
800
|
+
a(thisEnd + 'preds').each do |task, onEnd|
|
|
801
|
+
propagateDateToDep(task, onEnd)
|
|
802
|
+
end
|
|
803
|
+
a(thisEnd + 'succs').each do |task, onEnd|
|
|
804
|
+
propagateDateToDep(task, onEnd)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Propagate date to sub tasks which have only an implicit
|
|
808
|
+
# dependency on the parent task and no other criteria for this end of
|
|
809
|
+
# the task.
|
|
810
|
+
@property.children.each do |task|
|
|
811
|
+
if task.canInheritDate?(@scenarioIdx, atEnd)
|
|
812
|
+
task.propagateDate(@scenarioIdx, date, atEnd)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# The date propagation might have completed the date set of the enclosing
|
|
817
|
+
# containter task. If so, we can schedule it as well.
|
|
818
|
+
@property.parent.scheduleContainer(@scenarioIdx) if !@property.parent.nil?
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# This function determines if a task can inherit the start or end date
|
|
822
|
+
# from a parent task or the project time frame. +atEnd+ specifies whether
|
|
823
|
+
# the check should be done for the task end (true) or task start (false).
|
|
824
|
+
def canInheritDate?(atEnd)
|
|
825
|
+
# Inheriting a start or end date from the enclosing task or the project
|
|
826
|
+
# is allowed for the following scenarios:
|
|
827
|
+
# - --> - inherit start and end when no bookings but allocations
|
|
828
|
+
# present
|
|
829
|
+
# - <-- - dito
|
|
830
|
+
#
|
|
831
|
+
# - x-> - inhS
|
|
832
|
+
# - x-> | inhS
|
|
833
|
+
# - x-> -D inhS
|
|
834
|
+
# - x-> |D inhS
|
|
835
|
+
# - --> | inhS
|
|
836
|
+
# - --> -D inhS
|
|
837
|
+
# - --> |D inhS
|
|
838
|
+
# - <-- | inhS
|
|
839
|
+
# | --> - inhE
|
|
840
|
+
# | <-x - inhE
|
|
841
|
+
# |D <-x - inhE
|
|
842
|
+
# - <-x - inhE
|
|
843
|
+
# -D <-x - inhE
|
|
844
|
+
# | <-- - inhE
|
|
845
|
+
# |D <-- - inhE
|
|
846
|
+
# -D <-- - inhE
|
|
847
|
+
# Return false if we already have a date or if we have a dependency for
|
|
848
|
+
# this end.
|
|
849
|
+
thisEnd = atEnd ? 'end' : 'start'
|
|
850
|
+
hasThisDeps = !a(thisEnd + 'preds').empty? || !a(thisEnd + 'succs').empty?
|
|
851
|
+
hasThisSpec = a(thisEnd) || hasThisDeps
|
|
852
|
+
return false if hasThisSpec
|
|
853
|
+
|
|
854
|
+
# Containter task can inherit the date if they have no dependencies.
|
|
855
|
+
return true if @property.container?
|
|
856
|
+
|
|
857
|
+
thatEnd = atEnd ? 'start' : 'end'
|
|
858
|
+
hasThatDeps = !a(thatEnd + 'preds').empty? || !a(thatEnd + 'succs').empty?
|
|
859
|
+
hasThatSpec = a(thatEnd) || hasThatDeps
|
|
860
|
+
|
|
861
|
+
# Check for tasks that have no start and end spec, no duration spec but
|
|
862
|
+
# allocates. They can inherit the start and end date.
|
|
863
|
+
return true if hasThatSpec && !hasDurationSpec? && !a('allocate').empty?
|
|
864
|
+
|
|
865
|
+
if a('forward') ^ atEnd
|
|
866
|
+
# the scheduling direction is pointing away from this end
|
|
867
|
+
return true if hasDurationSpec? || !a('booking').empty?
|
|
868
|
+
|
|
869
|
+
return hasThatSpec
|
|
870
|
+
else
|
|
871
|
+
# the scheduling direction is pointing towards this end
|
|
872
|
+
return a(thatEnd) && !hasDurationSpec? &&
|
|
873
|
+
a('booking').empty? #&& a('allocate').empty?
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# Find the smallest possible interval that encloses all child tasks. Abort
|
|
878
|
+
# the operation if any of the child tasks are not yet scheduled.
|
|
879
|
+
def scheduleContainer
|
|
880
|
+
return if a('scheduled') || !@property.container?
|
|
881
|
+
|
|
882
|
+
nStart = nil
|
|
883
|
+
nEnd = nil
|
|
884
|
+
|
|
885
|
+
@property.children.each do |task|
|
|
886
|
+
# Abort if a child has not yet been scheduled.
|
|
887
|
+
return unless task['scheduled', @scenarioIdx]
|
|
888
|
+
|
|
889
|
+
if nStart.nil? || task['start', @scenarioIdx] < nStart
|
|
890
|
+
nStart = task['start', @scenarioIdx]
|
|
891
|
+
end
|
|
892
|
+
if nEnd.nil? || task['end', @scenarioIdx] > nEnd
|
|
893
|
+
nEnd = task['end', @scenarioIdx]
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
@property['scheduled', @scenarioIdx] = true
|
|
898
|
+
|
|
899
|
+
startSet = endSet = false
|
|
900
|
+
# Propagate the dates to other dependent tasks.
|
|
901
|
+
if a('start').nil? || a('start') > nStart
|
|
902
|
+
@property['start', @scenarioIdx] = nStart
|
|
903
|
+
startSet = true
|
|
904
|
+
end
|
|
905
|
+
if a('end').nil? || a('end') < nEnd
|
|
906
|
+
@property['end', @scenarioIdx] = nEnd
|
|
907
|
+
endSet = true
|
|
908
|
+
end
|
|
909
|
+
Log << "Container task #{@property.fullId}: #{a('start')} -> #{a('end')}"
|
|
910
|
+
|
|
911
|
+
# If we have modified the start or end date, we need to communicate this
|
|
912
|
+
# new date to surrounding tasks.
|
|
913
|
+
propagateDate(nStart, false) if startSet
|
|
914
|
+
propagateDate(nEnd, true) if endSet
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
# Return true if the task has a effort, length or duration setting.
|
|
918
|
+
def hasDurationSpec?
|
|
919
|
+
a('length') > 0 || a('duration') > 0 || a('effort') > 0 || a('milestone')
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# This function checks if the task has a dependency on another task or
|
|
923
|
+
# fixed date for a certain end. If +atEnd+ is true, the task end will be
|
|
924
|
+
# checked. Otherwise the start.
|
|
925
|
+
def hasDependencies(atEnd)
|
|
926
|
+
thisEnd = atEnd ? 'end' : 'start'
|
|
927
|
+
!a(thisEnd + 'succs').empty? || !a(thisEnd + 'preds').empty?
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def earliestStart
|
|
931
|
+
# puts "Finding earliest start date for #{@property.fullId}"
|
|
932
|
+
startDate = nil
|
|
933
|
+
a('depends').each do |dependency|
|
|
934
|
+
potentialStartDate =
|
|
935
|
+
dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx]
|
|
936
|
+
return nil if potentialStartDate.nil?
|
|
937
|
+
|
|
938
|
+
dateAfterLengthGap = potentialStartDate
|
|
939
|
+
gapLength = dependency.gapLength
|
|
940
|
+
while gapLength > 0 && dateAfterLengthGap < @project['end'] do
|
|
941
|
+
if @project.isWorkingTime(dateAfterLengthGap)
|
|
942
|
+
gapLength -= 1
|
|
943
|
+
end
|
|
944
|
+
dateAfterLengthGap += @project['scheduleGranularity']
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
if dateAfterLengthGap > potentialStartDate + dependency.gapDuration
|
|
948
|
+
potentialStartDate = dateAfterLengthGap
|
|
949
|
+
else
|
|
950
|
+
potentialStartDate += dependency.gapDuration
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
startDate = potentialStartDate if startDate.nil? ||
|
|
954
|
+
startDate < potentialStartDate
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
# If any of the parent tasks has an explicit start date, the task must
|
|
958
|
+
# start at or after this date.
|
|
959
|
+
task = @property
|
|
960
|
+
while (task = task.parent) do
|
|
961
|
+
if task['start', @scenarioIdx] &&
|
|
962
|
+
(startDate.nil? || task['start', @scenarioIdx] > startDate)
|
|
963
|
+
return task['start', @scenarioIdx]
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
startDate
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def latestEnd
|
|
971
|
+
# puts "Finding latest end date for #{@property.fullId}"
|
|
972
|
+
endDate = nil
|
|
973
|
+
a('precedes').each do |dependency|
|
|
974
|
+
potentialEndDate =
|
|
975
|
+
dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx]
|
|
976
|
+
return nil if potentialEndDate.nil?
|
|
977
|
+
|
|
978
|
+
dateBeforeLengthGap = potentialEndDate
|
|
979
|
+
gapLength = dependency.gapLength
|
|
980
|
+
while gapLength > 0 && dateBeforeLengthGap > @project['start'] do
|
|
981
|
+
if @project.isWorkingTime(dateBeforeLengthGap -
|
|
982
|
+
@project['scheduleGranularity'])
|
|
983
|
+
gapLength -= 1
|
|
984
|
+
end
|
|
985
|
+
dateBeforeLengthGap -= @project['scheduleGranularity']
|
|
986
|
+
end
|
|
987
|
+
if dateBeforeLengthGap < potentialEndDate - dependency.gapDuration
|
|
988
|
+
potentialEndDate = dateBeforeLengthGap
|
|
989
|
+
else
|
|
990
|
+
potentialEndDate -= dependency.gapDuration
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
endDate = potentialEndDate if endDate.nil? || endDate > potentialEndDate
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# If any of the parent tasks has an explicit end date, the task must end
|
|
997
|
+
# at or before this date.
|
|
998
|
+
task = @property
|
|
999
|
+
while (task = task.parent) do
|
|
1000
|
+
if task['end', @scenarioIdx] &&
|
|
1001
|
+
(endDate.nil? || task['end', @scenarioIdx] < endDate)
|
|
1002
|
+
return task['end', @scenarioIdx]
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
endDate
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def addBooking(booking)
|
|
1010
|
+
if a('booking').empty?
|
|
1011
|
+
# For the first item use the assignment form so that the 'provided'
|
|
1012
|
+
# attribute is set properly.
|
|
1013
|
+
@property['booking', @scenarioIdx] = [ booking ]
|
|
1014
|
+
else
|
|
1015
|
+
@property['booking', @scenarioIdx] << booking
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def markAsRunaway
|
|
1020
|
+
warning('runaway', "Task #{@property.fullId} does not fit into " +
|
|
1021
|
+
"project time frame")
|
|
1022
|
+
|
|
1023
|
+
@isRunAway = true
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def query_complete(query)
|
|
1027
|
+
if @property.leaf?
|
|
1028
|
+
query.sortableResult = query.numericalResult = a('complete').to_i
|
|
1029
|
+
query.result = "#{query.sortableResult}%"
|
|
1030
|
+
else
|
|
1031
|
+
query.result = ''
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
# Compute the cost generated by this Task for a given Account during a given
|
|
1036
|
+
# interval. If a Resource is provided as scopeProperty only the cost
|
|
1037
|
+
# directly generated by the resource is taken into account.
|
|
1038
|
+
def query_cost(query)
|
|
1039
|
+
if query.costAccount
|
|
1040
|
+
query.sortableResult = query.numericalResult =
|
|
1041
|
+
turnover(query.startIdx, query.endIdx, query.costAccount,
|
|
1042
|
+
query.scopeProperty)
|
|
1043
|
+
query.result = query.currencyFormat.format(query.sortableResult)
|
|
1044
|
+
else
|
|
1045
|
+
query.result = 'No cost account'
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
# The duration of the task. After scheduling, it can be determined for
|
|
1050
|
+
# all tasks. Also for those who did not have a 'duration' attribute.
|
|
1051
|
+
def query_duration(query)
|
|
1052
|
+
query.sortableResult = query.numericalResult =
|
|
1053
|
+
(a('end') - a('start')) / (60 * 60 * 24)
|
|
1054
|
+
query.result = query.scaleDuration(query.sortableResult)
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
# The effort allocated for the task in the specified interval. In case a
|
|
1058
|
+
# Resource is given as scope property only the effort allocated for this
|
|
1059
|
+
# resource is taken into account.
|
|
1060
|
+
def query_effort(query)
|
|
1061
|
+
query.sortableResult = query.numericalResult =
|
|
1062
|
+
getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty)
|
|
1063
|
+
query.result = query.scaleLoad(query.sortableResult)
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
# Compute the revenue generated by this Task for a given Account during a
|
|
1067
|
+
# given interval. If a Resource is provided as scopeProperty only the
|
|
1068
|
+
# revenue directly generated by the resource is taken into account.
|
|
1069
|
+
def query_revenue(query)
|
|
1070
|
+
if query.revenueAccount
|
|
1071
|
+
query.sortableResult = query.numericalResult =
|
|
1072
|
+
turnover(query.startIdx, query.endIdx, query.revenueAccount,
|
|
1073
|
+
query.scopeProperty)
|
|
1074
|
+
query.result = query.currencyFormat.format(query.sortableResult)
|
|
1075
|
+
else
|
|
1076
|
+
query.result = 'No revenue account'
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def getEffectiveWork(startIdx, endIdx, resource = nil)
|
|
1081
|
+
return 0.0 if a('milestone')
|
|
1082
|
+
|
|
1083
|
+
workLoad = 0.0
|
|
1084
|
+
if @property.container?
|
|
1085
|
+
@property.children.each do |task|
|
|
1086
|
+
workLoad += task.getEffectiveWork(@scenarioIdx, startIdx, endIdx,
|
|
1087
|
+
resource)
|
|
1088
|
+
end
|
|
1089
|
+
else
|
|
1090
|
+
if resource
|
|
1091
|
+
workLoad += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx,
|
|
1092
|
+
@property)
|
|
1093
|
+
else
|
|
1094
|
+
a('assignedresources').each do |r|
|
|
1095
|
+
workLoad += r.getEffectiveWork(@scenarioIdx, startIdx, endIdx,
|
|
1096
|
+
@property)
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
end
|
|
1100
|
+
workLoad
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Return a list of intervals that lay within _iv_ and are at least
|
|
1104
|
+
# minDuration long and contain no working time.
|
|
1105
|
+
def collectTimeOffIntervals(iv, minDuration)
|
|
1106
|
+
if a('shifts')
|
|
1107
|
+
a('shifts').collectTimeOffIntervals(iv, minDuration)
|
|
1108
|
+
else
|
|
1109
|
+
[]
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
private
|
|
1114
|
+
def scheduleSlot(slot, slotDuration)
|
|
1115
|
+
# Tasks must always be scheduled in a single contigous fashion. @lastSlot
|
|
1116
|
+
# indicates the slot that was used for the previous call. Depending on the
|
|
1117
|
+
# scheduling direction the next slot must be scheduled either right before
|
|
1118
|
+
# or after this slot. If the current slot is not directly aligned, we'll
|
|
1119
|
+
# wait for another call with a proper slot. The function returns true
|
|
1120
|
+
# only if a slot could be scheduled.
|
|
1121
|
+
if a('forward')
|
|
1122
|
+
# On first call, the @lastSlot is not set yet. We set it to the slot
|
|
1123
|
+
# before the start slot.
|
|
1124
|
+
if @lastSlot.nil?
|
|
1125
|
+
@lastSlot = a('start') - slotDuration
|
|
1126
|
+
@tentativeEnd = slot + slotDuration
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
return false unless slot == @lastSlot + slotDuration
|
|
1130
|
+
else
|
|
1131
|
+
# On first call, the @lastSlot is not set yet. We set it to the slot
|
|
1132
|
+
# to the end slot.
|
|
1133
|
+
if @lastSlot.nil?
|
|
1134
|
+
@lastSlot = a('end')
|
|
1135
|
+
@tentativeStart = slot
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
return false unless slot == @lastSlot - slotDuration
|
|
1139
|
+
end
|
|
1140
|
+
@lastSlot = slot
|
|
1141
|
+
|
|
1142
|
+
if a('length') > 0 || a('duration') > 0
|
|
1143
|
+
# The doneDuration counts the number of scheduled slots. It is increased
|
|
1144
|
+
# by one with every scheduled slot. The doneLength is only increased for
|
|
1145
|
+
# global working time slots.
|
|
1146
|
+
@doneDuration += 1
|
|
1147
|
+
if @project.isWorkingTime(slot, slot + slotDuration)
|
|
1148
|
+
@doneLength += 1
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# If we have reached the specified duration or lengths, we set the end
|
|
1152
|
+
# or start date and propagate the value to neighbouring tasks.
|
|
1153
|
+
if (a('length') > 0 && @doneLength >= a('length')) ||
|
|
1154
|
+
(a('duration') > 0 && @doneDuration >= a('duration'))
|
|
1155
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1156
|
+
if a('forward')
|
|
1157
|
+
propagateDate(slot + slotDuration, true)
|
|
1158
|
+
else
|
|
1159
|
+
propagateDate(slot, false)
|
|
1160
|
+
end
|
|
1161
|
+
return true
|
|
1162
|
+
end
|
|
1163
|
+
elsif a('effort') > 0
|
|
1164
|
+
bookResources(slot, slotDuration) if @doneEffort < a('effort')
|
|
1165
|
+
if @doneEffort >= a('effort')
|
|
1166
|
+
# The specified effort has been reached. The has been fully scheduled
|
|
1167
|
+
# now.
|
|
1168
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1169
|
+
if a('forward')
|
|
1170
|
+
propagateDate(@tentativeEnd, true)
|
|
1171
|
+
else
|
|
1172
|
+
propagateDate(@tentativeStart, false)
|
|
1173
|
+
end
|
|
1174
|
+
return true
|
|
1175
|
+
end
|
|
1176
|
+
elsif a('milestone')
|
|
1177
|
+
if a('forward')
|
|
1178
|
+
propagateDate(a('start'), true)
|
|
1179
|
+
else
|
|
1180
|
+
propagateDate(a('end'), false)
|
|
1181
|
+
end
|
|
1182
|
+
return true
|
|
1183
|
+
elsif a('start') && a('end')
|
|
1184
|
+
# Task with start and end date but no duration criteria
|
|
1185
|
+
if a('allocate').empty?
|
|
1186
|
+
# For start-end-tasks without allocation, we don't have to do
|
|
1187
|
+
# anything but to set the 'scheduled' flag.
|
|
1188
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1189
|
+
@property.parent.scheduleContainer(@scenarioIdx) if @property.parent
|
|
1190
|
+
return true
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
bookResources(slot, slotDuration)
|
|
1194
|
+
|
|
1195
|
+
# Depending on the scheduling direction we can mark the task as
|
|
1196
|
+
# scheduled once we have reached the other end.
|
|
1197
|
+
if (a('forward') && slot + slotDuration >= a('end')) ||
|
|
1198
|
+
(!a('forward') && slot <= a('start'))
|
|
1199
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1200
|
+
@property.parent.scheduleContainer(@scenarioIdx) if @property.parent
|
|
1201
|
+
return true
|
|
1202
|
+
end
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
false
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
# Return the date of the next slot this task wants to have scheduled. This
|
|
1209
|
+
# must either be the first slot ever or it must be directly adjecent to the
|
|
1210
|
+
# previous slot. If this task has not yet been scheduled at all, @lastSlot
|
|
1211
|
+
# is still nil. Otherwise it contains the date of the last scheduled slot.
|
|
1212
|
+
def nextSlot(slotDuration)
|
|
1213
|
+
return nil if a('scheduled') || @isRunAway
|
|
1214
|
+
|
|
1215
|
+
if a('forward')
|
|
1216
|
+
@lastSlot.nil? ? a('start') : @lastSlot + slotDuration
|
|
1217
|
+
else
|
|
1218
|
+
@lastSlot.nil? ? a('end') - slotDuration : @lastSlot - slotDuration
|
|
1219
|
+
end
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def bookResources(date, slotDuration)
|
|
1223
|
+
# In projection mode we do not allow bookings prior to the current date
|
|
1224
|
+
# for any task (in strict mode) or tasks which have user specified
|
|
1225
|
+
# bookings (sloppy mode).
|
|
1226
|
+
if @project.scenario(@scenarioIdx).get('projection') &&
|
|
1227
|
+
date < @project['now'] &&
|
|
1228
|
+
(@project.scenario(@scenarioIdx).get('strict') ||
|
|
1229
|
+
a('assignedresources').empty?)
|
|
1230
|
+
return
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
# If the task has shifts to limit the allocations, we check that we are
|
|
1234
|
+
# within a defined shift interval. If yes, we need to be on shift to
|
|
1235
|
+
# continue.
|
|
1236
|
+
if (shifts = a('shifts')) && shifts.assigned?(date)
|
|
1237
|
+
return if !shifts.onShift?(date)
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
# If the task has allocation limits we need to make sure that none of them
|
|
1241
|
+
# is already exceeded.
|
|
1242
|
+
@limits.each do |limit|
|
|
1243
|
+
return if !limit.ok?(date)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
sbIdx = @project.dateToIdx(date)
|
|
1247
|
+
|
|
1248
|
+
# We first have to make sure that if there are mandatory resources
|
|
1249
|
+
# that these are all available for the time slot.
|
|
1250
|
+
takenMandatories = []
|
|
1251
|
+
@mandatories.each do |allocation|
|
|
1252
|
+
return unless allocation.onShift?(date)
|
|
1253
|
+
|
|
1254
|
+
# For mandatory allocations with alternatives at least one of the
|
|
1255
|
+
# alternatives must be available.
|
|
1256
|
+
found = false
|
|
1257
|
+
allocation.candidates(@scenarioIdx).each do |candidate|
|
|
1258
|
+
# When a resource group is marked mandatory, all members of the
|
|
1259
|
+
# group must be available.
|
|
1260
|
+
allAvailable = true
|
|
1261
|
+
candidate.allLeaves.each do |resource|
|
|
1262
|
+
if !resource.available?(@scenarioIdx, sbIdx) ||
|
|
1263
|
+
takenMandatories.include?(resource)
|
|
1264
|
+
allAvailable = false
|
|
1265
|
+
break
|
|
1266
|
+
else
|
|
1267
|
+
takenMandatories << resource
|
|
1268
|
+
end
|
|
1269
|
+
end
|
|
1270
|
+
if allAvailable
|
|
1271
|
+
found = true
|
|
1272
|
+
break
|
|
1273
|
+
end
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
# At least one mandatory resource is not available. We cannot continue.
|
|
1277
|
+
return unless found
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
iv = Interval.new(date, date + slotDuration)
|
|
1281
|
+
a('allocate').each do |allocation|
|
|
1282
|
+
next unless allocation.onShift?(date)
|
|
1283
|
+
|
|
1284
|
+
# In case we have a persistent allocation we need to check if there is
|
|
1285
|
+
# already a locked resource and use it.
|
|
1286
|
+
if allocation.persistent && !allocation.lockedResource.nil?
|
|
1287
|
+
bookResource(allocation.lockedResource, sbIdx, date)
|
|
1288
|
+
else
|
|
1289
|
+
# If not, we create a list of candidates in the proper order and
|
|
1290
|
+
# assign the first one available.
|
|
1291
|
+
allocation.candidates(@scenarioIdx).each do |candidate|
|
|
1292
|
+
if bookResource(candidate, sbIdx, date)
|
|
1293
|
+
allocation.lockedResource = candidate
|
|
1294
|
+
break
|
|
1295
|
+
end
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
end
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def bookResource(resource, sbIdx, date)
|
|
1302
|
+
booked = false
|
|
1303
|
+
resource.allLeaves.each do |r|
|
|
1304
|
+
if r.book(@scenarioIdx, sbIdx, @property)
|
|
1305
|
+
|
|
1306
|
+
if a('assignedresources').empty?
|
|
1307
|
+
if a('forward')
|
|
1308
|
+
@property['start', @scenarioIdx] = @project.idxToDate(sbIdx)
|
|
1309
|
+
else
|
|
1310
|
+
@property['end', @scenarioIdx] = @project.idxToDate(sbIdx + 1)
|
|
1311
|
+
end
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
@tentativeStart = @project.idxToDate(sbIdx)
|
|
1315
|
+
@tentativeEnd = @project.idxToDate(sbIdx + 1)
|
|
1316
|
+
|
|
1317
|
+
@doneEffort += r['efficiency', @scenarioIdx]
|
|
1318
|
+
# Limits do not take efficiency into account. Limits are usage limits,
|
|
1319
|
+
# not effort limits.
|
|
1320
|
+
@limits.each do |limit|
|
|
1321
|
+
limit.inc(date)
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
unless a('assignedresources').include?(r)
|
|
1325
|
+
@property['assignedresources', @scenarioIdx] << r
|
|
1326
|
+
end
|
|
1327
|
+
booked = true
|
|
1328
|
+
end
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
booked
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
# Register the user provided bookings with the Resource scoreboards. A
|
|
1335
|
+
# booking describes the assignment of a Resource to a certain Task for a
|
|
1336
|
+
# specified Interval.
|
|
1337
|
+
def bookBookings
|
|
1338
|
+
a('booking').each do |booking|
|
|
1339
|
+
unless booking.resource.leaf?
|
|
1340
|
+
error('booking_resource_not_leaf',
|
|
1341
|
+
"Booked resources may not be group resources", true,
|
|
1342
|
+
booking.sourceFileInfo)
|
|
1343
|
+
end
|
|
1344
|
+
unless a('forward') || a('scheduled')
|
|
1345
|
+
error('booking_forward_only',
|
|
1346
|
+
"Only forward scheduled tasks may have booking statements.")
|
|
1347
|
+
end
|
|
1348
|
+
slotDuration = @project['scheduleGranularity']
|
|
1349
|
+
booking.intervals.each do |interval|
|
|
1350
|
+
startIdx = @project.dateToIdx(interval.start)
|
|
1351
|
+
date = interval.start
|
|
1352
|
+
endIdx = @project.dateToIdx(interval.end)
|
|
1353
|
+
tEnd = nil
|
|
1354
|
+
startIdx.upto(endIdx - 1) do |idx|
|
|
1355
|
+
tEnd = date + slotDuration
|
|
1356
|
+
if booking.resource.bookBooking(@scenarioIdx, idx, booking)
|
|
1357
|
+
# Booking was successful for this time slot.
|
|
1358
|
+
@doneEffort += booking.resource['efficiency', @scenarioIdx]
|
|
1359
|
+
|
|
1360
|
+
# Set start and lastSlot if appropriate. The task start will be
|
|
1361
|
+
# set to the begining of the first booked slot. The lastSlot
|
|
1362
|
+
# will be set to the last booked slot
|
|
1363
|
+
@lastSlot = date if @lastSlot.nil? || date > @lastSlot
|
|
1364
|
+
@tentativeEnd = tEnd if @tentativeEnd.nil? ||
|
|
1365
|
+
@tentativeEnd < tEnd
|
|
1366
|
+
@property['start', @scenarioIdx] = date if a('start').nil? ||
|
|
1367
|
+
date < a('start')
|
|
1368
|
+
|
|
1369
|
+
unless a('assignedresources').include?(booking.resource)
|
|
1370
|
+
@property['assignedresources', @scenarioIdx] << booking.resource
|
|
1371
|
+
end
|
|
1372
|
+
end
|
|
1373
|
+
if a('length') > 0 && @project.isWorkingTime(date, tEnd)
|
|
1374
|
+
# For tasks with a 'length' we track the covered work time and
|
|
1375
|
+
# set the task to 'scheduled' when we have enough length.
|
|
1376
|
+
@doneLength += 1
|
|
1377
|
+
if @doneLength >= a('length')
|
|
1378
|
+
@property['end', @scenarioIdx] = tEnd
|
|
1379
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1380
|
+
end
|
|
1381
|
+
end
|
|
1382
|
+
date = tEnd
|
|
1383
|
+
end
|
|
1384
|
+
if a('duration') > 0
|
|
1385
|
+
# For tasks with a 'duration' we track the covered duration and
|
|
1386
|
+
# set the task to 'scheduled' when we have enough duration.
|
|
1387
|
+
@doneDuration = ((@tentativeEnd - a('start')) /
|
|
1388
|
+
@project['scheduleGranularity']).to_i
|
|
1389
|
+
if @doneDuration >= a('duration')
|
|
1390
|
+
@property['end', @scenarioIdx] = tEnd
|
|
1391
|
+
@property['scheduled', @scenarioIdx] = true
|
|
1392
|
+
end
|
|
1393
|
+
end
|
|
1394
|
+
end
|
|
1395
|
+
end
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
# This function determines if a task is really a milestones and marks them
|
|
1399
|
+
# accordingly.
|
|
1400
|
+
def markMilestone
|
|
1401
|
+
return if @property.container? || hasDurationSpec? ||
|
|
1402
|
+
!a('booking').empty? || !a('allocate').empty?
|
|
1403
|
+
|
|
1404
|
+
# The following cases qualify for an automatic milestone promotion.
|
|
1405
|
+
# - --> -
|
|
1406
|
+
# | --> -
|
|
1407
|
+
# |D --> -
|
|
1408
|
+
# -D --> -
|
|
1409
|
+
# - <-- -
|
|
1410
|
+
# - <-- |
|
|
1411
|
+
# - <-- -D
|
|
1412
|
+
# - <-- |D
|
|
1413
|
+
hasStartSpec = !a('start').nil? || !a('depends').empty?
|
|
1414
|
+
hasEndSpec = !a('end').nil? || !a('precedes').empty?
|
|
1415
|
+
|
|
1416
|
+
@property['milestone', @scenarioIdx] =
|
|
1417
|
+
(hasStartSpec && a('forward') && !hasEndSpec) ||
|
|
1418
|
+
(!hasStartSpec && !a('forward') && hasEndSpec) ||
|
|
1419
|
+
(!hasStartSpec && !hasEndSpec)
|
|
1420
|
+
end
|
|
1421
|
+
|
|
1422
|
+
def checkDependency(dependency, depType)
|
|
1423
|
+
if (depTask = dependency.resolve(@project)).nil?
|
|
1424
|
+
# Remove the broken dependency. It could cause trouble later on.
|
|
1425
|
+
@property[depType, @scenarioIdx].delete(dependency)
|
|
1426
|
+
error('task_depend_unknown',
|
|
1427
|
+
"Task #{@property.fullId} has unknown #{depType} " +
|
|
1428
|
+
"#{dependency.taskId}")
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
if depTask == @property
|
|
1432
|
+
# Remove the broken dependency. It could cause trouble later on.
|
|
1433
|
+
@property[depType, @scenarioIdx].delete(dependency)
|
|
1434
|
+
error('task_depend_self', "Task #{@property.fullId} cannot " +
|
|
1435
|
+
"depend on self")
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
if depTask.isChildOf?(@property)
|
|
1439
|
+
# Remove the broken dependency. It could cause trouble later on.
|
|
1440
|
+
@property[depType, @scenarioIdx].delete(dependency)
|
|
1441
|
+
error('task_depend_child',
|
|
1442
|
+
"Task #{@property.fullId} cannot depend on child " +
|
|
1443
|
+
"#{depTask.fullId}")
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
if @property.isChildOf?(depTask)
|
|
1447
|
+
# Remove the broken dependency. It could cause trouble later on.
|
|
1448
|
+
@property[depType, @scenarioIdx].delete(dependency)
|
|
1449
|
+
error('task_depend_parent',
|
|
1450
|
+
"Task #{@property.fullId} cannot depend on parent " +
|
|
1451
|
+
"#{depTask.fullId}")
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
@property[depType, @scenarioIdx].each do |dep|
|
|
1455
|
+
if dep.task == depTask && dep != dependency
|
|
1456
|
+
# Remove the broken dependency. It could cause trouble later on.
|
|
1457
|
+
@property[depType, @scenarioIdx].delete(dependency)
|
|
1458
|
+
error('task_depend_multi',
|
|
1459
|
+
"No need to specify dependency #{depTask.fullId} multiple " +
|
|
1460
|
+
"times for task #{@property.fullId}.")
|
|
1461
|
+
end
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
depTask
|
|
1465
|
+
end
|
|
1466
|
+
|
|
1467
|
+
# Set @startIsDetermed or @endIsDetermed (depending on _setStart) to
|
|
1468
|
+
# _value_.
|
|
1469
|
+
def setDetermination(setStart, value)
|
|
1470
|
+
setStart ? @startIsDetermed = value : @endIsDetermed = value
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
# This function is called to propagate the start or end date of the
|
|
1474
|
+
# current task to a dependend Task +task+. If +atEnd+ is true, the date
|
|
1475
|
+
# should be propagated to the end of the +task+, otherwise to the start.
|
|
1476
|
+
def propagateDateToDep(task, atEnd)
|
|
1477
|
+
#puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId}"
|
|
1478
|
+
# Don't propagate if the task is already completely scheduled or is a
|
|
1479
|
+
# container.
|
|
1480
|
+
return if task['scheduled', @scenarioIdx] || task.container?
|
|
1481
|
+
|
|
1482
|
+
# Don't propagate if the task already has a date for that end.
|
|
1483
|
+
return unless task[atEnd ? 'end' : 'start', @scenarioIdx].nil?
|
|
1484
|
+
|
|
1485
|
+
# Don't propagate if the task has a duration or is a milestone and the
|
|
1486
|
+
# task end to set is in the scheduling direction.
|
|
1487
|
+
return if task.hasDurationSpec?(@scenarioIdx) &&
|
|
1488
|
+
!(atEnd ^ task['forward', @scenarioIdx])
|
|
1489
|
+
|
|
1490
|
+
# Check if all other dependencies for that task end have been determined
|
|
1491
|
+
# already and use the latest or earliest possible date. Don't propagate
|
|
1492
|
+
# if we don't have all dates yet.
|
|
1493
|
+
return if (nDate = (atEnd ? task.latestEnd(@scenarioIdx) :
|
|
1494
|
+
task.earliestStart(@scenarioIdx))).nil?
|
|
1495
|
+
|
|
1496
|
+
# Looks like it is ok to propagate the date.
|
|
1497
|
+
task.propagateDate(@scenarioIdx, nDate, atEnd)
|
|
1498
|
+
# puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId} done"
|
|
1499
|
+
end
|
|
1500
|
+
|
|
1501
|
+
# This is a helper function for calcPathCriticalness(). It computes the
|
|
1502
|
+
# larges criticalness of the pathes through the end-successors of this task
|
|
1503
|
+
# and all its parent tasks.
|
|
1504
|
+
def calcPathCriticalnessEndSuccs
|
|
1505
|
+
maxCriticalness = 0.0
|
|
1506
|
+
# Gather a list of all end-successors of this task and its parent task.
|
|
1507
|
+
tList = []
|
|
1508
|
+
p = @property
|
|
1509
|
+
while (p)
|
|
1510
|
+
p['endsuccs', @scenarioIdx].each do |task, onEnd|
|
|
1511
|
+
tList << [ task, onEnd ] unless tList.include?([ task, onEnd ])
|
|
1512
|
+
end
|
|
1513
|
+
p = p.parent
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1516
|
+
tList.each do |task, onEnd|
|
|
1517
|
+
if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) >
|
|
1518
|
+
maxCriticalness
|
|
1519
|
+
maxCriticalness = criticalness
|
|
1520
|
+
end
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
maxCriticalness
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
# Calculate the current completion degree for tasks that have no user
|
|
1527
|
+
# specified completion value.
|
|
1528
|
+
def calcCompletion
|
|
1529
|
+
# If the user provided a completion degree we are not touching it.
|
|
1530
|
+
if property.provided('complete')
|
|
1531
|
+
return
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
if a('start').nil? || a('end').nil?
|
|
1535
|
+
@property['complete', @scenarioIdx] = 0.0
|
|
1536
|
+
return
|
|
1537
|
+
end
|
|
1538
|
+
|
|
1539
|
+
if a('milestone')
|
|
1540
|
+
@property['complete', @scenarioIdx] =
|
|
1541
|
+
@property['end', @scenarioIdx] <= @project['now'] ? 100.0 : 0.0
|
|
1542
|
+
elsif @property.container?
|
|
1543
|
+
#TODO
|
|
1544
|
+
else
|
|
1545
|
+
# Normal leaf task
|
|
1546
|
+
if a('end') <= @project['now']
|
|
1547
|
+
# The task has ended already. It's 100% complete.
|
|
1548
|
+
completion = 100.0
|
|
1549
|
+
elsif @project['now'] <= a('start')
|
|
1550
|
+
# The task has not started yet. Its' 0% complete.
|
|
1551
|
+
completion = 0.0
|
|
1552
|
+
else
|
|
1553
|
+
# The task is in progress. Calculate the current completion degree.
|
|
1554
|
+
completion = ((@project['now'] - a('start')) /
|
|
1555
|
+
(a('end') - a('start'))) * 100.0
|
|
1556
|
+
end
|
|
1557
|
+
@property['complete', @scenarioIdx] = completion
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# Compute the turnover generated by this Task for a given Account _account_
|
|
1562
|
+
# during the interval specified by _startIdx_ and _endIdx_. These can either
|
|
1563
|
+
# be TjTime values or Scoreboard indexes. If a Resource _resource_ is given,
|
|
1564
|
+
# only the turnover directly generated by the resource is taken into
|
|
1565
|
+
# account.
|
|
1566
|
+
def turnover(startIdx, endIdx, account, resource = nil)
|
|
1567
|
+
amount = 0.0
|
|
1568
|
+
if @property.container?
|
|
1569
|
+
@property.children.each do |child|
|
|
1570
|
+
amount += child.turnover(@scenarioIdx, startIdx, endIdx, account,
|
|
1571
|
+
resource)
|
|
1572
|
+
end
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# If there are no chargeset defined for this task, we don't need to
|
|
1576
|
+
# compute the resource related or other cost.
|
|
1577
|
+
unless a('chargeset').empty?
|
|
1578
|
+
resourceCost = 0.0
|
|
1579
|
+
otherCost = 0.0
|
|
1580
|
+
|
|
1581
|
+
# Container tasks don't have resource cost.
|
|
1582
|
+
unless @property.container?
|
|
1583
|
+
if resource
|
|
1584
|
+
resourceCost = resource.cost(@scenarioIdx, startIdx, endIdx,
|
|
1585
|
+
@property)
|
|
1586
|
+
else
|
|
1587
|
+
a('assignedresources').each do |r|
|
|
1588
|
+
resourceCost += r.cost(@scenarioIdx, startIdx, endIdx, @property)
|
|
1589
|
+
end
|
|
1590
|
+
end
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1593
|
+
unless a('charge').empty?
|
|
1594
|
+
# Add one-time and periodic charges to the amount.
|
|
1595
|
+
startDate = startIdx.is_a?(TjTime) ? startIdx :
|
|
1596
|
+
@project.idxToDate(startIdx)
|
|
1597
|
+
endDate = endIdx.is_a?(TjTime) ? endIdx :
|
|
1598
|
+
@project.idxToDate(endIdx)
|
|
1599
|
+
iv = Interval.new(startDate, endDate)
|
|
1600
|
+
a('charge').each do |charge|
|
|
1601
|
+
otherCost += charge.turnover(iv)
|
|
1602
|
+
end
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
totalCost = resourceCost + otherCost
|
|
1606
|
+
# Now weight the total cost by the share of the account
|
|
1607
|
+
a('chargeset').each do |set|
|
|
1608
|
+
set.each do |accnt, share|
|
|
1609
|
+
if share > 0.0 && (accnt == account || accnt.isChildOf?(account))
|
|
1610
|
+
amount += totalCost * share
|
|
1611
|
+
end
|
|
1612
|
+
end
|
|
1613
|
+
end
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
amount
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
end
|
|
1620
|
+
|
|
1621
|
+
end
|
|
1622
|
+
|