Class Index [+]

Quicksearch

TaskJuggler::NikuReport

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.

Public Class Methods

new(report) click to toggle source
    # 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

Public Instance Methods

generateIntermediateFormat() click to toggle source
    # 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
to_csv() click to toggle source
     # File lib/reports/NikuReport.rb, line 196
196:     def to_csv
197:       table = []
198:       # Header line with project names
199:       table << (row = [])
200:       # First column is the resource name and ID.
201:       row << ""
202:       @projects.each_key do |projectId|
203:         row << @projects[projectId].name
204:       end
205: 
206:       # Header line with project IDs
207:       table << (row = [])
208:       row << "Resource"
209:       @projects.each_key do |projectId|
210:         row << projectId
211:       end
212: 
213:       @resourcesTotalEffort.each_key do |resourceId|
214:         # Add one line per resource.
215:         table << (row = [])
216:         row << "#{@resources[resourceId].name} (#{resourceId})"
217:         @projects.each_key do |projectId|
218:           row << sum(projectId, resourceId)
219:         end
220:       end
221: 
222:       table
223:     end
to_html() click to toggle source
     # 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
to_niku() click to toggle source
     # 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

Private Instance Methods

collectProjects() click to toggle source

Search the Task list for the various ClarityPIDs and create a new Task list for each ClarityPID.

     # File lib/reports/NikuReport.rb, line 335
335:     def collectProjects
336:       # Prepare the task list.
337:       taskList = PropertyList.new(@project.tasks)
338:       taskList.setSorting(@report.get('sortTasks'))
339:       taskList = filterTaskList(taskList, nil, @report.get('hideTask'),
340:                                 @report.get('rollupTask'), @report.get('openNodes'))
341: 
342: 
343:       taskList.each do |task|
344:         # We only care about tasks that are leaf tasks and have resource
345:         # allocations.
346:         next unless task.leaf? ||
347:                     task['assignedresources', @scenarioIdx].empty?
348: 
349:         id = task.get('ClarityPID')
350:         # Ignore tasks without a ClarityPID attribute.
351:         next if id.nil?
352:         if id.empty?
353:           raise TjException.new,
354:                 "ClarityPID of task #{task.fullId} may not be empty"
355:         end
356: 
357:         name = task.get('ClarityPName')
358:         if name.nil?
359:           raise TjException.new,
360:                 "ClarityPName of task #{task.fullId} has not been set!"
361:         end
362:         if name.empty?
363:           raise TjException.new,
364:                 "ClarityPName of task #{task.fullId} may not be empty!"
365:         end
366: 
367:         if (project = @projects[id]).nil?
368:           # We don't have a record for the Clarity project yet, so we create a
369:           # new NikuProject object.
370:           project = NikuProject.new(id, name)
371:           # And store it in the project list hashed by the ClarityPID.
372:           @projects[id] = project
373:         else
374:           # Due to a design flaw in the Niku file format, Clarity projects are
375:           # identified by a name and an ID. We have to check that those pairs
376:           # are always the same.
377:           if (fTask = project.tasks.first).get('ClarityPName') != name
378:             raise TjException.new,
379:               "Task #{task.fullId} and task #{fTask.fullId} " +
380:               "have same ClarityPID (#{id}) but different ClarityPName " +
381:               "(#{name}/#{fTask.get('ClarityPName')})"
382:           end
383:         end
384:         # Append the Task to the task list of the Clarity project.
385:         project.tasks << task
386:       end
387: 
388:       if @projects.empty?
389:         raise TjException.new,
390:           'No tasks with the custom attributes ClarityPID and ClarityPName ' +
391:           'were found!'
392:       end
393:     end
computeProjectAllocations() click to toggle source

Compute the total effort each Resource is allocated to the Task objects that have the same ClarityPID.

     # File lib/reports/NikuReport.rb, line 397
