Modify

Opened 8 months ago

#11549 new enhancement

introduced fields.remain in tracpm.py

Reported by: falkb Owned by: ChrisNelson
Priority: normal Component: TracJsGanttPlugin
Severity: normal Keywords:
Cc: Trac Release:

Description

I use Trac with these additional custom ticket fields:

  1. estimatedhours.label = originally planned time
  2. remaininghours.label = currently estimated rest time
  3. totalhours.label = sum of worked time

I set in trac.ini

[TracPM]
fields.estimate = estimatedhours
fields.worked = totalhours

but this patch allows me setting also the remaininghours for the Gantt chart to consider:

[TracPM]
fields.estimate = estimatedhours
fields.worked = totalhours
fields.remain = remaininghours

The patch is here:

!diff
Index: tracpm.py
===================================================================
--- tracpm.py	(revision 13386)
+++ tracpm.py	(working copy)
@@ -150,6 +150,8 @@
            """Ticket field to use as the data source for estimated work""")
     Option(cfgSection, 'fields.worked', None,
            """Ticket field to use as the data source for completed work""")
+    Option(cfgSection, 'fields.remain', None,
+           """Ticket field to use as the data source for remaining work""")
     Option(cfgSection, 'fields.start', None, 
            """Ticket field to use as the data source for start date""")
     Option(cfgSection, 'fields.finish', None, 
@@ -187,7 +189,7 @@
         self.sources = {}
 
         # All the data sources that may come from custom fields.
-        fields = ('percent', 'estimate', 'worked', 'start', 'finish',
+        fields = ('percent', 'estimate', 'worked', 'remain', 'start', 'finish',
                   'pred', 'succ', 'parent')
 
         # All the data sources that may come from external relations.
@@ -501,11 +503,11 @@
             est = 0.0
         # Closed tickets took as long as they took
         elif ticket['status'] == 'closed' and work:
-            est = work
+            est = 0 # work
         # If the task is over its estimate, assume it will take
         # pad more time
-        elif work > est:
-            est = work + self.estPad
+        #elif work > est:
+        #    est = work + self.estPad
         # If unestimated, use the default
         elif not est or est == 0:
             est = self.dftEst
@@ -540,7 +542,9 @@
                     percent = 0
                 else:
                     worked = float(worked)
-                    percent = '%s/%s' % (worked, estimate)
+                    prcnt = worked * 100.0 / (float(estimate) + worked)
+                    percent = int(prcnt)
+                    #self.env.log.info('#### percent(%s) = (worked=%d, estimate=%d) : %d' % (ticket['id'], worked, estimate, percent))
         # Use percent if provided
         elif self.isCfg('percent'):
             try:
@@ -794,6 +798,9 @@
         if self.isCfg('worked'):
             ticket[self.fields['worked']] = 0
 
+        if self.isCfg('remain'):
+            ticket[self.fields['remain']] = 0
+
         # There is no percent complete for a pseudoticket
         if self.isCfg('percent'):
             ticket[self.fields['percent']] = 0
@@ -820,11 +827,11 @@
         else:
             milestones = []
 
-        for t in tickets:
-            if 'milestone' in t and \
-                    t['milestone'] != '' and \
-                    t['milestone'] not in milestones:
-                milestones.append(t['milestone'])
+            for t in tickets:
+                if 'milestone' in t and \
+                        t['milestone'] != '' and \
+                        t['milestone'] not in milestones:
+                    milestones.append(t['milestone'])
 
         # Need a unique ID for each task.
         if len(milestones) > 0:
@@ -1008,7 +1015,7 @@
             nullable = [ 'pred', 'succ', 
                          'start', 'finish', 
                          'parent', 
-                         'worked', 'estimate', 'percent' ]
+                         'worked', 'estimate', 'remain', 'percent' ]
             for field in nullable:
                 if self.isField(field):
                     fieldName = self.fields[self.sources[field]]
@@ -1841,7 +1848,12 @@
                 # Set the field
                 t['_calc_finish'] = finish
 
+            start_based_on_work = False
+            overruled_start = None
+            start = None
+
             if t.get('_calc_start') == None:
+                hours = 0
                 # If work has begun, the start is the actual start.
                 if t.get('_actual_start') and options.get('useActuals'):
                     start = [ to_datetime(t['_actual_start']), True ]
@@ -1856,6 +1868,7 @@
                 # Otherwise, the start is based on the finish and the
                 # work to be done before then.
                 else:
+                    start_based_on_work = True
                     hours = self.pm.workHours(t)
                     start = t['_calc_finish'][0] + \
                         _calendarOffset(t, 
@@ -1865,9 +1878,27 @@
 
                 t['_calc_start'] = start
 
+                if start_based_on_work:
+                    if self.pm.isCfg('remain') and t.get(self.pm.fields['remain']) != None:
+                        remain_str = t[self.pm.fields['remain']]
+                        if remain_str != '':
+                            remaininghours = float(remain_str)
+                        else:
+                            remaininghours = 0.0
+
+                        start = t['_calc_finish'][0] + \
+                            _calendarOffset(t, 
+                                            -1*(remaininghours), 
+                                            t['_calc_finish'][0])
+                        start = [start, t['_calc_finish'][1]]
+                        overruled_start = start
+                        #t['_calc_start'] = overruled_start
+
                 # Adjust implicit finish for explicit start
                 if _betterDate(start, finish):
                     hours = self.pm.workHours(t)
+                    if (overruled_start):
+                        hours = remaining_hours
                     finish[0] = start[0] + _calendarOffset(t,
                                                            hours,
                                                            start[0])
@@ -1876,13 +1907,19 @@
 
             # Remember the limit for open tickets
             if t['status'] != 'closed':
+                if start is None:
+                    start = t['_calc_start']
                 limit = self.limits.get(t['owner'])
-                if not limit or limit > t['_calc_start'][0]:
-                    self.limits[t['owner']] = t['_calc_start'][0]
+                if not limit or limit > start[0]:
+                    self.limits[t['owner']] = start[0]
 
             self.taskStack.pop()
 
-            return t['_calc_start']
+            if overruled_start:
+                #self.env.log.warning('ticket=%s' % t['id'])
+                return overruled_start
+            else:
+                return t['_calc_start']
 
         # Schedule a task As Soon As Possible
         # Return the finish of the task as a date object

Now the length of a ticket bar is equal to 'estimatedhours' but tickets can overlap because resource leveling will consider the 'remaininghours'.

Remaining issue: For proper ressource leveling, I don't get it work that closed tickets have the original bar length of 'estimatedhours'.

Attachments (0)

Change History (0)

Add Comment

Modify Ticket

Action
as new .
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.