Files

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_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 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
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 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
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 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
htmlTabCell(text, headerCell = false, align = 'right') click to toggle source
     # 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
projectTotal(projectId) click to toggle source
     # 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
resourceTotal(resourceId) click to toggle source
     # 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
sum(projectId, resourceId) click to toggle source
     # 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
total() click to toggle source
     # File lib/reports/NikuReport.rb, line 224
224:     def total
225:       total = 0.0
226:       @projects.each_key do |projectId|
227:         @resources.each_key do |resourceId|
228:           total += sum(projectId, resourceId)
229:         end
230:       end
231:       total
232:     end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.