[MERGE]: Merge with lp:~openerp-dev/openobject-addons/trunk-import_sugarcrm
[odoo/odoo.git] / addons / project / project.py
index b2dedfd..14ee94e 100644 (file)
@@ -126,29 +126,21 @@ class project(osv.osv):
         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
         '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
 
-        'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose, but a user has to be member of a project to add a the to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
+        'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
+            help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
             store = {
                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
             }),
-        'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
-            store = {
-                'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
-                'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
-            }),
+        'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
             store = {
                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
             }),
-        'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
-            store = {
-                'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
-                'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
-                'project.task.work': (_get_project_work, ['hours'], 10),
-            }),
+        'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
         'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
         'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
         'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
@@ -213,20 +205,38 @@ class project(osv.osv):
         if context is None:
             context = {}
 
-        proj = self.browse(cr, uid, id, context=context)
         default = default or {}
         context['active_test'] = False
         default['state'] = 'open'
+        proj = self.browse(cr, uid, id, context=context)
         if not default.get('name', False):
             default['name'] = proj.name + _(' (copy)')
+
         res = super(project, self).copy(cr, uid, id, default, context)
+        return res
+
+
+    def template_copy(self, cr, uid, id, default={}, context=None):
+        task_obj = self.pool.get('project.task')
+        proj = self.browse(cr, uid, id, context=context)
+
+        default['tasks'] = [] #avoid to copy all the task automaticly
+        res = self.copy(cr, uid, id, default=default, context=context)
+
+        #copy all the task manually
+        map_task_id = {}
+        for task in proj.tasks:
+            map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
+
+        self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
+        task_obj.duplicate_task(cr, uid, map_task_id, context=context)
 
         return res
 
     def duplicate_template(self, cr, uid, ids, context=None):
         if context is None:
             context = {}
-        project_obj = self.pool.get('project.project')
+        task_pool = self.pool.get('project.task')
         data_obj = self.pool.get('ir.model.data')
         result = []
         for proj in self.browse(cr, uid, ids, context=context):
@@ -238,7 +248,8 @@ class project(osv.osv):
                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
