← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~camptocamp/ocb-addons/7.0-bugfix-1189480-with-perf-mdh into lp:ocb-addons

 

Matthieu Dietrich @ camptocamp has proposed merging lp:~camptocamp/ocb-addons/7.0-bugfix-1189480-with-perf-mdh into lp:ocb-addons.

Requested reviews:
  Nicolas Bessi - Camptocamp (nbessi-c2c)
  Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c): code review, no test
Related bugs:
  Bug #1189480 in OpenERP Community Backports (Addons): "planned_hours, total_hours, effective_hours are wrongly calculated in parent project"
  https://bugs.launchpad.net/ocb-addons/+bug/1189480

For more details, see:
https://code.launchpad.net/~camptocamp/ocb-addons/7.0-bugfix-1189480-with-perf-mdh/+merge/197325

The quick fix proposed in lp:~camptocamp/openobject-addons/7.0-fixes-bug-1189480 works, but the multiple read() severely impact performance. This solution uses a WITH RECURSIVE SQL statement in order to 1) avoid any extraneous read() 2) get all the information in one request.

I also added a YAML test to test both this new method and a hierarchical computation, since all YAML tests in addons are for a single level of projects.
-- 
https://code.launchpad.net/~camptocamp/ocb-addons/7.0-bugfix-1189480-with-perf-mdh/+merge/197325
Your team OpenERP Community Backports Team is subscribed to branch lp:ocb-addons.
=== modified file 'project/__openerp__.py'
--- project/__openerp__.py	2012-10-23 16:05:04 +0000
+++ project/__openerp__.py	2013-12-02 08:29:42 +0000
@@ -82,6 +82,7 @@
         'test/project_demo.yml',
         'test/project_process.yml',
         'test/task_process.yml',
+        'test/hours_process.yml',
     ],
     'installable': True,
     'auto_install': False,

=== modified file 'project/project.py'
--- project/project.py	2013-11-22 16:55:04 +0000
+++ project/project.py	2013-12-02 08:29:42 +0000
@@ -135,6 +135,7 @@
             res.update(ids)
         return list(res)
 
+    # Deprecated; the _progress_rate method does not use this anymore
     def _get_project_and_children(self, cr, uid, ids, context=None):
         """ retrieve all children projects of project ids;
             return a dictionary mapping each project to its parent project (or None)
@@ -154,28 +155,75 @@
         return res
 
     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
-        child_parent = self._get_project_and_children(cr, uid, ids, context)
         # compute planned_hours, total_hours, effective_hours specific to each project
+        # How this works: the WITH RECURSIVE statement will create an union line
+        # for each parent project with the hours of each child project; the final
+        # SUM(...) ensures we get a total of hours by project.
         cr.execute("""
-            SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
-                COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
-            FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
-            GROUP BY project_id
-            """, (tuple(child_parent.keys()),))
+        WITH RECURSIVE recur_table(project_id,
+                                   parent_id,
+                                   planned_hours,
+                                   total_hours,
+                                   effective_hours) AS (
+            SELECT project.id,
+                   parent.id,
+                   COALESCE(task.planned, 0.0),
+                   COALESCE(task.total, 0.0),
+                   COALESCE(task.effective, 0.0)
+            FROM project_project project
+                 LEFT JOIN account_analytic_account account
+                      ON project.analytic_account_id = account.id
+                 LEFT JOIN project_project parent
+                      ON parent.analytic_account_id = account.parent_id
+                 LEFT JOIN (SELECT project_id,
+                                   SUM(planned_hours) as planned,
+                                   SUM(total_hours) as total,
+                                   SUM(effective_hours) as effective
+                            FROM project_task
+                            WHERE state <> 'cancelled'
+                            GROUP BY project_id) AS task
+                      ON project.id = task.project_id
+        UNION ALL
+            SELECT project.id,
+                   parent.id,
+                   recur_table.planned_hours,
+                   recur_table.total_hours,
+                   recur_table.effective_hours
+            FROM project_project project
+                 LEFT JOIN account_analytic_account account
+                      ON project.analytic_account_id = account.id
+                 LEFT JOIN project_project parent
+                      ON parent.analytic_account_id = account.parent_id
+                 LEFT JOIN (SELECT project_id,
+                                   SUM(planned_hours) as planned,
+                                   SUM(total_hours) as total,
+                                   SUM(effective_hours) as effective
+                            FROM project_task
+                            WHERE state <> 'cancelled'
+                            GROUP BY project_id) AS task
+                      ON project.id = task.project_id
+                 JOIN recur_table ON project.id = recur_table.parent_id
+        )
+        SELECT project_id,
+               SUM(planned_hours),
+               SUM(total_hours),
+               SUM(effective_hours)
+        FROM recur_table
+        WHERE project_id IN %s
+        GROUP BY project_id
+            """, (tuple(ids),))
         # aggregate results into res
