class TaskJuggler::MspXmlRE

This specialization of ReportBase implements an export of the project data into Microsoft Project XML format. Due to limitations of MS Project and this implementation, only a subset of core data is being exported. The exported data is already a scheduled project with full resource/task assignment data.

Public Class Methods

new(report) click to toggle source

Create a new object and set some default values.

Calls superclass method
# File lib/taskjuggler/reports/MspXmlRE.rb, line 25
def initialize(report)
  super(report)

  # This report type currently only supports a single scenario. Use the
  # first specified one.
  @scenarioIdx = a('scenarios').first

  # Hash to map calendar names to UIDs (numbers).
  @calendarUIDs = {}
  @timeformat = "%Y-%m-%dT%H:%M:%S"
end

Public Instance Methods

generateIntermediateFormat() click to toggle source
Calls superclass method
# File lib/taskjuggler/reports/MspXmlRE.rb, line 37
def generateIntermediateFormat
  super
end
to_mspxml() click to toggle source

Return the project data in Microsoft Project XML format.

# File lib/taskjuggler/reports/MspXmlRE.rb, line 42
    def to_mspxml
      @query = @project.reportContexts.last.query.dup

      # Prepare the resource list.
      @resourceList = PropertyList.new(@project.resources)
      @resourceList.setSorting(a('sortResources'))
      @resourceList = filterResourceList(@resourceList, nil, a('hideResource'),
                                         a('rollupResource'), a('openNodes'))
      @resourceList.sort!

      # Prepare the task list.
      @taskList = PropertyList.new(@project.tasks)
      @taskList.includeAdopted
      @taskList.setSorting(a('sortTasks'))
      @taskList = filterTaskList(@taskList, nil, a('hideTask'), a('rollupTask'),
                                 a('openNodes'))
      @taskList.sort!
      @taskList.checkForDuplicates(@report.sourceFileInfo)

      @file = XMLDocument.new
      @file << XMLBlob.new('<?xml version="1.0" encoding="UTF-8" ' +
                           'standalone="yes"?>')
      @file << XMLComment.new(<<"EOT"
Generated by #{AppConfig.softwareName} v#{AppConfig.version} on #{TjTime.new}
For more information about #{AppConfig.softwareName} see #{AppConfig.contact}.
Project: #{@project['name']}
Date:    #{@project['now']}
EOT
                             )
      @file << (project =
                XMLElement.new('Project',
                               'xmlns' =>
                               'http://schemas.microsoft.com/project'))

      calendars = generateProjectAttributes(project)
      generateTasks(project)
      generateResources(project, calendars)
      generateAssignments(project)

      @file.to_s
    end

Private Instance Methods

daytime_to_s(t) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 402
def daytime_to_s(t)
  h = (t / (60 * 60)).to_i
  m = ((t - (h * 60 * 60)) / 60).to_i
  s = (t % 60).to_i
  sprintf('%02d:%02d:%02d', h, m, s)
end
durationToMsp(duration) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 392
def durationToMsp(duration)
  return '' if duration == 0

  hours = (duration / (60 * 60)).to_i
  minutes = ((duration - (hours * 60 * 60)) / 60).to_i
  seconds = (duration % 60).to_i

  "PT#{hours}H#{minutes}M#{seconds}S"
end
findRolledUpParent(task) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 376
def findRolledUpParent(task)
  return nil unless (rollupTask = a('rollupTask'))

  hideTask = a('hideTask')
  while task
    @query.property = task
    # We don't want to include any tasks that are explicitely hidden via
    # 'hidetask'.
    return nil if hideTask && hideTask.eval(@query)

    return task if rollupTask.eval(@query) && @taskList.include?(task)

    task = task.parent
  end
