The Niku report can be used to export resource allocation data for certain task groups in the Niku XOG format. This file can be read by the Clarity enterprise resource management software from Computer Associates. Since I don’t think this is a use case for many users, the implementation is somewhat of a hack. The report relies on 3 custom attributes that the user has to define in the project. Resources must be tagged with a ClarityRID and Tasks must have a ClarityPID and a ClarityPName. This file format works for our Clarity installation. I have no idea if it is even portable to other Clarity installations.
# File lib/reports/NikuReport.rb, line 55 55: def initialize(report) 56: super(report) 57: 58: # A Hash to store NikuProject objects by id 59: @projects = {} 60: 61: # A Hash to map ClarityRID to Resource 62: @resources = {} 63: 64: # Resources total effort during the report period hashed by ClarityId 65: @resourcesTotalEffort = {} 66: 67: @scenarioIdx = nil 68: end
# File lib/reports/NikuReport.rb, line 70 70: def generateIntermediateFormat 71: super 72: 73: @scenarioIdx = a('scenarios')[0] 74: 75: computeResourceTotals 76: collectProjects 77: computeProjectAllocations 78: end
# File lib/reports/NikuReport.rb, line 80 80: def to_html 81: tableFrame = generateHtmlTableFrame 82: 83: tableFrame << (tr = XMLElement.new('tr')) 84: tr << (td = XMLElement.new('td')) 85: td << (table = XMLElement.new('table', 'class' => 'tj_table', 86: 'cellspacing' => '1')) 87: 88: # Table Header with two rows. First the project name, then the ID. 89: table << (thead = XMLElement.new('thead')) 90: thead << (tr = XMLElement.new('tr', 'class' => 'tabline')) 91: # First line 92: tr << htmlTabCell('Project', true, 'right') 93: @projects.each_key do |projectId| 94: # Don't include projects without allocations. 95: next if projectTotal(projectId) <= 0.0 96: name = @projects[projectId].name 97: # To avoid exploding tables for long project names, we only show the 98: # last 15 characters for those. We expect the last characters to be 99: # more significant in those names than the first. 100: name = '...' + name[15..1] if name.length > 15 101: tr << htmlTabCell(name, true, 'center') 102: end 103: tr << htmlTabCell('', true) 104: # Second line 105: thead << (tr = XMLElement.new('tr', 'class' => 'tabline')) 106: tr << htmlTabCell('Resource', true, 'left') 107: @projects.each_key do |projectId| 108: # Don't include projects without allocations. 109: next if projectTotal(projectId) <= 0.0 110: tr << htmlTabCell(projectId, true, 'center') 111: end 112: tr << htmlTabCell('Total', true, 'center') 113: 114: # The actual content. One line per resource. 115: table << (tbody = XMLElement.new('tbody')) 116: @resourcesTotalEffort.each_key do |resourceId| 117: tbody << (tr = XMLElement.new('tr', 'class' => 'tabline')) 118: tr << htmlTabCell("#{@resources[resourceId].name} (#{resourceId})", 119: true, 'left') 120: 121: @projects.each_key do |projectId| 122: next if projectTotal(projectId) <= 0.0 123: value = sum(projectId, resourceId) 124: tr << htmlTabCell(value >= 0.01 ? format("%.2f", value) : '') 125: end 126: 127: tr << htmlTabCell(format("%.2f", resourceTotal(resourceId)), true) 128: end 129: 130: # Project totals 131: tbody << (tr = XMLElement.new('tr', 'class' => 'tabline')) 132: tr << htmlTabCell('Total', 'true', 'left') 133: @projects.each_key do |projectId| 134: next if projectTotal(projectId) <= 0.0 135: tr << htmlTabCell(format("%.2f", projectTotal(projectId)), true, 136: 'right') 137: end 138: tr << htmlTabCell(format("%.2f", total()), true, 'right') 139: tableFrame 140: end
# File lib/reports/NikuReport.rb, line 142 142: def to_niku 143: xml = XMLDocument.new 144: xml << XMLComment.new(Generated by #{AppConfig.softwareName} v#{AppConfig.version} on #{TjTime.now}For more information about #{AppConfig.softwareName} see #{AppConfig.contact}.Project: #{@project['name']}Date: #{@project['now']} 145: ) 146: xml << (nikuDataBus = 147: XMLElement.new('NikuDataBus', 148: 'xmlns:xsi' => 149: 'http://www.w3.org/2001/XMLSchema-instance', 150: 'xsi:noNamespaceSchemaLocation' => 151: '../xsd/nikuxog_project.xsd')) 152: nikuDataBus << XMLElement.new('Header', 'action' => 'write', 153: 'externalSource' => 'NIKU', 154: 'objectType' => 'project', 155: 'version' => '7.5.0') 156: nikuDataBus << (projects = XMLElement.new('Projects')) 157: 158: timeFormat = '%Y-%m-%dT%H:%M:%S' 159: @projects.each_value do |prj| 160: # Don't include projects with 0 allocations 161: next if projectTotal(prj.id) <= 0.0 162: 163: projects << (project = 164: XMLElement.new('Project', 165: 'name' => prj.name, 166: 'projectID' => prj.id)) 167: project << (resources = XMLElement.new('Resources')) 168: prj.resources.each_value do |res| 169: resources << (resource = 170: XMLElement.new('Resource', 171: 'resourceID' => res.id, 172: 'defaultAllocation' => '0')) 173: resource << (allocCurve = XMLElement.new('AllocCurve')) 174: allocCurve << (XMLElement.new('Segment', 175: 'start' => 176: a('start').to_s(timeFormat), 177: 'finish' => 178: (a('end') - 1).to_s(timeFormat), 179: 'sum' => sum(prj.id, res.id).to_s)) 180: end 181: 182: # The custom information section usually contains Clarity installation 183: # specific parts. They are identical for each project section, so we 184: # mis-use the title attribute to insert them as an XML blob. 185: project << XMLBlob.new(a('title')) unless a('title').empty? 186: end 187: 188: xml.to_s 189: end
Search the Task list for the various ClarityPIDs and create a new Task list for each ClarityPID.
# File lib/reports/NikuReport.rb, line 305 305: def collectProjects 306: # Prepare the task list. 307: taskList = PropertyList.new(@project.tasks) 308: taskList.setSorting(@report.get('sortTasks')) 309: taskList = filterTaskList(taskList, nil, @report.get('hideTask'), 310: @report.get('rollupTask')) 311: 312: 313: taskList.each do |task| 314: # We only care about tasks that are leaf tasks and have resource 315: # allocations. 316: next unless task.leaf? || 317: task['assignedresources', @scenarioIdx].empty? 318: 319: id = task.get('ClarityPID') 320: # Ignore tasks without a ClarityPID attribute. 321: next if id.nil? 322: if id.empty? 323: raise TjException.new, 324: "ClarityPID of task #{task.fullId} may not be empty" 325: end 326: 327: name = task.get('ClarityPName') 328: if name.nil? 329: raise TjException.new, 330: "ClarityPName of task #{task.fullId} has not been set!" 331: end 332: if name.empty? 333: raise TjException.new, 334: "ClarityPName of task #{task.fullId} may not be empty!" 335: end 336: 337: if (project = @projects[id]).nil? 338: # We don't have a record for the Clarity project yet, so we create a 339: # new NikuProject object. 340: project = NikuProject.new(id, name) 341: # And store it in the project list hashed by the ClarityPID. 342: @projects[id] = project 343: else 344: # Due to a design flaw in the Niku file format, Clarity projects are 345: # identified by a name and an ID. We have to check that those pairs 346: # are always the same. 347: if (fTask = project.tasks.first).get('ClarityPName') != name 348: raise TjException.new, 349: "Task #{task.fullId} and task #{fTask.fullId} " + 350: "have same ClarityPID (#{id}) but different ClarityPName " + 351: "(#{name}/#{fTask.get('ClarityPName')})" 352: end 353: end 354: # Append the Task to the task list of the Clarity project. 355: project.tasks << task 356: end 357: 358: if @projects.empty? 359: raise TjException.new, 360: 'No tasks with the custom attributes ClarityPID and ClarityPName ' + 361: 'were found!' 362: end 363: end
Compute the total effort each Resource is allocated to the Task objects that have the same ClarityPID.
# File lib/reports/NikuReport.rb, line 367 367: def computeProjectAllocations 368: @projects.each_value do |project| 369: project.tasks.each do |task| 370: task['assignedresources', @scenarioIdx].each do |resource| 371: # Only consider resources that are in the filtered resource list. 372: next unless @resources[resource.get('ClarityRID')] 373: 374: # Prepare a template for the Query we will use to get all the data. 375: queryAttrs = { 'project' => @project, 376: 'property' => task, 377: 'scopeProperty' => resource, 378: 'scenarioIdx' => @scenarioIdx, 379: 'loadUnit' => a('loadUnit'), 380: 'numberFormat' => a('numberFormat'), 381: 'timeFormat' => a('timeFormat'), 382: 'currencyFormat' => a('currencyFormat'), 383: 'start' => a('start'), 'end' => a('end'), 384: 'costAccount' => a('costAccount'), 385: 'revenueAccount' => a('revenueAccount') } 386: 387: query = Query.new(queryAttrs) 388: query.attributeId = 'effort' 389: query.process 390: work = query.to_num 391: 392: # If the resource was not actually working on this task during the 393: # report period, we don't create a record for it. 394: next if work <= 0.0 395: 396: resourceId = resource.get('ClarityRID') 397: if (resourceRecord = project.resources[resourceId]).nil? 398: # If we don't already have a NikuResource object for the 399: # Resource, we create a new one. 400: resourceRecord = NikuResource.new(resourceId) 401: # Store the new NikuResource in the resource list of the 402: # NikuProject record. 403: project.resources[resourceId] = resourceRecord 404: end 405: resourceRecord.sum += query.to_num 406: end 407: end 408: end 409: end
The report must contain percent values for the allocation of the resources. A value of 1.0 means 100%. The resource is fully allocated for the whole report period. To compute the percentage later on, we first have to compute the maximum possible allocation.
# File lib/reports/NikuReport.rb, line 247 247: def computeResourceTotals 248: # Prepare the resource list. 249: resourceList = PropertyList.new(@project.resources) 250: resourceList.setSorting(@report.get('sortResources')) 251: resourceList = filterResourceList(resourceList, nil, 252: @report.get('hideResource'), 253: @report.get('rollupResource')) 254: 255: resourceList.each do |resource| 256: # We only care about leaf resources that have the custom attribute 257: # 'ClarityRID' set. 258: next if !resource.leaf? || 259: (resourceId = resource.get('ClarityRID')).nil? || 260: resourceId.empty? 261: 262: # Prepare a template for the Query we will use to get all the data. 263: queryAttrs = { 'project' => @project, 264: 'property' => resource, 265: 'scopeProperty' => nil, 266: 'scenarioIdx' => @scenarioIdx, 267: 'loadUnit' => a('loadUnit'), 268: 'numberFormat' => a('numberFormat'), 269: 'timeFormat' => a('timeFormat'), 270: 'currencyFormat' => a('currencyFormat'), 271: 'start' => a('start'), 'end' => a('end'), 272: 'costAccount' => a('costAccount'), 273: 'revenueAccount' => a('revenueAccount') } 274: 275: query = Query.new(queryAttrs) 276: 277: # First get the allocated effort. 278: query.attributeId = 'effort' 279: query.process 280: total = query.to_num 281: 282: # Then add the still available effort. 283: query.attributeId = 'freework' 284: query.process 285: total += query.to_num 286: 287: next if total <= 0.0 288: 289: @resources[resourceId] = resource 290: 291: # This is the maximum possible work of this resource in the report 292: # period. 293: @resourcesTotalEffort[resourceId] = total 294: end 295: 296: # Make sure that we have at least one Resource with a ClarityRID. 297: if @resourcesTotalEffort.empty? 298: raise TjException.new, 299: 'No resources with the custom attribute ClarityRID were found!' 300: end 301: end
# File lib/reports/NikuReport.rb, line 234 234: def htmlTabCell(text, headerCell = false, align = 'right') 235: td = XMLElement.new('td', 'class' => headerCell ? 'tabhead' : 'taskcell1') 236: td << XMLNamedText.new(text, 'div', 237: 'class' => headerCell ? 'headercelldiv' : 'celldiv', 238: 'style' => "text-align:#{align}") 239: td 240: end
# File lib/reports/NikuReport.rb, line 216 216: def projectTotal(projectId) 217: total = 0.0 218: @resources.each_key do |resourceId| 219: total += sum(projectId, resourceId) 220: end 221: total 222: end
# File lib/reports/NikuReport.rb, line 208 208: def resourceTotal(resourceId) 209: total = 0.0 210: @projects.each_key do |projectId| 211: total += sum(projectId, resourceId) 212: end 213: total 214: end
# File lib/reports/NikuReport.rb, line 198 198: def sum(projectId, resourceId) 199: project = @projects[projectId] 200: return 0.0 unless project 201: 202: resource = project.resources[resourceId] 203: return 0.0 unless resource && @resourcesTotalEffort[resourceId] 204: 205: resource.sum / @resourcesTotalEffort[resourceId] 206: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.