-        res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
-        for id, planned, total, effective in cr.fetchall():
-            # add the values specific to id to all parent projects of id in the result
-            while id:
-                if id in ids:
-                    res[id]['planned_hours'] += planned
-                    res[id]['total_hours'] += total
-                    res[id]['effective_hours'] += effective
-                id = child_parent[id]
+        res = dict([(result_line[0], {'planned_hours': result_line[1],
+                                      'total_hours': result_line[2],
+                                      'effective_hours': result_line[3]})
+                    for result_line in cr.fetchall()])
         # compute progress rates
         for id in ids:
             if res[id]['total_hours']:
-                res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
+                res[id]['progress_rate'] = round(100.0 *
+                                                 res[id]['effective_hours'] /
+                                                 res[id]['total_hours'],
+                                                 2)
             else:
                 res[id]['progress_rate'] = 0.0
         return res
@@ -192,7 +240,7 @@
         res =  super(project, self).unlink(cr, uid, ids, context=context)
         mail_alias.unlink(cr, uid, alias_ids, context=context)
         return res
-    
+
     def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
         res = {}
         attachment = self.pool.get('ir.attachment')
@@ -203,7 +251,7 @@
             task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True)
             res[id] = (project_attachments or 0) + (task_attachments or 0)
         return res
-        
+
     def _task_count(self, cr, uid, ids, field_name, arg, context=None):
         if context is None:
             context = {}
@@ -228,7 +276,7 @@
     def attachment_tree_view(self, cr, uid, ids, context):
         task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
         domain = [
-             '|', 
+             '|',
              '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
              '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)
 		]
@@ -736,7 +784,7 @@
                 new_name = _("%s (copy)") % (default.get('name', ''))
                 default.update({'name':new_name})
         return super(task, self).copy_data(cr, uid, id, default, context)
-    
+
     def copy(self, cr, uid, id, default=None, context=None):
         if context is None:
             context = {}