end
generateAssignment(assignments, task, resource, uid) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 314
def generateAssignment(assignments, task, resource, uid)
  assignments << (a = XMLElement.new('Assignment'))
  a << XMLNamedText.new(uid.to_s, 'UID')
  a << XMLNamedText.new(@taskList[task].get('index').to_s,
                        'TaskUID')
  a << XMLNamedText.new(resource.get('index').to_s,
                        'ResourceUID')
  a << XMLNamedText.new(resource['efficiency', @scenarioIdx].to_s,
                        'Units')
  a << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat),
                        'start')
  a << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat),
                        'finish')
  a << XMLNamedText.new('100.0', 'Cost')
  a << XMLNamedText.new('8', 'WorkContour')
  # The PercentWorkComplete value must be 0. Otherwise the completed
  # work of the assignments will be ignored.
  a << XMLNamedText.new('0', 'PercentWorkComplete')
  responsible = task['responsible', @scenarioIdx].
    map { |r| r.name }.join(', ')
  a << XMLNamedText.new(responsible, 'AssnOwner') unless responsible.empty?

  # Setup the query for this task and resource.
  @query.property = resource
  @query.scopeProperty = task
  @query.attributeId = 'effort'
  @query.scenarioIdx = @scenarioIdx
  @query.start = task['start', @scenarioIdx]
  @query.end = task['end', @scenarioIdx]
  @query.process

  tStart = task['start', @scenarioIdx]
  # We provide assignement data on a day-by-day basis. We report the work
  # that happens each day from task start to task end.
  tStart = tStart.midnight
  tEnd = task['end', @scenarioIdx]
  t = tStart
  while t < tEnd
    tn = t.sameTimeNextDay
    # We need to make sure that the stored intervals are within the task
    # and report boundaries. tn and tnc are corrected versions of t and tn
    # that meet this criterium.
    tc = t < task['start', @scenarioIdx] ? task['start', @scenarioIdx] : t
    tc = tc < a('start') ? a('start') : tc
    tnc = tn > task['end', @scenarioIdx] ? task['end', @scenarioIdx] : tn
    tnc = tnc > a('end') ? a('end') : tnc
    @query.start = tc
    @query.end = tnc
    @query.process
    workSeconds = @query.to_num * @project.dailyWorkingHours * 3600
    a << (td = XMLElement.new('TimephasedData'))
    td << XMLNamedText.new(uid.to_s, 'UID')
    # The Type must be 1 or MS Project will take forever to load the file.
    td << XMLNamedText.new('1', 'Type')
    td << XMLNamedText.new(tc.to_s(@timeformat), 'Start')
    td << XMLNamedText.new((tnc - 1).to_s(@timeformat), 'Finish')
    td << XMLNamedText.new('2', 'Unit')
    td << XMLNamedText.new(durationToMsp(workSeconds), 'Value')
    t = tn
  end
end
generateAssignments(project) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 150
def generateAssignments(project)
  project << (assignments = XMLElement.new('Assignments'))

  i = 0
  @taskList.each do |task|
    rollupTask = a('rollupTask')
    @query.property = task
    @query.scopeProperty = nil
    # We only generate assignments for leaf tasks and rolled-up container
    # tasks.
    next if (task.container? && !(rollupTask && rollupTask.eval(@query)))

    task.assignedResources(@scenarioIdx).each do |resource|
      generateAssignment(assignments, task, resource, i)
      i += 1
    end
  end
end
generateCalendar(calendars, workinghours, name) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 169
def generateCalendar(calendars, workinghours, name)
  calendars << (cal = XMLElement.new('Calendar'))
  uid = @calendarUIDs.length.to_s
  @calendarUIDs[name] = uid
  cal << XMLNamedText.new(uid, 'UID')
  cal << XMLNamedText.new(name, 'Name')
  cal << XMLNamedText.new('1', 'IsBaseCalendar')
  cal << XMLNamedText.new('-1', 'BaseCalendarUID')

  cal << (weekdays = XMLElement.new('WeekDays'))
  d = 1
  workinghours.days.each do |day|
    weekdays << (weekday = XMLElement.new('WeekDay'))
    weekday << XMLNamedText.new(d.to_s, 'DayType')
    d += 1
    if day.empty?
      weekday << XMLNamedText.new('0', 'DayWorking')
    else
      weekday << XMLNamedText.new('1', 'DayWorking')
      weekday << (workingtimes = XMLElement.new('WorkingTimes'))
      day.each do |iv|
        workingtimes << (worktime = XMLElement.new('WorkingTime'))
        worktime << XMLNamedText.new(daytime_to_s(iv[0]), 'FromTime')
        worktime << XMLNamedText.new(daytime_to_s(iv[1]), 'ToTime')
      end
    end
  end