397:     def computeProjectAllocations
398:       @projects.each_value do |project|
399:         project.tasks.each do |task|
400:           task['assignedresources', @scenarioIdx].each do |resource|
401:             # Only consider resources that are in the filtered resource list.
402:             next unless @resources[resource.get('ClarityRID')]
403: 
404:             # Prepare a template for the Query we will use to get all the data.
405:             queryAttrs = { 'project' => @project,
406:                            'property' => task,
407:                            'scopeProperty' => resource,
408:                            'scenarioIdx' => @scenarioIdx,
409:                            'loadUnit' => a('loadUnit'),
410:                            'numberFormat' => a('numberFormat'),
411:                            'timeFormat' => a('timeFormat'),
412:                            'currencyFormat' => a('currencyFormat'),
413:                            'start' => a('start'), 'end' => a('end'),
414:                            'costAccount' => a('costAccount'),
415:                            'revenueAccount' => a('revenueAccount') }
416: 
417:             query = Query.new(queryAttrs)
418:             query.attributeId = 'effort'
419:             query.process
420:             work = query.to_num
421: 
422:             # If the resource was not actually working on this task during the
423:             # report period, we don't create a record for it.
424:             next if work <= 0.0
425: 
426:             resourceId = resource.get('ClarityRID')
427:             if (resourceRecord = project.resources[resourceId]).nil?
428:               # If we don't already have a NikuResource object for the
429:               # Resource, we create a new one.
430:               resourceRecord = NikuResource.new(resourceId)
431:               # Store the new NikuResource in the resource list of the
432:               # NikuProject record.
433:               project.resources[resourceId] = resourceRecord
434:             end
435:             resourceRecord.sum += query.to_num
436:           end
437:         end
438:       end
439:     end
computeResourceTotals() click to toggle source

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 276
276:     def computeResourceTotals
277:       # Prepare the resource list.
278:       resourceList = PropertyList.new(@project.resources)
279:       resourceList.setSorting(@report.get('sortResources'))
280:       resourceList = filterResourceList(resourceList, nil,
281:                                         @report.get('hideResource'),
282:                                         @report.get('rollupResource'),
283:                                         @report.get('openNodes'))
284: 
285:       resourceList.each do |resource|
286:         # We only care about leaf resources that have the custom attribute
287:         # 'ClarityRID' set.
288:         next if !resource.leaf? ||
289:                 (resourceId = resource.get('ClarityRID')).nil? ||
290:                 resourceId.empty?
291: 
292:         # Prepare a template for the Query we will use to get all the data.
293:         queryAttrs = { 'project' => @project,
294:                        'property' => resource,
295:                        'scopeProperty' => nil,
296:                        'scenarioIdx' => @scenarioIdx,
297:                        'loadUnit' => a('loadUnit'),
298:                        'numberFormat' => a('numberFormat'),
299:                        'timeFormat' => a('timeFormat'),
300:                        'currencyFormat' => a('currencyFormat'),
301:                        'start' => a('start'), 'end' => a('end'),
302:                        'costAccount' => a('costAccount'),
303:                        'revenueAccount' => a('revenueAccount') }
304: 
305:         query = Query.new(queryAttrs)
306: 
307:         # First get the allocated effort.
308:         query.attributeId = 'effort'
309:         query.process
310:         total = query.to_num
311: 
312:         # Then add the still available effort.
313:         query.attributeId = 'freework'
314:         query.process
315:         total += query.to_num
316: 
317:         next if total <= 0.0
318: 
319:         @resources[resourceId] = resource
320: 
321:         # This is the maximum possible work of this resource in the report
322:         # period.
323:         @resourcesTotalEffort[resourceId] = total
324:       end
325: 
326:       # Make sure that we have at least one Resource with a ClarityRID.
327:       if @resourcesTotalEffort.empty?
328:         raise TjException.new,
329:           'No resources with the custom attribute ClarityRID were found!'
330:       end
331:     end
htmlTabCell(text, headerCell = false, align = 'right') click to toggle source
     # File lib/reports/NikuReport.rb, line 263
263:     def htmlTabCell(text, headerCell = false, align = 'right')
264:       td = XMLElement.new('td', 'class' => headerCell ? 'tabhead' : 'taskcell1')
265:       td << XMLNamedText.new(text, 'div',
266:                              'class' => headerCell ? 'headercelldiv' : 'celldiv',
267:                              'style' => "text-align:#{align}")
268:       td
269:     end
projectTotal(projectId) click to toggle source
     # File lib/reports/NikuReport.rb, line 245
245:     def projectTotal(projectId)
246:       total = 0.0
247:       @resources.each_key do |resourceId|
248:         total += sum(projectId, resourceId)
249:       end
250:       total
251:     end
resourceTotal(resourceId) click to toggle source
     # File lib/reports/NikuReport.rb, line 237
237:     def resourceTotal(resourceId)
238:       total = 0.0
239:       @projects.each_key do |projectId|
240:         total += sum(projectId, resourceId)
241:       end
242:       total
243:     end
sum(projectId, resourceId) click to toggle source
     # File lib/reports/NikuReport.rb, line 227
227:     def sum(projectId, resourceId)
228:       project = @projects[projectId]
229:       return 0.0 unless project
230: 
231:       resource = project.resources[resourceId]
232:       return 0.0 unless resource && @resourcesTotalEffort[resourceId]
233: 
234:       resource.sum / @resourcesTotalEffort[resourceId]
235:     end
total() click to toggle source
     # File lib/reports/NikuReport.rb, line 253
253:     def total
254:       total = 0.0
255:       @projects.each_key do |projectId|
256:         @resources.each_key do |resourceId|
257:           total += sum(projectId, resourceId)
258:         end
259:       end
260:       total
261:     end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.