=== added file 'project/test/hours_process.yml'
--- project/test/hours_process.yml	1970-01-01 00:00:00 +0000
+++ project/test/hours_process.yml	2013-12-02 08:29:42 +0000
@@ -0,0 +1,174 @@
+-
+  First, create the analytic accounts
+-
+  !record {model: account.analytic.account, id: parent_project_account}:
+    name: Parent Account
+    code: PAR
+    type: view
+-
+  !record {model: account.analytic.account, id: child1_project_account}:
+    name: Child Account 1
+    code: C1
+    type: view
+-
+  I create a main project
+-
+  !record {model: project.project, id: project_project_parent}:
+    company_id: base.main_company
+    name: Parent Project
+    user_id: base.user_demo
+    parent_id: all_projects_account
+    analytic_account_id: parent_project_account
+    alias_model: project.task
+-
+  A first child project
+-
+  !record {model: project.project, id: project_project_child_1}:
+    company_id: base.main_company
+    name: Child Project 1
+    user_id: base.user_demo
+    parent_id: parent_project_account
+    analytic_account_id: child1_project_account
+    alias_model: project.task
+-
+  A second child project
+-
+  !record {model: project.project, id: project_project_child_2}:
+    company_id: base.main_company
+    name: Child Project 2
+    user_id: base.user_demo
+    parent_id: parent_project_account
+    alias_model: project.task
+-
+  A child of the first child project
+-
+  !record {model: project.project, id: project_project_child_1_1}:
+    company_id: base.main_company
+    name: Child Project 1 1
+    user_id: base.user_demo
+    parent_id: child1_project_account
+    alias_model: project.task
+-
+  A task for the main project, with 20 hours planned and 20 hours remaining
+-
+  !record {model: project.task, id: project_task_parent_project}:
+    date_start: !eval time.strftime('%Y-05-%d %H:%M:%S')
+    name: Task parent project
+    planned_hours: 20.0
+    remaining_hours: 20.0
+    project_id: project_project_parent
+    user_id: base.user_demo
+-
+  I test the hours
+-
+  !python {model: project.project}: |
+    project = self.browse(cr, uid, ref("project_project_parent"))
+    assert abs(project.planned_hours - 20.0) < 10**-4, "Planned hours are not correct! 20.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 20.0) < 10**-4, "Total hours are not correct! 20.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+-
+  A task for the first child project, with 30 hours planned and 30 hours remaining
+-
+  !record {model: project.task, id: project_task_child_project_1}:
+    date_start: !eval time.strftime('%Y-05-%d %H:%M:%S')
+    name: Task child project 1
+    planned_hours: 30.0
+    remaining_hours: 30.0
+    project_id: project_project_child_1
+    user_id: base.user_demo
+-
+  I test the hours
+-
+  !python {model: project.project}: |
+    project = self.browse(cr, uid, ref("project_project_parent"))
+    assert abs(project.planned_hours - 50.0) < 10**-4, "Planned hours are not correct! 50.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 50.0) < 10**-4, "Total hours are not correct! 50.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+    project = self.browse(cr, uid, ref("project_project_child_1"))
+    assert abs(project.planned_hours - 30.0) < 10**-4, "Planned hours are not correct! 30.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 30.0) < 10**-4, "Total hours are not correct! 30.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+-
+  A task for the second child project, with 40 hours planned and 10 hours remaining
+-
+  !record {model: project.task, id: project_task_child_project_2}:
+    date_start: !eval time.strftime('%Y-05-%d %H:%M:%S')
+    name: Task child project 2
+    planned_hours: 40.0
+    remaining_hours: 10.0
+    project_id: project_project_child_2
+    user_id: base.user_demo
+-
+  I test the hours
+-
+  !python {model: project.project}: |
+    project = self.browse(cr, uid, ref("project_project_parent"))
+    assert abs(project.planned_hours - 90.0) < 10**-4, "Planned hours are not correct! 90.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 60.0) < 10**-4, "Total hours are not correct! 60.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+    project = self.browse(cr, uid, ref("project_project_child_2"))
+    assert abs(project.planned_hours - 40.0) < 10**-4, "Planned hours are not correct! 40.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 10.0) < 10**-4, "Total hours are not correct! 10.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rat
+-
+  A task for the child child project, with 50 hours planned and 50 hours remaining
+-
+  !record {model: project.task, id: project_task_child_project_1_1}:
+    date_start: !eval time.strftime('%Y-05-%d %H:%M:%S')
+    name: Task child project 1 1
+    planned_hours: 50.0
+    remaining_hours: 50.0
+    project_id: project_project_child_1_1
+    user_id: base.user_demo
+-
+  I test the hours
+-
+  !python {model: project.project}: |
+    project = self.browse(cr, uid, ref("project_project_parent"))
+    assert abs(project.planned_hours - 140.0) < 10**-4, "Planned hours are not correct! 140.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 110.0) < 10**-4, "Total hours are not correct! 110.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+    project = self.browse(cr, uid, ref("project_project_child_1"))
+    assert abs(project.planned_hours - 80.0) < 10**-4, "Planned hours are not correct! 80.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 80.0) < 10**-4, "Total hours are not correct! 80.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+    project = self.browse(cr, uid, ref("project_project_child_1_1"))
+    assert abs(project.planned_hours - 50.0) < 10**-4, "Planned hours are not correct! 50.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 50.0) < 10**-4, "Total hours are not correct! 50.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 0.0) < 10**-4, "Effective hours are not correct! 0.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 0.0) < 10**-4, "Progress rate is not correct! 0.0 != %s" % project.progress_rate
+-
+  I create a task work on child 1 task
+-
+  !record {model: project.task.work, id: project_child_1_work}:
+    name: test work
+    hours: 10.0
+    task_id: project_task_child_project_1
+    user_id: base.user_demo
+-
+  and refresh the task to account for it
+-
+  !record {model: project.task, id: project_task_child_project_1}:
+    planned_hours: 20.0
+-
+  I test the hours
+-
+  !python {model: project.project}: |
+    project = self.browse(cr, uid, ref("project_project_parent"))
+    assert abs(project.planned_hours - 130.0) < 10**-4, "Planned hours are not correct! 130.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 110.0) < 10**-4, "Total hours are not correct! 110.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 10.0) < 10**-4, "Effective hours are not correct! 10.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 9.09) < 10**-4, "Progress rate is not correct! 9.09 != %s" % project.progress_rate
+    project = self.browse(cr, uid, ref("project_project_child_1"))
+    assert abs(project.planned_hours - 70.0) < 10**-4, "Planned hours are not correct! 70.0 != %s" % project.planned_hours
+    assert abs(project.total_hours - 80.0) < 10**-4, "Total hours are not correct! 80.0 != %s" % project.total_hours
+    assert abs(project.effective_hours - 10.0) < 10**-4, "Effective hours are not correct! 10.0 != %s" % project.effective_hours
+    assert abs(project.progress_rate - 12.5) < 10**-4, "Progress rate is not correct! 12.5 != %s" % project.progress_rate
+


Follow ups