end
generateProjectAttributes(p) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 86
def generateProjectAttributes(p)
  p << XMLNamedText.new('14', 'SaveVersion')
  p << XMLNamedText.new(@report.name + '.xml', 'Name')
  p << XMLNamedText.new(TjTime.new.to_s(@timeformat), 'CreationDate')
  p << XMLNamedText.new('1', 'ScheduleFromStart')
  p << XMLNamedText.new(@project['start'].to_s(@timeformat),
                        'StartDate')
  p << XMLNamedText.new(@project['end'].to_s(@timeformat),
                        'FinishDate')
  p << XMLNamedText.new('09:00:00', 'DefaultStartTime')
  p << XMLNamedText.new('17:00:00', 'DefaultFinishTime')
  p << XMLNamedText.new('1', 'CalendarUID')
  p << XMLNamedText.new((@project.dailyWorkingHours * 60).to_i.to_s,
                        'MinutesPerDay')
  p << XMLNamedText.new((@project.weeklyWorkingDays *
                         @project.dailyWorkingHours * 60).to_i.to_s,
                        'MinutesPerWeek')
  p << XMLNamedText.new((@project.yearlyWorkingDays / 12).to_s,
                        'DaysPerMonth')
  p << XMLNamedText.new(@project['now'].to_s(@timeformat), 'CurrentDate')
  p << XMLNamedText.new(@project['now'].to_s(@timeformat), 'StatusDate')
  loadUnitsMap = {
    :minutes => 1,
    :hours => 2,
    :days => 3,
    :weeks => 4,
    :months => 5,
    :quarters => 5,
    :years => 5,
    :shortAuto => 3,
    :longAuto => 3
  }
  p << XMLNamedText.new(loadUnitsMap[a('loadUnit')].to_s, 'WorkFormat')
  p << XMLNamedText.new('1', 'NewTasksAreManual')
  p << XMLNamedText.new('0', 'SpreadPercentComplete')
  rate = (@project['rate'] / @project.dailyWorkingHours).to_s
  p << XMLNamedText.new(rate, 'StandardRate')
  p << XMLNamedText.new(rate, 'OvertimeRate')
  p << XMLNamedText.new(@project['currency'], 'CurrencySymbol')
  p << XMLNamedText.new(@project['currency'], 'CurrencyCode')
  #p << XMLNamedText.new('0', 'MicrosoftProjectServerURL')

  p << (calendars = XMLElement.new('Calendars'))
  generateCalendar(calendars, @project['workinghours'], 'Standard')

  calendars
end
generateResource(resources, resource, calendars) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 279
def generateResource(resources, resource, calendars)
  # MS Project can only deal with a flat resource list. We don't export
  # resource groups.
  return unless resource.leaf?

  resources << (r = XMLElement.new('Resource'))
  r << XMLNamedText.new(resource.get('index').to_s, 'UID')
  # All TJ resources are people or equipment.
  r << XMLNamedText.new('1', 'Type')
  r << XMLNamedText.new(resource.name, 'Name')
  r << XMLNamedText.new(resource.id, 'Initials')
  # MS Project seems to use hourly rates, TJ daily rates.
  rate = (resource['rate', @scenarioIdx] / @project.dailyWorkingHours).to_s
  r << XMLNamedText.new(rate, 'StandardRate')
  r << XMLNamedText.new(rate, 'OvertimeRate')
  r << XMLNamedText.new(resource['efficiency', @scenarioIdx].to_s,
                        'MaxUnits')
  if (email = resource.get('email'))
    r << XMLNamedText.new(email, 'EmailAddress')
  end
  r << XMLNamedText.new(resource.parent.name, 'Group') if resource.parent
  #if (code = resource.get('Code'))
  #  r << XMLNamedText.new(code, 'Code')
  #  r << XMLNamedText.new('1', 'IsEnterprise')
  #end
  #if (ntaccount = resource.get('NTAccount'))
  #  r << XMLNamedText.new(ntaccount, 'NTAccount')
  #end
  # Generate a calendar for this resource and assign it.
  generateCalendar(calendars, resource['workinghours', @scenarioIdx],
                   "Calendar #{resource.name}")
  r << XMLNamedText.new(@calendarUIDs["Calendar #{resource.name}"],
                        'CalendarUID')
