Skip to content

Instantly share code, notes, and snippets.

@zlorb
Last active September 4, 2025 17:24
Show Gist options
  • Select an option

  • Save zlorb/ff122e8563793bb28f79 to your computer and use it in GitHub Desktop.

Select an option

Save zlorb/ff122e8563793bb28f79 to your computer and use it in GitHub Desktop.

Revisions

  1. zlorb renamed this gist Jun 11, 2014. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. zlorb revised this gist Jun 11, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion gistfile1.py
    Original file line number Diff line number Diff line change
    @@ -7,7 +7,7 @@
    import traceback

    # Sample code for accessing MSProject
    # (C) Zohar Lorberbaum
    # 2014 (C) Zohar Lorberbaum

    debug = False

  3. zlorb revised this gist Jun 11, 2014. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions gistfile1.py
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,9 @@
    import win32com.client
    import traceback

    # Sample code for accessing MSProject
    # (C) Zohar Lorberbaum

    debug = False

    def proj2time(t):
  4. zlorb created this gist Jun 11, 2014.
    598 changes: 598 additions & 0 deletions gistfile1.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,598 @@
    import sys, time, datetime
    from copy import copy, deepcopy
    from collections import OrderedDict
    import string
    import math
    import win32com.client
    import traceback

    debug = False

    def proj2time(t):
    """Convert MSProject time to Python time"""
    return time.mktime(time.strptime(t.Format('%m/%d/%y %H:%M:%S'), '%m/%d/%y %H:%M:%S'))

    def expectedProgress(start, finish, t=None):
    """Get expected percent progress given start, finish, and point t in time (in seconds since the Epoch).
    If t is not defined, use current time (now)."""
    s = proj2time(start)
    f = proj2time(finish)
    if not t: t = time.time()
    if t<=s:
    return 0
    elif t>f:
    return 100
    else:
    p = int( (t-s)/(f-s)*100.0 ) # linear task progress
    return p

    def expectedWork(etotal, estart, efinish, t):
    """Get expected work progress given total work, start, finish, and point t in time (in seconds since the Epoch)."""
    if t<=estart:
    return 0.0
    elif t>efinish:
    return float(etotal)
    else:
    p = float(etotal)*(t-estart)/(efinish-estart) # linear task progress
    return p

    def expectedWork2(etotal, estart, efinish, t):
    """Heuristic expected percent progress given start, finish, and point t in time (in seconds since the Epoch).
    If t is not defined, use current time (now)."""
    if t<=estart:
    return 0.0
    elif t>efinish:
    return float(etotal)
    else:
    a1 = 4.0
    a2 = 2.0
    a3 = -1.0
    c1 = 1.0
    c2 = 3.0
    c3 = 9.0
    f1 = 100.0*(t-estart)/(efinish-estart)
    f2 = math.sin(c1*math.pi*(t-estart)/(efinish-estart))
    f3 = f1-a1*f2
    f4 = math.sin(c2*math.pi*(t-estart)/(efinish-estart))
    f5 = f3-a2*f4
    f6 = math.sin(c3*math.pi*(t-estart)/(efinish-estart))
    f7 = f5-a3*f6
    f0 = 0.0-a1*f2-a2*f4-a3*f6
    p = float( etotal*f7/100.0 ) # variable task progress
    return p

    class MSProject:
    """MSProject class."""
    def __init__(self):
    self.mpp = win32com.client.Dispatch("MSProject.Application")
    self.Project = None
    self._Tasks = None
    if debug: self.mpp.Visible = 1
    return

    def __call__(self):
    print 'MSProject call'
    return

    def __getattr__(self,attr):
    if attr == 'Tasks':
    #if not self.__dict__.has_key('_Tasks'):
    if not self._Tasks:
    if not self.__dict__.has_key('Project'):
    print "You have to load a file first."
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, attr))
    else:
    self._Tasks=Tasks(self.mpp, self.Project)
    return self._Tasks
    else:
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, attr))

    def __repr__(self):
    return "MSProject class"

    def __dir__(self):
    d=['Tasks']
    d += self.__dict__.keys()
    d += self.__class__.__dict__.keys()
    return d

    def load(self, doc):
    """Load a given MSProject file."""
    try:
    self.mpp.FileOpen(doc)
    self.Project = self.mpp.ActiveProject
    return True
    except Exception, e:
    print "Error opening file",e
    return False

    def saveAndClose(self):
    """Close an open MSProject, saving changes."""
    if self.__dict__.has_key('Project'):
    self.mpp.FileSave()
    self.mpp.Quit()
    return

    def dump(self):
    """Dump file contents, for debugging purposes."""
    if not self.__dict__.has_key('Project'):
    print "No project file is open. Use 'open' command first."
    return False
    try:
    print "This project has ", str(self.Project.Tasks.Count), " Tasks"
    for i in range(1,self.Project.Tasks.Count+1):
    print i,
    try:
    print self.Project.Tasks.Item(i).Name[:60].encode('ascii', 'ignore'),
    print self.Project.Tasks.Item(i).Text1.encode('ascii', 'ignore'), # Custom field
    print self.Project.Tasks.Item(i).ResourceNames.encode('ascii', 'ignore'),
    print self.Project.Tasks.Item(i).Start,
    print self.Project.Tasks.Item(i).Finish,
    print self.Project.Tasks.Item(i).PercentWorkComplete,
    print '%'
    except:
    print 'Empty'
    return True
    except Exception, e:
    print "Error:", e
    return False


    class Tasks(object):
    """Class to hold task lines in MSProject.
    Access to items is via Accept360 S/N."""
    def __init__(self, mpp, Project):
    self.mpp = mpp
    self.Project = Project
    self._Tasks = None
    self._RFQAs = None
    self._compoundTask = None
    self._unknowns = None
    self._msfields = ['Name', 'Resources', 'Start', 'Finish', 'PercentWorkComplete', 'Priority', 'ReleaseName']
    return

    def __repr__(self):
    if self._Tasks:
    return 'Requirements: '+str(self._Tasks.keys())
    else:
    return 'Please load a file and get tasks first.'

    def __dir__(self):
    l = self.__class__.__dict__.keys() + self.__dict__.keys()
    if self._Tasks:
    for k in self._Tasks.keys():
    l.append('SN'+str(k))
    l.remove('_Tasks')
    return l

    def __call__(self, *args, **kargs):
    if kargs:
    if kargs.has_key('SN'):
    if self._Tasks:
    if self._Tasks.has_key(kargs['SN']):
    return self._Tasks[kargs['SN']]
    else:
    print 'No requirements were retreived from server yet.\nPlease perform a parsed query.'
    return None
    elif args:
    if self._Tasks:
    if len(args)>0:
    for arg in args:
    if arg[:2]=='SN':
    if self._Tasks.has_key(arg[2:]):
    return self._Tasks[arg[2:]]
    else:
    if self._Tasks.has_key(arg):
    return self._Tasks[arg]
    print str(arg) + ' was not found'
    return None
    else:
    print 'Please load a file and get tasks first.'
    return None
    else:
    if not self._Tasks:
    self.getTasks()
    return self._Tasks

    def __getattr__(self,attr):
    if attr == 'Tasks':
    if not self._Tasks:
    self.getTasks()
    return self._Tasks
    elif self.__dict__.has_key(attr):
    return self.__dict__[attr]
    elif self._Tasks:
    if len(attr)>2:
    if attr[:2]=='SN':
    if self._Tasks.has_key(attr[2:]):
    return self._Tasks[attr[2:]]
    else:
    if self._Tasks.has_key(attr):
    return self._Tasks[attr]
    if attr in self._compoundTask.keys():
    return self._compoundTask[attr]['id']
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, attr))

    def __getitem__(self, attr):
    if attr == 'Tasks':
    if not self._Tasks:
    self.getTasks()
    return self._Tasks
    elif self.__dict__.has_key(attr):
    return self.__dict__[attr]
    elif self._Tasks:
    if len(attr)>2:
    if attr[:2]=='SN':
    if self._Tasks.has_key(attr[2:]):
    return self._Tasks[attr[2:]]
    else:
    if self._Tasks.has_key(attr):
    return self._Tasks[attr]
    if attr in self._compoundTask.keys():
    return self._compoundTask[attr]['id']
    return str(attr) + ' was not found'
    else:
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, attr))


    def getTasks(self):
    """Return all tasks that have a value in the 'Accept360 S/N' field and have a resource assigned."""
    self._Tasks = dict()
    self._RFQAs = dict()
    self._compoundTask = dict()
    self._unknowns = dict()
    if not self.__dict__.has_key('Project'):
    print "No project file is open. Use 'open' command first."
    return False
    # Get all MSProject tasks: for duplicate 'Accept360' fields - update Start, End, Resource, PercentWorkComplete accordingly
    try:
    for i in range(1,self.Project.Tasks.Count+1):
    try:
    task = False
    rfqa = False
    Py = self.Project.Tasks.Item(i).Text4 # Helper column to allow ignoring lines.
    if Py.lower()!='ignore':
    SN = self.Project.Tasks.Item(i).Text1 # A custom column to store unique task S/N
    Priority = self.Project.Tasks.Item(i).Text2 # A custom column to store task priority
    ReleaseName = self.Project.Tasks.Item(i).Text3 # A custom column to store release name
    QCDB = self.Project.Tasks.Item(i).Text5 # A custom column to store test results database name
    QCRel = self.Project.Tasks.Item(i).Text6 # A custom column to store test results path
    if not SN: continue # skip items w/o serial number
    if str(SN)=='': continue # skip items w/o serial number
    sns = SN.split(',') # handle comma separated multiple serial numbers
    for s in sns:
    sn = str(s)
    if self.Project.Tasks.Item(i).ResourceNames!=None and str(self.Project.Tasks.Item(i).ResourceNames)!='': # skip tasks with no resource - most likely an RFQA or not interesting
    if proj2time(self.Project.Tasks.Item(i).Finish)-proj2time(self.Project.Tasks.Item(i).Start)>0.0: # skip zero duration tasks
    task = True
    if not self._Tasks.has_key(sn): # new task S/N
    self._Tasks[sn]=dict()
    self._Tasks[sn]['Name'] = str(self.Project.Tasks.Item(i).Name.encode('ascii', 'ignore'))
    self._Tasks[sn]['Priority'] = str(Priority.encode('ascii', 'ignore'))
    self._Tasks[sn]['ReleaseName'] = str(ReleaseName.encode('ascii', 'ignore'))
    self._Tasks[sn]['QCDB'] = str(QCDB.encode('ascii', 'ignore'))
    self._Tasks[sn]['QCRelease'] = str(QCRel.encode('ascii', 'ignore'))
    self._Tasks[sn]['Resources'] = list()
    for resr in self.Project.Tasks.Item(i).ResourceNames.split(','): # handle multiple testers on the same task
    self._Tasks[sn]['Resources'].append(str(resr.encode('ascii', 'ignore')))
    self._Tasks[sn]['id'] = list()
    self._Tasks[sn]['id'].append(i)
    self._Tasks[sn]['outline'] = list()
    self._Tasks[sn]['outline'].append(self.Project.Tasks.Item(i).OutlineNumber)
    self._Tasks[sn]['Start'] = proj2time(self.Project.Tasks.Item(i).Start)
    self._Tasks[sn]['Finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    self._Tasks[sn]['PercentWorkCompleteList'] = list()
    self._Tasks[sn]['PercentWorkCompleteList'].append(int(self.Project.Tasks.Item(i).PercentWorkComplete))
    else: # update existing task S/N
    for resr in self.Project.Tasks.Item(i).ResourceNames.split(','): # handle multiple testers on the same task
    if not str(resr) in self._Tasks[sn]['Resources']:
    self._Tasks[sn]['Resources'].append(str(resr.encode('ascii', 'ignore')))
    if self._Tasks[sn]['Start'] > proj2time(self.Project.Tasks.Item(i).Start):
    self._Tasks[sn]['Start'] = proj2time(self.Project.Tasks.Item(i).Start)
    if self._Tasks[sn]['Finish'] < proj2time(self.Project.Tasks.Item(i).Finish):
    self._Tasks[sn]['Finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    self._Tasks[sn]['PercentWorkCompleteList'].append(int(self.Project.Tasks.Item(i).PercentWorkComplete))
    if self._Tasks[sn]['id'].count(i)==0:
    self._Tasks[sn]['id'].append(i)
    if self._Tasks[sn]['outline'].count(self.Project.Tasks.Item(i).OutlineNumber)==0:
    self._Tasks[sn]['outline'].append(self.Project.Tasks.Item(i).OutlineNumber)
    else: # no resource - might be an RFQA item
    #if proj2time(self.Project.Tasks.Item(i).Finish)-proj2time(self.Project.Tasks.Item(i).Start)==0.0: # zero duration - better chance for being an RFQA item
    if self.Project.Tasks.Item(i).PredecessorTasks.Count == 0: # no predecessors - even better chance for being an RFQA item
    if self.Project.Tasks.Item(i).OutlineChildren.Count == 0: # no subtasks - even better chance for being an RFQA item
    rfqa = True
    if not self._RFQAs.has_key(sn): # new RFQA S/N
    self._RFQAs[sn]=dict()
    # keep updating with lowest items on the tasks list, as these are likely to be RFQA items
    self._RFQAs[sn]['Name'] = str(self.Project.Tasks.Item(i).Name.encode('ascii', 'ignore'))
    self._RFQAs[sn]['Start'] = proj2time(self.Project.Tasks.Item(i).Start)
    self._RFQAs[sn]['Finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    self._RFQAs[sn]['Priority'] = str(Priority.encode('ascii', 'ignore'))
    self._RFQAs[sn]['ReleaseName'] = str(ReleaseName.encode('ascii', 'ignore'))
    self._RFQAs[sn]['id'] = list()
    self._RFQAs[sn]['id'].append(i)
    if len(sns)>1: # keep a list of all lines that have multiple SNs
    if not SN in self._compoundTask.keys():
    self._compoundTask[SN]=list()
    self._compoundTask[SN].append(i)
    if not task and not rfqa: # keep a list of all tasks with serial number that were not handled as a task nor as an RFQA
    if not SN in self._unknowns.keys():
    self._unknowns[SN]=list()
    self._unknowns[SN].append(i)
    except AttributeError:
    continue # empty line in the file
    # compute average %complete for duplicated tasks
    for sn in self._Tasks.keys():
    tot = 0
    for p in self._Tasks[sn]['PercentWorkCompleteList']:
    tot += p
    self._Tasks[sn]['PercentWorkComplete'] = int(tot/len(self._Tasks[sn]['PercentWorkCompleteList']))
    return self._Tasks
    except Exception, details:
    err = time.asctime()
    err += ' Error in ' + traceback.extract_stack()[-1][2] + ' :\n'
    err += ' ' + str(details)
    err += traceback.format_tb(sys.exc_info()[2])[0]
    print "Error:", err
    return self._Tasks


    def __setitem__(self, key, values):
    "Example: m.Tasks['26123']={'codeComplete':37.0}"
    if not self._Tasks:
    print 'No MSProject file was loaded and parsed yet.\nPlease load a file and get tasks first.'
    return None
    elif type(values)!=type(dict()):
    print 'Task updated fields should be in a form of a dictionary.\nPossible keys are:',
    print self._msfields
    return None
    elif len(key)>2:
    if key[:2]=='SN':
    if self._Tasks.has_key(key[2:]):
    return self._update(key[2:], values)
    elif self._Tasks.has_key(key):
    return self._update(key, values)
    else:
    for i in range(1,self.Project.Tasks.Count+1):
    try:
    SN = self.Project.Tasks.Item(i).Text1 # This is the custom column where we currently store Accept360 S/N
    if SN == key:
    return self._update(key, values)
    except AttributeError:
    continue # empty line in the file
    print str(key) + ' was not found'
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, key))

    def updateTask(self, key, values):
    "Example: m.updateTask('26123', {'codeComplete':37.0})"
    if not self._Tasks:
    print 'No MSProject file was loaded and parsed yet.\nPlease load a file and get tasks first.'
    return None
    elif type(values)!=type(dict()):
    print 'Task updated fields should be in a form of a dictionary.\nPossible keys are:',
    print self._msfields
    return None
    elif len(key)>2:
    if key[:2]=='SN':
    if self._Tasks.has_key(key[2:]):
    return self._update(key[2:], values)
    elif self._Tasks.has_key(key):
    return self._update(key, values)
    else:
    for i in range(1,self.Project.Tasks.Count+1):
    try:
    SN = self.Project.Tasks.Item(i).Text1 # This is the custom column where we currently store Accept360 S/N
    if SN == key:
    return self._update(key, values)
    except AttributeError:
    continue # empty line in the file
    print str(key) + ' was not found'
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, key))

    def updateRFQA(self, key, values):
    "Example: m.updateRFQA('26123', {'Finish': '2012/1/2'})"
    if not self._RFQAs:
    print 'No MSProject file was loaded and parsed yet.\nPlease load a file and get tasks first.'
    return None
    elif type(values)!=type(dict()):
    print 'Task updated fields should be in a form of a dictionary.\nPossible keys are:',
    print self._msfields
    return None
    elif len(key)>2:
    if key[:2]=='SN':
    if self._RFQAs.has_key(key[2:]):
    return self._updateRFQA(key[2:], values)
    elif self._RFQAs.has_key(key):
    return self._updateRFQA(key, values)
    print str(key) + ' was not found'
    raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, key))

    def _update(self, key, values):
    updt = True
    found = False
    for i in self._Tasks[key]['id']:
    for k, v in values.iteritems():
    if not k in self._msfields:
    raise KeyError("Unknown or unsupported MSProject field: %s" % k)
    if debug: print 'updating:', i, k, v
    if not k in ['Priority', 'ReleaseName']:
    setattr(self.Project.Tasks.Item(i), k, v)
    elif k == 'Priority':
    setattr(self.Project.Tasks.Item(i), 'Text2', v)
    elif k == 'ReleaseName':
    setattr(self.Project.Tasks.Item(i), 'Text3', v)
    else:
    updt = False # we should never get here
    found = True
    if debug: print '\t>updated task at line:', i-1
    return updt & found

    def _updateRFQA(self, key, values):
    updt = True
    found = False
    for i in self._RFQAs[key]['id']:
    for k, v in values.iteritems():
    if not k in self._msfields:
    raise KeyError("Unknown or unsupported MSProject field: %s" % k)
    if debug: print 'updating:', i, k, v
    if not k in ['Priority', 'ReleaseName']:
    setattr(self.Project.Tasks.Item(i), k, v)
    elif k == 'Priority':
    setattr(self.Project.Tasks.Item(i), 'Text2', v)
    elif k == 'ReleaseName':
    setattr(self.Project.Tasks.Item(i), 'Text3', v)
    else:
    updt = False # we should never get here, probably better to raise exception
    found = True
    return updt & found

    def updateProgressPerResource(self, acceptSN, resource, PercentWorkComplete):
    """Update task progress for a given S/N and resource."""
    updt = True
    found = False
    for i in self._Tasks[acceptSN]['id']:
    if self.Project.Tasks.Item(i).ResourceNames == resource:
    setattr(self.Project.Tasks.Item(i), 'PercentWorkComplete', PercentWorkComplete)
    found = True
    if debug: print '\t>updated task at line:', i-1
    return updt & found

    def updateRFQADate(self, acceptSN, newDate, resetStart=False):
    """Update task RFQA date.
    By default RFQA start(=original) time is left untouched."""
    updt = True
    found = False
    for i in self._RFQAs[acceptSN]['id']:
    setattr(self.Project.Tasks.Item(i), 'Finish', datetime.datetime.strptime(newDate, '%Y/%m/%d'))
    if resetStart:
    setattr(self.Project.Tasks.Item(i), 'Start', datetime.datetime.strptime(newDate, '%Y/%m/%d'))
    found = True
    if debug: print '\t>updated RFQA at line:', i-1
    return updt & found

    def findRange(self, taskName, startID=1):
    """Return the ID range of subtasks of a given task name or task SN. Only the first task of that name is handled."""
    parentID = None
    for i in range(startID, self.Project.Tasks.Count+1):
    if self.Project.Tasks.Item(i):
    if self.Project.Tasks.Item(i).Name == str(taskName) or self.Project.Tasks.Item(i).Text1 == str(taskName):
    parentID = i
    parentLevel = self.Project.Tasks.Item(i).OutlineNumber + '.'
    break # we deal only with the first item found
    if not parentID:
    raise AttributeError("MSProject file has no task '%s'" % str(taskName))
    startID = parentID+1
    endID = parentID
    for i in range(startID, self.Project.Tasks.Count+1):
    if self.Project.Tasks.Item(i):
    if self.Project.Tasks.Item(i).OutlineNumber[0:len(parentLevel)] == parentLevel:
    endID = i
    else:
    break # no point to continue further
    if endID<startID:
    startID = parentID
    #print '*** No sub-tasks were found'
    return startID, endID


    def findSubRange(self, taskID):
    """Return the ID range of subtasks of a given task ID."""
    parentLevel = self.Project.Tasks.Item(taskID).OutlineNumber + '.'
    parentID = taskID
    startID = parentID+1
    endID = parentID
    for i in range(startID, self.Project.Tasks.Count+1):
    if self.Project.Tasks.Item(i):
    if self.Project.Tasks.Item(i).OutlineNumber[0:len(parentLevel)] == parentLevel:
    endID = i
    else:
    break # no point to continue further
    if endID<startID:
    startID = parentID
    #print '*** No sub-tasks were found'
    return startID, endID


    def buildAnalysisTree(self, task):
    """Create 2 trees of all analysis items, including their sub-tasks and information (start, end, progress).
    First tree is a dict with all AN keys. 2nd tree is a dict with all (py) categories.
    Code assumes that all subtasks of an AN item are of the same category"""
    tree = OrderedDict()
    cats = dict()
    trange = self.findRange(task)
    for i in range(trange[0]-1,trange[1]):
    sn = self.Project.Tasks.Item(i).Text1 # S/N column
    py = self.Project.Tasks.Item(i).Text4 # py column
    if sn.upper()[:3]=='AN-' and py.lower()!='ignore': # or i==(trange[0]-1):
    tree[sn]=dict()
    tree[sn]['subrange'] = self.findRange(sn, i)
    tree[sn]['start'] = proj2time(self.Project.Tasks.Item(i).Start)
    tree[sn]['finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    tree[sn]['category'] = py
    tree[sn]['totalWork'] = 0
    tree[sn]['actualWork'] = 0
    tree[sn]['percentWorkComplete'] = list()
    tree[sn]['subTasks'] = dict()
    if not py in cats:
    cats[py]=dict()
    cats[py]['subrange'] = list()
    cats[py]['AN'] = list()
    cats[py]['totalWork'] = 0
    cats[py]['actualWork'] = 0
    cats[py]['percentWorkComplete'] = list()
    cats[py]['subTasks'] = dict()
    cats[py]['start'] = proj2time(self.Project.Tasks.Item(i).Start)
    cats[py]['finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    cats[py]['AN'].append(sn)
    if cats[py]['start']<proj2time(self.Project.Tasks.Item(i).Start):
    cats[py]['start'] = proj2time(self.Project.Tasks.Item(i).Start)
    if proj2time(self.Project.Tasks.Item(i).Finish)>cats[py]['finish']:
    cats[py]['finish'] = proj2time(self.Project.Tasks.Item(i).Finish)
    for j in range(tree[sn]['subrange'][0],tree[sn]['subrange'][1]+1):
    if (self.Project.Tasks.Item(j).Text1 or self.Project.Tasks.Item(j).ResourceNames) and self.Project.Tasks.Item(j).Text4.lower()!='ignore':
    rsn = string.join(self.Project.Tasks.Item(j).ResourceNames.split(','),'')
    #s = self.findRange(self.Project.Tasks.Item(j).Text1, j)
    s = self.findSubRange(j)
    if s[0]==s[1]: # Work only on items that do not have sub-tasks
    sns = str(self.Project.Tasks.Item(j).Text1)+'_'+str(rsn)+'_'+str(j)
    tree[sn]['subTasks'][sns]=dict()
    tree[sn]['subTasks'][sns]['start'] = proj2time(self.Project.Tasks.Item(j).Start)
    tree[sn]['subTasks'][sns]['finish'] = proj2time(self.Project.Tasks.Item(j).Finish)
    tree[sn]['subTasks'][sns]['work'] = self.Project.Tasks.Item(j).Work
    tree[sn]['subTasks'][sns]['actual'] = self.Project.Tasks.Item(j).ActualWork
    tree[sn]['subTasks'][sns]['percent'] = self.Project.Tasks.Item(j).PercentWorkComplete
    tree[sn]['subTasks'][sns]['category'] = py
    if tree[sn]['subTasks'][sns]['start']<tree[sn]['start']:
    tree[sn]['start'] = tree[sn]['subTasks'][sns]['start']
    if tree[sn]['subTasks'][sns]['finish']>tree[sn]['finish']:
    tree[sn]['finish'] = tree[sn]['subTasks'][sns]['finish']
    tree[sn]['totalWork'] += tree[sn]['subTasks'][sns]['work']
    tree[sn]['actualWork'] += tree[sn]['subTasks'][sns]['actual']
    tree[sn]['percentWorkComplete'].append(int(tree[sn]['subTasks'][sns]['percent']))
    cats[py]['subTasks'][sns]=dict()
    cats[py]['subTasks'][sns]['start'] = proj2time(self.Project.Tasks.Item(j).Start)
    cats[py]['subTasks'][sns]['finish'] = proj2time(self.Project.Tasks.Item(j).Finish)
    cats[py]['subTasks'][sns]['work'] = self.Project.Tasks.Item(j).Work
    cats[py]['subTasks'][sns]['actual'] = self.Project.Tasks.Item(j).ActualWork
    cats[py]['subTasks'][sns]['percent'] = self.Project.Tasks.Item(j).PercentWorkComplete
    cats[py]['subTasks'][sns]['AN'] = sn
    if cats[py]['subTasks'][sns]['start']<cats[py]['start']:
    cats[py]['start'] = cats[py]['subTasks'][sns]['start']
    if cats[py]['subTasks'][sns]['finish']>cats[py]['finish']:
    cats[py]['finish'] = cats[py]['subTasks'][sns]['finish']
    cats[py]['totalWork'] += cats[py]['subTasks'][sns]['work']
    cats[py]['actualWork'] += cats[py]['subTasks'][sns]['actual']
    cats[py]['percentWorkComplete'].append(int(cats[py]['subTasks'][sns]['percent']))
    return tree, cats


    def findDeadline(self, taskID):
    """Find deadline attribute for a give task."""
    try:
    pyt = self.Project.Tasks.Item(taskID).Deadline.Format('%Y/%m/%d')
    except AttributeError:
    pyt = self.Project.Tasks.Item(taskID).Finish.Format('%Y/%m/%d')
    pyp = self.Project.Tasks.Item(taskID).PercentWorkComplete
    pyo = self.Project.Tasks.Item(taskID).OutlineNumber
    pyol = len(pyo.split('.'))
    return pyt, pyp, pyol