-            new_id = project_obj.copy(cr, uid, proj.id, default = {
+            context.update({'copy':True})
+            new_id = self.template_copy(cr, uid, proj.id, default = {
                                     'name': proj.name +_(' (copy)'),
                                     'state':'open',
                                     'date_start':new_date_start,
@@ -339,7 +350,7 @@ class task(osv.osv):
 
     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
         return {'value':{'remaining_hours': planned - effective}}
-    
+
     def onchange_project(self, cr, uid, id, project_id):
         if not project_id:
             return {}
@@ -348,7 +359,7 @@ class task(osv.osv):
         if partner_id:
             return {'value':{'partner_id':partner_id.id}}
         return {}
-    
+
     def _default_project(self, cr, uid, context=None):
         if context is None:
             context = {}
@@ -356,6 +367,25 @@ class task(osv.osv):
             return int(context['project_id'])
         return False
 
+    def duplicate_task(self, cr, uid, map_ids, context=None):
+        for new in map_ids.values():
+            task = self.browse(cr, uid, new, context)
+            child_ids = [ ch.id for ch in task.child_ids]
+            if task.child_ids:
+                for child in task.child_ids:
+                    if child.id in map_ids.keys():
+                        child_ids.remove(child.id)
+                        child_ids.append(map_ids[child.id])
+
+            parent_ids = [ ch.id for ch in task.parent_ids]
+            if task.parent_ids:
+                for parent in task.parent_ids:
+                    if parent.id in map_ids.keys():
+                        parent_ids.remove(parent.id)
+                        parent_ids.append(map_ids[parent.id])
+            #FIXME why there is already the copy and the old one
+            self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
+
     def copy_data(self, cr, uid, id, default={}, context=None):
         default = default or {}
         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
@@ -364,7 +394,10 @@ class task(osv.osv):
         default['active'] = True
         default['type_id'] = False
         if not default.get('name', False):
-            default['name'] = self.browse(cr, uid, id, context=context).name
+            default['name'] = self.browse(cr, uid, id, context=context).name or ''
+            if not context.get('copy',False):
+                 new_name = _("%s (copy)")%default.get('name','')
+                 default.update({'name':new_name})
         return super(task, self).copy_data(cr, uid, id, default, context)
 
     def _check_dates(self, cr, uid, ids, context=None):
@@ -393,16 +426,16 @@ class task(osv.osv):
         'active': fields.function(_is_template, method=True, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
         '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')], 'Priority'),
+        'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
         'type_id': fields.many2one('project.task.type', 'Stage'),
         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
                                   help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
                                   \n If the task is over, the states is set to \'Done\'.'),
-        'create_date': fields.datetime('Create Date', readonly=True),
-        'date_start': fields.datetime('Starting Date'),
-        'date_end': fields.datetime('Ending Date'),
-        'date_deadline': fields.date('Deadline'),
+        'create_date': fields.datetime('Create Date', readonly=True,select=True),
+        'date_start': fields.datetime('Starting Date',select=True),
+        'date_end': fields.datetime('Ending Date',select=True),
+        'date_deadline': fields.date('Deadline',select=True),
         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
@@ -452,31 +485,49 @@ class task(osv.osv):
     _order = "sequence,priority, date_start, name, id"
 
     def _check_recursion(self, cr, uid, ids, context=None):
-        obj_task = self.browse(cr, uid, ids[0], context=context)
-        parent_ids = [x.id for x in obj_task.parent_ids]
-        children_ids = [x.id for x in obj_task.child_ids]
+        for id in ids:
+            visited_branch = set()
+            visited_node = set()
+            res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
+            if not res:
+                return False
+
+        return True
 
-        if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
+    def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
+        if id in visited_branch: #Cycle
             return False
 
-        while(ids):
-            cr.execute('SELECT DISTINCT task_id '\
-                       'FROM project_task_parent_rel '\
-                       'WHERE parent_id IN %s', (tuple(ids),))
-            child_ids = map(lambda x: x[0], cr.fetchall())
-            c_ids = child_ids
-            if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
+        if id in visited_node: #Already tested don't work one more time for nothing
+            return True
+
+        visited_branch.add(id)
+        visited_node.add(id)
+
+        #visit child using DFS
+        task = self.browse(cr, uid, id, context=context)
+        for child in task.child_ids:
+            res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
+            if not res:
+                return False
+
+        visited_branch.remove(id)
+        return True
+
+    def _check_dates(self, cr, uid, ids, context=None):
+        if context == None:
+            context = {}
+        obj_task = self.browse(cr, uid, ids[0], context=context)
+        start = obj_task.date_start or False
+        end = obj_task.date_end or False
+        if start and end :
+            if start > end:
                 return False
-            while len(c_ids):
-                s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
-                if (list(set(parent_ids).intersection(set(s_ids)))):
-                    return False
-                c_ids = s_ids
-            ids = child_ids
         return True
 
     _constraints = [
-        (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids'])
+        (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
+        (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
     ]
     #
     # Override view according to the company definition
@@ -540,6 +591,7 @@ class task(osv.osv):
         """
         request = self.pool.get('res.request')
         for task in self.browse(cr, uid, ids, context=context):
+            vals = {}
             project = task.project_id
             if project:
                 # Send request to project manager
@@ -562,7 +614,11 @@ class task(osv.osv):
                             reopen = False
                     if reopen:
                         self.do_reopen(cr, uid, [parent_id.id])
-            self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
+            vals.update({'state': 'done'})
+            vals.update({'remaining_hours': 0.0})
+            if not task.date_end:
+                vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
+            self.write(cr, uid, [task.id],vals)
             message = _("The task '%s' is done") % (task.name,)
             self.log(cr, uid, task.id, message)
         return True