end
generateResources(project, calendars) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 142
def generateResources(project, calendars)
  project << (resources = XMLElement.new('Resources'))

  @resourceList.each do |resource|
    generateResource(resources, resource, calendars)
  end
end
generateTask(tasks, task) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 198
def generateTask(tasks, task)
  @query.property = task
  task.calcCompletion(@scenarioIdx)
  percentComplete = task['complete', @scenarioIdx]

  tasks << (t = XMLElement.new('Task'))
  t << XMLNamedText.new(task.get('index').to_s, 'UID')
  t << XMLNamedText.new(task.get('index').to_s, 'ID')
  t << XMLNamedText.new('1', 'Active')
  t << XMLNamedText.new('0', 'Type')
  t << XMLNamedText.new('0', 'IsNull')
  t << XMLNamedText.new(task.get('name'), 'Name')
  t << XMLNamedText.new(task.get('bsi'), 'WBS')
  t << XMLNamedText.new(task.get('bsi'), 'OutlineNumber')
  t << XMLNamedText.new((task.level -
                         (a('taskroot') ? a('taskroot').level : 0)).to_s,
                         'OutlineLevel')
  t << XMLNamedText.new(task['priority', @scenarioIdx].to_s, 'Priority')
  t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat),
                        'Start')
  t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat),
                        'Finish')
  t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat),
                        'ManualStart')
  t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat),
                        'ManualFinish')
  t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat),
                        'ActualStart')
  t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat),
                          'ActualFinish')
  t << XMLNamedText.new('2', 'ConstraintType')
  t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat),
                        'ConstraintDate')
  t << XMLNamedText.new('3', 'FixedCostAccrual')
  if (note = task.get('note'))
    t << XMLNamedText.new(note.to_s, 'Notes')
  end
  responsible = task['responsible', @scenarioIdx].
    map { |r| r.name }.join(', ')
  t << XMLNamedText.new(responsible, 'Contact') unless responsible.empty?

  if task.container?
    rollupTask = a('rollupTask')
    t << XMLNamedText.new(rollupTask ? '1' : '0', 'Manual')
    t << XMLNamedText.new(rollupTask && rollupTask.eval(@query) ? '0' : '1',
                          'Summary')
  else
    t << XMLNamedText.new('1', 'Manual')
    t << XMLNamedText.new('0', 'Summary')
    t << XMLNamedText.new('0', 'Estimated')
    t << XMLNamedText.new('5', 'DurationFormat')
    if task['milestone', @scenarioIdx]
      t << XMLNamedText.new('1', 'Milestone')
    else
      t << XMLNamedText.new('0', 'Milestone')
      t << XMLNamedText.new(percentComplete.to_i.to_s,
                            'PercentComplete')
      t << XMLNamedText.new(percentComplete.to_i.to_s,
                            'PercentWorkComplete')
    end
  end
  task['startpreds', @scenarioIdx].each do |dt, onEnd|
    next unless @taskList.include?(dt)
    next if task.parent &&
            task.parent['startpreds', @scenarioIdx].include?([ dt, onEnd ])
    t << (pl = XMLElement.new('PredecessorLink'))
    pl << XMLNamedText.new(@taskList[dt].get('index').to_s,
                           'PredecessorUID')
    pl << XMLNamedText.new(onEnd ? '1' : '3', 'Type')
  end
  task['endpreds', @scenarioIdx].each do |dt, onEnd|
    next unless @taskList.include?(dt)
    next if task.parent &&
            task.parent['endpreds', @scenarioIdx].include?([ dt, onEnd ])
    t << (pl = XMLElement.new('PredecessorLink'))
    pl << XMLNamedText.new(@taskList[dt].get('index').to_s,
                           'PredecessorUID')
    pl << XMLNamedText.new(onEnd ? '0' : '2', 'Type')
  end
end
generateTasks(project) click to toggle source
# File lib/taskjuggler/reports/MspXmlRE.rb, line 134
def generateTasks(project)
  project << (tasks = XMLElement.new('Tasks'))

  @taskList.each do |task|
    generateTask(tasks, task)
  end
end