improve
[odoo/odoo.git] / addons / project / project.py
index cd1d43b..33843b7 100644 (file)
@@ -1,8 +1,9 @@
+# -*- encoding: utf-8 -*-
 ##############################################################################
 #
-# Copyright (c) 2005-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
+# Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
 #
-# $Id: project.py 1011 2005-07-26 08:11:45Z nicoe $
+# $Id$
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
 #
 ##############################################################################
 
+from lxml import etree
 from mx import DateTime
 from mx.DateTime import now
 import time
 
-import netsvc
 from osv import fields, osv
-import ir
 
 class project(osv.osv):
-       _name = "project.project"
-       _description = "Project"
-
-       def _calc_effective(self, cr, uid, ids, name, args, context):
-               ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
-               res_sum = {}
-               if ids2:
-                       proj_set = ','.join(map(str, ids2))
-                       cr.execute(("SELECT t.project_id, COALESCE(SUM(w.hours),0) FROM project_task t LEFT JOIN project_task_work w on (w.task_id = t.id) WHERE t.project_id in (%s) GROUP BY project_id") % (proj_set,))
-                       for project_id, sum in cr.fetchall():
-                               res_sum[project_id] = sum
-               res={}
-               for id in ids:
-                       ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
-                       res.setdefault(id, 0.0)
-                       for idx in ids3:
-                               res[id] += res_sum.get(idx, 0.0)
-               return res
-
-       def _calc_planned(self, cr, uid, ids, name, args, context):
-               ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
-               res_sum = {}
-               if ids2:
-                       proj_set = ','.join(map(str, ids2))
-                       cr.execute(("SELECT project_id, COALESCE(SUM(planned_hours),0) FROM project_task WHERE project_id IN (%s) GROUP BY project_id") % (proj_set,))
-                       for project_id, sum in cr.fetchall():
-                               res_sum[project_id] = sum
-               res = {}
-               for id in ids:
-                       ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
-                       res.setdefault(id, 0.0)
-                       for idx in ids3:
-                               res[id] += res_sum.get(idx, 0.0)
-               return res
-
-       def _check_recursion(self, cr, uid, ids):
-               level = 100
-               while len(ids):
-                       cr.execute('select distinct parent_id from project_project where id in ('+','.join(map(str, ids))+')')
-                       ids = filter(None, map(lambda x:x[0], cr.fetchall()))
-                       if not level:
-                               return False
-                       level -= 1
-               return True
-
-       def onchange_partner_id(self, cr, uid, ids, part):
-               if not part:
-                       return {'value':{'contact_id': False, 'pricelist_id': False}}
-               addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
-               print part, self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist
-
-               pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist[0]
-               return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
-
-       _columns = {
-                'name': fields.char("Project name", size=128, required=True),
-                'active': fields.boolean('Active'),
-                'category_id': fields.many2one('account.analytic.account','Analytic Account'),
-                'priority': fields.integer('Priority'),
-                'manager': fields.many2one('res.users', 'Project manager'),
-                'warn_manager': fields.boolean('Warn manager'),
-                'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project members'),
-                'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
-                'parent_id': fields.many2one('project.project', 'Parent project'),
-                'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
-                'planned_hours': fields.function(_calc_planned, method=True, string='Planned hours'),
-                'effective_hours': fields.function(_calc_effective, method=True, string='Hours spent'),
-                'date_start': fields.date('Project started on'),
-                'date_end': fields.date('Project should end on'),
-                'tariff': fields.float('Sales price'),
-                'mode': fields.selection([('project', 'By project'), ('hour', 'By hour'), ('effective', 'By effective hour')], 'Price setting mode'),
-                'partner_id': fields.many2one('res.partner', 'Customer'),
-                'contact_id': fields.many2one('res.partner.address', 'Contact'),
-                'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
-                'tax_ids': fields.many2many('account.tax', 'project_account_tax_rel', 'project_id','tax_id', 'Applicable taxes'),
-                'warn_customer': fields.boolean('Warn customer'),
-                'warn_header': fields.text('Mail header'),
-                'warn_footer': fields.text('Mail footer'),
-                'notes': fields.text('Notes'),
-                'timesheet_id': fields.many2one('hr.timesheet.group', 'Working hours'),
-               'state': fields.selection([('open', 'Open'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True),
-        }
-       
-       _defaults = {
-               'active': lambda *a: True,
-               'manager': lambda object,cr,uid,context: uid,
-               'priority': lambda *a: 1,
-               'date_start': lambda *a: time.strftime('%Y-%m-%d'),
-               'state': lambda *a: 'open'
-       }
-       
-       _order = "priority"
-       _constraints = [
-               (_check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
-       ]
-
-       # toggle activity of projects, their sub projects and their tasks
-       def toggleActive(self, cr, uid, ids, context={}):
-               for proj in self.browse(cr, uid, ids, context):
-                       self.setActive(cr, uid, proj.id, not proj.active, context)
-               return True
-
-       # set active value for a project, its sub projects and its tasks
-       def setActive(self, cr, uid, id, value, context={}):
-               proj = self.browse(cr, uid, id, context)
-               self.write(cr, uid, [id], {'active': value}, context)
-               cr.execute('select id from project_task where project_id=%d', (proj.id,))
-               tasks_id = [x[0] for x in cr.fetchall()]
-               self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
-               project_ids = [x[0] for x in cr.fetchall()]
-               for child in project_ids:
-                       self.setActive(cr, uid, child, value, context)
-               return True
+    _name = "project.project"
+    _description = "Project"
+
+    def _calc_effective(self, cr, uid, ids, name, args, context):
+        ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
+        res_sum = {}
+        if ids2:
+            cr.execute('SELECT t.project_id, COALESCE(SUM(w.hours),0) \
+                    FROM project_task t \
+                        LEFT JOIN project_task_work w \
+                            ON (w.task_id = t.id) \
+                    WHERE t.project_id in (' + ','.join([str(x) for x in ids2]) + ') \
+                        AND active \
+                    GROUP BY project_id')
+            for project_id, sum in cr.fetchall():
+                res_sum[project_id] = sum
+        res={}
+        for id in ids:
+            ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
+            res.setdefault(id, 0.0)
+            for idx in ids3:
+                res[id] += res_sum.get(idx, 0.0)
+        return res
+
+    def _calc_planned(self, cr, uid, ids, name, args, context):
+        ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
+        res_sum = {}
+        if ids2:
+            cr.execute('SELECT project_id, COALESCE(SUM(total_hours),0) \
+                    FROM project_task \
+                    WHERE project_id IN (' + ','.join([str(x) for x in ids2]) + ') \
+                        AND active \
+                    GROUP BY project_id')
+            for project_id, sum in cr.fetchall():
+                res_sum[project_id] = sum
+        res = {}
+        for id in ids:
+            ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
+            res.setdefault(id, 0.0)
+            for idx in ids3:
+                res[id] += res_sum.get(idx, 0.0)
+        return res
+
+    def check_recursion(self, cursor, user, ids, parent=None):
+        return super(project, self).check_recursion(cursor, user, ids,
+                parent=parent)
+
+    def onchange_partner_id(self, cr, uid, ids, part):
+        if not part:
+            return {'value':{'contact_id': False, 'pricelist_id': False}}
+        addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
+
+        pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
+        return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
+
+    def _progress_rate(self, cr, uid, ids, name, arg, context=None):
+        res = {}.fromkeys(ids, 0.0)
+        if not ids:
+            return res
+        cr.execute('''SELECT
+                project_id, sum(progress*total_hours), sum(total_hours) 
+            FROM
+                project_task 
+            WHERE
+                project_id in ('''+','.join(map(str,ids))+''') AND
+                state<>'cancelled'
+            GROUP BY
+                project_id''')
+        for id,prog,tot in cr.fetchall():
+            if tot:
+                res[id] = prog / tot
+        return res
+
+    _columns = {
+        'name': fields.char("Project Name", size=128, required=True),
+        'active': fields.boolean('Active'),
+        'category_id': fields.many2one('account.analytic.account','Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It ables to connect projects with budgets, plannings, costs and revenues analysis, timesheet on projects, etc."),
+        'priority': fields.integer('Sequence'),
+        'manager': fields.many2one('res.users', 'Project Manager'),
+        'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team."),
+        'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members'),
+        'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
+        'parent_id': fields.many2one('project.project', 'Parent Project'),
+        'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
+        'planned_hours': fields.function(_calc_planned, method=True, string='Planned hours'),
+        'effective_hours': fields.function(_calc_effective, method=True, string='Hours spent'),
+        'progress_rate': fields.function(_progress_rate, method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
+        'date_start': fields.date('Starting Date'),
+        'date_end': fields.date('Expected End'),
+        'partner_id': fields.many2one('res.partner', 'Partner'),
+        'contact_id': fields.many2one('res.partner.address', 'Contact'),
+        'warn_customer': fields.boolean('Warn Partner'),
+        'warn_header': fields.text('Mail header'),
+        'warn_footer': fields.text('Mail footer'),
+        'notes': fields.text('Notes'),
+        'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
+        'state': fields.selection([('template', 'Template'), ('open', 'Open'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True),
+     }
+
+    _defaults = {
+        'active': lambda *a: True,
+        'manager': lambda object,cr,uid,context: uid,
+        'priority': lambda *a: 1,
+        'date_start': lambda *a: time.strftime('%Y-%m-%d'),
+        'state': lambda *a: 'open'
+    }
+
+    _order = "priority"
+    _constraints = [
+        (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
+    ]
+
+    # toggle activity of projects, their sub projects and their tasks
+    def set_template(self, cr, uid, ids, context={}):
+        res = self.setActive(cr, uid, ids, value=False, context=context) 
+        return res
+
+    def set_done(self, cr, uid, ids, context={}):
+        self.write(cr, uid, ids, {'state':'done'}, context=context)
+        return True
+
+    def set_cancel(self, cr, uid, ids, context={}):
+        self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
+        return True
+
+    def set_pending(self, cr, uid, ids, context={}):
+        self.write(cr, uid, ids, {'state':'pending'}, context=context)
+        return True
+
+    def set_open(self, cr, uid, ids, context={}):
+        self.write(cr, uid, ids, {'state':'open'}, context=context)
+        return True
+
+    def reset_project(self, cr, uid, ids, context={}):
+        res = self.setActive(cr, uid, ids,value=True, context=context)
+        return res
+
+    def copy(self, cr, uid, id, default={},context={}):
+        default = default or {}
+        default['tasks'] = []
+        default['child_id'] = []
+        return super(project, self).copy(cr, uid, id, default, context)
+
+    def duplicate_template(self, cr, uid, ids,context={}):
+        for proj in self.browse(cr, uid, ids):
+            parent_id=context.get('parent_id',False)
+            new_id=self.pool.get('project.project').copy(cr, uid, proj.id,default={'name':proj.name+_(' (copy)'),'state':'open','parent_id':parent_id})
+            cr.execute('select id from project_task where project_id=%d', (proj.id,))
+            res = cr.fetchall()
+            for (tasks_id,) in res:
+                self.pool.get('project.task').copy(cr, uid, tasks_id,default={'project_id':new_id,'active':True}, context=context)
+            cr.execute('select id from project_project where parent_id=%d', (proj.id,))
+            res = cr.fetchall()
+            project_ids = [x[0] for x in res]
+            for child in project_ids:
+                self.duplicate_template(cr, uid, [child],context={'parent_id':new_id}) 
+
+        # TODO : Improve this to open the new project (using a wizard)
+
+        cr.commit()
+        raise osv.except_osv(_('Operation Done'), _('A new project has been created !\nWe suggest you to close this one and work on this new project.'))
+
+    # set active value for a project, its sub projects and its tasks
+    def setActive(self, cr, uid, ids, value=True, context={}):   
+        for proj in self.browse(cr, uid, ids, context):            
+            self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
+            cr.execute('select id from project_task where project_id=%d', (proj.id,))
+            tasks_id = [x[0] for x in cr.fetchall()]
+            if tasks_id:
+                self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
+            cr.execute('select id from project_project where parent_id=%d', (proj.id,))            
+            project_ids = [x[0] for x in cr.fetchall()]            
+            for child in project_ids:
+                self.setActive(cr, uid, [child], value, context)               
+        return True
 project()
 
 class project_task_type(osv.osv):
-       _name = 'project.task.type'
-       _description = 'Project task type'
-       _columns = {
-               'name': fields.char('Type', required=True, size=64),
-               'description': fields.text('Description'),
-       }
+    _name = 'project.task.type'
+    _description = 'Project task type'
+    _columns = {
+        'name': fields.char('Type', required=True, size=64, translate=True),
+        'description': fields.text('Description'),
+    }
 project_task_type()
 
 class task(osv.osv):
-       _name = "project.task"
-       _description = "Task"
-       def _hours_effect(self, cr, uid, ids, name, args, context):
-               task_set = ','.join(map(str, ids))
-               cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
-               res = {}
-               for id in ids:
-                       res[id] = 0.0
-               for task_id, sum in cr.fetchall():
-                       res[task_id] = sum
-               return res
-
-       _columns = {
-               'name': fields.char('Task summary', size=128, required=True),
-               'active': fields.boolean('Active'),
-               'description': fields.text('Description'),
-               'cust_desc': fields.text("Description for the customer"),
-               'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
-               'sequence': fields.integer('Sequence'),
-               'type': fields.many2one('project.task.type', 'Type'),
-               'state': fields.selection([('draft', 'Draft'),('open', 'Open'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State'),
-               'date_start': fields.datetime('Date Start'),
-               'date_deadline': fields.datetime('Deadline'),
-               'date_close': fields.datetime('Date Closed', readonly=True),
-               'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
-               'notes': fields.text('Notes'),
-               'start_sequence': fields.boolean('Wait for previous sequences'),
-               'planned_hours': fields.float('Planned hours'),
-               'effective_hours': fields.function(_hours_effect, method=True, string='Effective hours'),
-               'progress': fields.integer('Progress (0-100)'),
-               'billable': fields.boolean('To be invoiced'),
-               'invoice_id': fields.many2one('account.invoice','Generated Invoice'),
-               'user_id': fields.many2one('res.users', 'Assigned to'),
-               'partner_id': fields.many2one('res.partner', 'Customer'),
-               'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
-               'procurement_id': fields.many2one('mrp.procurement', 'Procurement', ondelete='set null')
-       }
-       _defaults = {
-               'user_id': lambda obj,cr,uid,context: uid,
-               'state': lambda *a: 'open',
-               'priority': lambda *a: '2',
-               'progress': lambda *a: 0,
-               'sequence': lambda *a: 10,
-               'active': lambda *a: True,
-       }
-       _order = "state, sequence, priority, date_deadline, id"
-
-       def do_close(self, cr, uid, ids, *args):
-               request = self.pool.get('res.request')
-               tasks = self.browse(cr, uid, ids)
-               for task in tasks:
-                       project = task.project_id
-                       if project: 
-                               if project.warn_manager and project.manager and (project.manager.id != uid):
-                                       request.create(cr, uid, {
-                                               'name': "Task '%s' closed" % task.name,
-                                               'state': 'waiting',
-                                               'act_from': uid,
-                                               'act_to': project.manager.id,
-                                               'ref_partner_id': task.partner_id.id,
-                                               'ref_doc1': 'project.task,%d'% (task.id,),
-                                               'ref_doc2': 'project.project,%d'% (project.id,),
-                                       })
-                       self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'progress': 100})
-                       if task.procurement_id:
-                               wf_service = netsvc.LocalService("workflow")
-                               wf_service.trg_validate(uid, 'mrp.procurement', task.procurement_id.id, 'subflow.done', cr)
-               return True
-
-       def do_reopen(self, cr, uid, ids, *args):
-               request = self.pool.get('res.request')
-               tasks = self.browse(cr, uid, ids)
-               for task in tasks:
-                       project = task.project_id
-                       if project and project.warn_manager and project.manager.id and (project.manager.id != uid):
-                               request.create(cr, uid, {
-                                       'name': "Task '%s' reopened" % task.name,
-                                       'state': 'waiting',
-                                       'act_from': uid,
-                                       'act_to': project.manager.id,
-                                       'ref_partner_id': task.partner_id.id,
-                                       'ref_doc1': 'project.task,%d' % task.id,
-                                       'ref_doc2': 'project.project,%d' % project.id,
-                               })
-
-                       self.write(cr, uid, [task.id], {'state': 'open'})
-               return True
-
-       def do_cancel(self, cr, uid, ids, *args):
-               request = self.pool.get('res.request')
-               tasks = self.browse(cr, uid, ids)
-               for task in tasks:
-                       project = task.project_id
-                       if project.warn_manager and project.manager and (project.manager.id != uid):
-                               request.create(cr, uid, {
-                                       'name': "Task '%s' cancelled" % task.name,
-                                       'state': 'waiting',
-                                       'act_from': uid,
-                                       'act_to': project.manager.id,
-                                       'ref_partner_id': task.partner_id.id,
-                                       'ref_doc1': 'project.task,%d' % task.id,
-                                       'ref_doc2': 'project.project,%d' % project.id,
-                               })
-                       self.write(cr, uid, [task.id], {'state': 'cancelled'})
-                       if task.procurement_id:
-                               wf_service = netsvc.LocalService("workflow")
-                               wf_service.trg_validate(uid, 'mrp.procurement', task.procurement_id.id, 'subflow.cancel', cr)
-               return True
-
-       def do_open(self, cr, uid, ids, *args):
-               tasks= self.browse(cr,uid,ids)
-               for t in tasks:
-                       self.write(cr, uid, [t.id], {'state': 'open','date_start':t.date_start or time.strftime('%Y-%m-%d %H:%M:%S')})
-               return True
-
-       def do_draft(self, cr, uid, ids, *args):
-               self.write(cr, uid, ids, {'state': 'draft'})
-               return True
-
-
-       def do_pending(self, cr, uid, ids, *args):
-               self.write(cr, uid, ids, {'state': 'pending'})
-               return True
-
-               
+    _name = "project.task"
+    _description = "Task"
+    _date_name = "date_start"
+    def _str_get(self, task, level=0, border='***', context={}):
+        return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
+            border[0]+' '+(task.name or '')+'\n'+ \
+            (task.description or '')+'\n\n'
+
+    def _history_get(self, cr, uid, ids, name, args, context={}):
+        result = {}
+        for task in self.browse(cr, uid, ids, context=context):
+            result[task.id] = self._str_get(task, border='===')
+            t2 = task.parent_id
+            level = 0
+            while t2:
+                level -= 1
+                result[task.id] = self._str_get(t2, level) + result[task.id]
+                t2 = t2.parent_id
+            t3 = map(lambda x: (x,1), task.child_ids)
+            while t3:
+                t2 = t3.pop(0)
+                result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
+                t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
+        return result
+
+# Compute: effective_hours, total_hours, progress
+    def _hours_get(self, cr, uid, ids, field_names, args, context):
+        task_set = ','.join(map(str, ids))
+        cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
+        hours = dict(cr.fetchall())
+        res = {}
+        for task in self.browse(cr, uid, ids, context=context):
+            res[task.id] = {}
+            res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
+            res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
+            if (task.remaining_hours + hours.get(task.id, 0.0)):
+                res[task.id]['progress'] = min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100)
+            else:
+                res[task.id]['progress'] = 0.0
+            res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
+        return res
+
+    def onchange_planned(self, cr, uid, ids, planned, effective):
+        return {'value':{'remaining_hours': planned-effective}}
+
+    #_sql_constraints = [
+    #    ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
+    #]
+
+    _columns = {
+        'active': fields.boolean('Active'),
+        'name': fields.char('Task summary', size=128, required=True),
+        'description': fields.text('Description'),
+        'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
+        'sequence': fields.integer('Sequence'),
+        'type': fields.many2one('project.task.type', 'Type'),
+        'state': fields.selection([('draft', 'Draft'),('open', 'Open'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'Status', readonly=True, required=True),
+        'date_start': fields.datetime('Date Opened'),
+        'date_deadline': fields.datetime('Deadline'),
+        'date_close': fields.datetime('Date Closed', readonly=True),
+        'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
+        'parent_id': fields.many2one('project.task', 'Parent Task'),
+        'child_ids': fields.one2many('project.task', 'parent_id', 'Delegated Tasks'),
+        'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
+        'notes': fields.text('Notes'),
+
+        'planned_hours': fields.float('Planned Hours', readonly=True, states={'draft':[('readonly',False)]}, required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
+        'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
+        'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
+        'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
+        'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
+        'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as: Total Time - Estimated Time. It gives the difference of the time estimated by the project manager and the real time to close the task."),
+
+        'user_id': fields.many2one('res.users', 'Assigned to'),
+        'partner_id': fields.many2one('res.partner', 'Partner'),
+        'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done', readonly=False, states={'draft':[('readonly',True)]}),
+    }
+    _defaults = {
+        'user_id': lambda obj,cr,uid,context: uid,
+        'state': lambda *a: 'draft',
+        'priority': lambda *a: '2',
+        'progress': lambda *a: 0,
+        'sequence': lambda *a: 10,
+        'active': lambda *a: True,
+        'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
+    }
+    _order = "sequence, priority, date_deadline, id"
+
+    #
+    # Override view according to the company definition
+    #
+    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False):
+        tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode
+        f = self.pool.get('res.company').fields_get(cr, uid, ['project_time_mode'], context)
+        word = dict(f['project_time_mode']['selection'])[tm]
+
+        res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar)
+        if tm=='hours':
+            return res
+        eview = etree.fromstring(res['arch'])
+        def _check_rec(eview, tm):
+            if eview.attrib.get('widget',False) == 'float_time':
+                eview.set('widget','float')
+            for child in eview:
+                _check_rec(child, tm)
+            return True
+        _check_rec(eview, tm)
+        res['arch'] = etree.tostring(eview)
+        for f in res['fields']:
+            if 'Hours' in res['fields'][f]['string']:
+                res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',word)
+        return res
+
+    def do_close(self, cr, uid, ids, *args):
+        request = self.pool.get('res.request')
+        tasks = self.browse(cr, uid, ids)
+        for task in tasks:
+            project = task.project_id
+            if project:
+                if project.warn_manager and project.manager and (project.manager.id != uid):
+                    request.create(cr, uid, {
+                        'name': "Task '%s' closed" % task.name,
+                        'state': 'waiting',
+                        'act_from': uid,
+                        'act_to': project.manager.id,
+                        'ref_partner_id': task.partner_id.id,
+                        'ref_doc1': 'project.task,%d'% (task.id,),
+                        'ref_doc2': 'project.project,%d'% (project.id,),
+                    })
+            self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
+            if task.parent_id and task.parent_id.state in ('pending','draft'):
+                self.do_reopen(cr, uid, [task.parent_id.id])
+        return True
+
+    def do_reopen(self, cr, uid, ids, *args):
+        request = self.pool.get('res.request')
+        tasks = self.browse(cr, uid, ids)
+        for task in tasks:
+            project = task.project_id
+            if project and project.warn_manager and project.manager.id and (project.manager.id != uid):
+                request.create(cr, uid, {
+                    'name': "Task '%s' reopened" % task.name,
+                    'state': 'waiting',
+                    'act_from': uid,
+                    'act_to': project.manager.id,
+                    'ref_partner_id': task.partner_id.id,
+                    'ref_doc1': 'project.task,%d' % task.id,
+                    'ref_doc2': 'project.project,%d' % project.id,
+                })
+
+            self.write(cr, uid, [task.id], {'state': 'open'})
+        return True
+
+    def do_cancel(self, cr, uid, ids, *args):
+        request = self.pool.get('res.request')
+        tasks = self.browse(cr, uid, ids)
+        for task in tasks:
+            project = task.project_id
+            if project.warn_manager and project.manager and (project.manager.id != uid):
+                request.create(cr, uid, {
+                    'name': "Task '%s' cancelled" % task.name,
+                    'state': 'waiting',
+                    'act_from': uid,
+                    'act_to': project.manager.id,
+                    'ref_partner_id': task.partner_id.id,
+                    'ref_doc1': 'project.task,%d' % task.id,
+                    'ref_doc2': 'project.project,%d' % project.id,
+                })
+            self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
+        return True
+
+    def do_open(self, cr, uid, ids, *args):
+        tasks= self.browse(cr,uid,ids)
+        for t in tasks:
+            self.write(cr, uid, [t.id], {'state': 'open'})
+        return True
+
+    def do_draft(self, cr, uid, ids, *args):
+        self.write(cr, uid, ids, {'state': 'draft'})
+        return True
+
+
+    def do_pending(self, cr, uid, ids, *args):
+        self.write(cr, uid, ids, {'state': 'pending'})
+        return True
+
+
 task()
 
 class project_work(osv.osv):
-       _name = "project.task.work"
-       _description = "Task Work"
-       _columns = {
-               'name': fields.char('Work summary', size=128),
-               'date': fields.datetime('Date'),
-               'task_id': fields.many2one('project.task', 'Task', ondelete='cascade'),
-               'hours': fields.float('Hours spent'),
-               'user_id': fields.many2one('res.users', 'Done by', required=True),
-       }
-       _defaults = {
-               'user_id': lambda obj,cr,uid,context: uid,
-               'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
-       }
-       _order = "date desc"
+    _name = "project.task.work"
+    _description = "Task Work"
+    _columns = {
+        'name': fields.char('Work summary', size=128),
+        'date': fields.datetime('Date'),
+        'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
+        'hours': fields.float('Time Spent'),
+        'user_id': fields.many2one('res.users', 'Done by', required=True),
+    }
+    _defaults = {
+        'user_id': lambda obj,cr,uid,context: uid,
+        'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
+    }
+    _order = "date desc"
+    def create(self, cr, uid, vals, *args, **kwargs):
+        if 'task_id' in vals:
+            cr.execute('update project_task set remaining_hours=remaining_hours+%.2f where id=%d', (-vals.get('hours',0.0), vals['task_id']))
+        return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
+
+    def write(self, cr, uid, ids,vals,context={}):
+        for work in self.browse(cr, uid, ids, context):
+            cr.execute('update project_task set remaining_hours=remaining_hours+%.2f+(%.2f) where id=%d', (-vals.get('hours',0.0), work.hours, work.task_id.id))
+        return super(project_work,self).write(cr, uid, ids, vals, context)
+
+    def unlink(self, cr, uid, ids, *args, **kwargs):
+        for work in self.browse(cr, uid, ids):
+            cr.execute('update project_task set remaining_hours=remaining_hours+%.2f where id=%d', (work.hours, work.task_id.id))
+        return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
 project_work()
 
 
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+