Merge lp:~openerp-dev/openobject-addons/trunk-project-copy-tde into lp:openobject-addons

Proposed by Thibault Delavallée (OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-addons/trunk-project-copy-tde
Merge into: lp:openobject-addons
Diff against target: 356 lines (+122/-118)
4 files modified
portal_project/tests/test_access_rights.py (+2/-2)
project/project.py (+116/-112)
project/tests/common.py (+2/-2)
project/tests/test_project_flow.py (+2/-2)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/trunk-project-copy-tde
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+186364@code.launchpad.net

Description of the change

[IMP] project: better copy/duplication
- when duplicating a classic project: do not duplicate tasks
- when duplicating a tmeplate project: duplicate its tasks and attachments (+ cleaned a bit the
various methods used)
- when creating a template contract: create a template project

To post a comment you must log in.
8908. By Thibault Delavallée (OpenERP)

[FIX] project: added active_test fto find deactivated tasks

8909. By Thibault Delavallée (OpenERP)

[REF] project: base test file is now in common.py

8910. By Turkesh Patel (openERP)

[MRG] merge with lp:openobject-addons

8911. By Turkesh Patel (openERP)

[MRG] merge with lp:openobject-addons

Unmerged revisions

8911. By Turkesh Patel (openERP)

[MRG] merge with lp:openobject-addons

8910. By Turkesh Patel (openERP)

[MRG] merge with lp:openobject-addons

8909. By Thibault Delavallée (OpenERP)

[REF] project: base test file is now in common.py

8908. By Thibault Delavallée (OpenERP)

[FIX] project: added active_test fto find deactivated tasks

8907. By Thibault Delavallée (OpenERP)

[IMP] project: better copy/duplication
- when duplicating a classic project: do not duplicate tasks
- when duplicating a tmeplate project: duplicate its tasks and attachments (+ cleaned a bit the
various methods used)
- when creating a template contract: create a template project

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'portal_project/tests/test_access_rights.py'
2--- portal_project/tests/test_access_rights.py 2013-07-10 13:29:54 +0000
3+++ portal_project/tests/test_access_rights.py 2013-10-07 10:05:06 +0000
4@@ -19,12 +19,12 @@
5 #
6 ##############################################################################
7
8-from openerp.addons.project.tests.test_project_base import TestProjectBase
9+from openerp.addons.project.tests.common import TestProject
10 from openerp.osv.orm import except_orm
11 from openerp.tools import mute_logger
12
13
14-class TestPortalProjectBase(TestProjectBase):
15+class TestPortalProjectBase(TestProject):
16
17 def setUp(self):
18 super(TestPortalProjectBase, self).setUp()
19
20=== modified file 'project/project.py'
21--- project/project.py 2013-09-19 14:23:38 +0000
22+++ project/project.py 2013-10-07 10:05:06 +0000
23@@ -19,7 +19,7 @@
24 #
25 ##############################################################################
26
27-from datetime import datetime, date
28+from datetime import date, datetime
29 from lxml import etree
30 import time
31
32@@ -314,7 +314,7 @@
33 ]
34
35 def set_template(self, cr, uid, ids, context=None):
36- return self.setActive(cr, uid, ids, value=False, context=context)
37+ return self.set_template_state(cr, uid, ids, True, context=context)
38
39 def set_done(self, cr, uid, ids, context=None):
40 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
41@@ -329,99 +329,115 @@
42 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
43
44 def reset_project(self, cr, uid, ids, context=None):
45- return self.setActive(cr, uid, ids, value=True, context=context)
46-
47- def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
48- """ copy and map tasks from old to new project """
49- if context is None:
50- context = {}
51- map_task_id = {}
52- task_obj = self.pool.get('project.task')
53- proj = self.browse(cr, uid, old_project_id, context=context)
54- for task in proj.tasks:
55- map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
56- self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
57- task_obj.duplicate_task(cr, uid, map_task_id, context=context)
58- return True
59-
60- def copy(self, cr, uid, id, default=None, context=None):
61- if context is None:
62- context = {}
63+ return self.set_template_state(cr, uid, ids, False, context=context)
64+
65+ def copy_data(self, cr, uid, id, default=None, context=None):
66 if default is None:
67 default = {}
68+ current_project = self.browse(cr, uid, id, context=context)
69+ default['line_ids'] = []
70+ # update name
71+ if not default.get('name'):
72+ default.update(name=_("%s (copy %s)") % (current_project.name, fields.datetime.now()))
73+ # handle tasks
74+ if current_project.state == 'template':
75+ mapping = {}
76+ for task in current_project.tasks:
77+ mapping[task.id] = self.pool['project.task'].copy(cr, uid, task.id, context=context)
78+ self.pool['project.task'].update_parents_and_childs(cr, uid, mapping.values(), mapping, context=context)
79+ default['tasks'] = [(6, 0, mapping.values())]
80+ else:
81+ default['tasks'] = []
82+ # handle attachments: copy them instead of just keeping the links
83+ if current_project.state == 'template':
84+ attach_obj = self.pool['ir.attachment']
85+ attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
86+ for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
87+ attach_obj.copy_data(cr, uid, attach.id, {
88+ 'name': '%s %s' % (attach.name, fields.datetime.now())
89+ }, context=context)
90+ return super(project, self).copy_data(cr, uid, id, default, context)
91
92+ def copy(self, cr, uid, id, default=None, context=None):
93+ if context is None:
94+ context = {}
95 context['active_test'] = False
96- default['state'] = 'open'
97- default['line_ids'] = []
98- default['tasks'] = []
99- proj = self.browse(cr, uid, id, context=context)
100- if not default.get('name', False):
101- default.update(name=_("%s (copy)") % (proj.name))
102- res = super(project, self).copy(cr, uid, id, default, context)
103- self.map_tasks(cr, uid, id, res, context=context)
104- return res
105+ return super(project, self).copy(cr, uid, id, default, context)
106
107 def duplicate_template(self, cr, uid, ids, context=None):
108+ return self.create_from_template(cr, uid, ids, context=context)
109+
110+ def create_from_template(self, cr, uid, ids, context=None):
111 if context is None:
112 context = {}
113+ context['copy'] = True # compatibility with some old ugly code in account
114 data_obj = self.pool.get('ir.model.data')
115- result = []
116- for proj in self.browse(cr, uid, ids, context=context):
117- parent_id = context.get('parent_id', False)
118- context.update({'analytic_project_copy': True})
119- new_date_start = time.strftime('%Y-%m-%d')
120- new_date_end = False
121- if proj.date_start and proj.date:
122- start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
123- end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
124- new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
125- context.update({'copy':True})
126- new_id = self.copy(cr, uid, proj.id, default = {
127- 'name':_("%s (copy)") % (proj.name),
128- 'state':'open',
129- 'date_start':new_date_start,
130- 'date':new_date_end,
131- 'parent_id':parent_id}, context=context)
132- result.append(new_id)
133-
134- child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
135- parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
136+
137+ new_projects = []
138+ for template in self.browse(cr, uid, ids, context=context):
139+ date_start = date.today()
140+ date_end = False
141+ if template.date and template.date_start:
142+ date_end = date_start + (datetime.strptime(template.date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date() - datetime.strptime(template.date_start, tools.DEFAULT_SERVER_DATETIME_FORMAT).date())
143+ project_id = self.copy(cr, uid, template.id, default={
144+ 'state': 'open',
145+ 'date_start': date_start,
146+ 'date': date_end,
147+ 'parent_id': context.get('parent_id', False),
148+ }, context=context)
149+ new_projects.append(project_id)
150+
151+ child_ids = self.search(cr, uid, [('parent_id', '=', template.analytic_account_id.id)], context=context)
152 if child_ids:
153- self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
154+ project_account_id = self.browse(cr, uid, project_id, ['analytic_account_id'], context=context).analytic_account_id.id
155+ self.duplicate_template(cr, uid, child_ids, context={'parent_id': project_account_id})
156
157- if result and len(result):
158- res_id = result[0]
159- form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
160- form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
161- tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
162- tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
163- search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
164- search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
165+ if new_projects:
166+ try:
167+ form_view_id = data_obj.get_object_reference(cr, uid, 'project', 'edit_project')[1]
168+ except ValueError:
169+ form_view_id = False
170+ try:
171+ tree_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project')[1]
172+ except ValueError:
173+ tree_view_id = False
174+ try:
175+ search_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project_project_filter')[1]
176+ except ValueError:
177+ search_view_id = False
178 return {
179 'name': _('Projects'),
180 'view_type': 'form',
181 'view_mode': 'form,tree',
182 'res_model': 'project.project',
183 'view_id': False,
184- 'res_id': res_id,
185- 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
186+ 'res_id': new_projects[0],
187+ 'views': [(form_view_id, 'form'), (tree_view_id, 'tree')],
188 'type': 'ir.actions.act_window',
189- 'search_view_id': search_view['res_id'],
190+ 'search_view_id': search_view_id,
191 'nodestroy': True
192 }
193+ return False
194
195 # set active value for a project, its sub projects and its tasks
196 def setActive(self, cr, uid, ids, value=True, context=None):
197- task_obj = self.pool.get('project.task')
198- for proj in self.browse(cr, uid, ids, context=None):
199- self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
200- cr.execute('select id from project_task where project_id=%s', (proj.id,))
201- tasks_id = [x[0] for x in cr.fetchall()]
202- if tasks_id:
203- task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
204- child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
205- if child_ids:
206- self.setActive(cr, uid, child_ids, value, context=None)
207+ return self.set_template_state(cr, uid, ids, not value, context=context)
208+
209+ def set_template_state(self, cr, uid, ids, template_state=False, context=None):
210+ """ (Re)set the project as template.
211+ :param boolean template_state: True => 'template', False => 'open'
212+ """
213+ if context is None:
214+ context = {}
215+ active_ctx = dict(context, {'active_test': False})
216+ state = 'open' if template_state else 'template'
217+ self.write(cr, uid, ids, {'state': state}, context=context)
218+ task_ids = self.pool['project.task'].search(cr, uid, [('project_id', 'in', ids)], context=active_ctx) # search instead of browse because of active_flag - active_test context key seems buggy with cache
219+ self.pool['project.task'].write(cr, uid, task_ids, {'active': state == 'open'}, context=context)
220+ parent_ids = [project.analytic_account_id.id for project in self.browse(cr, uid, ids, context=context)]
221+ child_project_ids = self.search(cr, uid, [('parent_id', 'in', parent_ids)], context=context)
222+ if child_project_ids:
223+ return self.set_template_state(cr, uid, child_project_ids, state, context=context)
224 return True
225
226 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
227@@ -684,49 +700,36 @@
228 vals['date_start'] = fields.datetime.now()
229 return {'value': vals}
230
231- def duplicate_task(self, cr, uid, map_ids, context=None):
232- for new in map_ids.values():
233- task = self.browse(cr, uid, new, context)
234- child_ids = [ ch.id for ch in task.child_ids]
235- if task.child_ids:
236- for child in task.child_ids:
237- if child.id in map_ids.keys():
238- child_ids.remove(child.id)
239- child_ids.append(map_ids[child.id])
240-
241- parent_ids = [ ch.id for ch in task.parent_ids]
242- if task.parent_ids:
243- for parent in task.parent_ids:
244- if parent.id in map_ids.keys():
245- parent_ids.remove(parent.id)
246- parent_ids.append(map_ids[parent.id])
247- #FIXME why there is already the copy and the old one
248- self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
249+ def update_parents_and_childs(self, cr, uid, ids, mapping, context=None):
250+ for task in self.browse(cr, uid, ids, context=context):
251+ new_parent_ids = [mapping[parent.id] for parent in task.parent_ids if parent.id in mapping]
252+ new_parent_ids += [parent.id for parent in task.parent_ids if parent.id not in mapping]
253+ new_child_ids = [mapping[child.id] for child in task.child_ids if child.id in mapping]
254+ new_child_ids += [child.id for child in task.child_ids if child.id not in mapping]
255+ self.write(cr, uid, [task.id], {'parent_ids': [(6, 0, new_parent_ids)], 'child_ids': [(6, 0, new_child_ids)]}, context=context)
256+ return True
257
258 def copy_data(self, cr, uid, id, default=None, context=None):
259 if default is None:
260 default = {}
261- default = default or {}
262- default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
263- if not default.get('remaining_hours', False):
264- default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
265- default['active'] = True
266- if not default.get('name', False):
267- default['name'] = self.browse(cr, uid, id, context=context).name or ''
268- if not context.get('copy',False):
269- new_name = _("%s (copy)") % (default.get('name', ''))
270- default.update({'name':new_name})
271+ current_task = self.browse(cr, uid, id, context=context)
272+ if not 'stage' in default:
273+ default['stage_id'] = self._get_default_stage_id(cr, uid, context=context)
274+ default.update({
275+ 'active': True,
276+ 'work_ids': [],
277+ 'date_start': False,
278+ 'date_end': False,
279+ 'date_deadline': False,
280+ })
281+ if 'remaining_hours' not in default:
282+ default['remaining_hours'] = float(current_task.planned_hours)
283+ if not default.get('name'):
284+ new_name = current_task.name
285+ if not context.get('copy'):
286+ new_name = _("%s (copy)") % new_name
287+ default['name'] = new_name
288 return super(task, self).copy_data(cr, uid, id, default, context)
289-
290- def copy(self, cr, uid, id, default=None, context=None):
291- if context is None:
292- context = {}
293- if default is None:
294- default = {}
295- stage = self._get_default_stage_id(cr, uid, context=context)
296- if stage:
297- default['stage_id'] = stage
298- return super(task, self).copy(cr, uid, id, default, context)
299
300 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
301 res = {}
302@@ -1222,12 +1225,13 @@
303 This function is called at the time of analytic account creation and is used to create a project automatically linked to it if the conditions are meet.
304 '''
305 project_pool = self.pool.get('project.project')
306- project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])
307+ project_id = project_pool.search(cr, uid, [('analytic_account_id', '=', analytic_account_id)])
308 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
309 project_values = {
310 'name': vals.get('name'),
311 'analytic_account_id': analytic_account_id,
312- 'type': vals.get('type','contract'),
313+ 'type': vals.get('type', 'contract'),
314+ 'state': 'template' if vals.get('type') == 'template' else 'open',
315 }
316 return project_pool.create(cr, uid, project_values, context=context)
317 return False
318
319=== renamed file 'project/tests/test_project_base.py' => 'project/tests/common.py'
320--- project/tests/test_project_base.py 2013-08-27 13:30:58 +0000
321+++ project/tests/common.py 2013-10-07 10:05:06 +0000
322@@ -22,10 +22,10 @@
323 from openerp.addons.mail.tests.common import TestMail
324
325
326-class TestProjectBase(TestMail):
327+class TestProject(TestMail):
328
329 def setUp(self):
330- super(TestProjectBase, self).setUp()
331+ super(TestProject, self).setUp()
332 cr, uid = self.cr, self.uid
333
334 # Usefull models
335
336=== modified file 'project/tests/test_project_flow.py'
337--- project/tests/test_project_flow.py 2013-07-10 13:29:54 +0000
338+++ project/tests/test_project_flow.py 2013-10-07 10:05:06 +0000
339@@ -19,7 +19,7 @@
340 #
341 ##############################################################################
342
343-from openerp.addons.project.tests.test_project_base import TestProjectBase
344+from openerp.addons.project.tests.common import TestProject
345 from openerp.osv.orm import except_orm
346 from openerp.tools import mute_logger
347
348@@ -49,7 +49,7 @@
349 Integrator at Agrolait"""
350
351
352-class TestProjectFlow(TestProjectBase):
353+class TestProjectFlow(TestProject):
354
355 @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
356 def test_00_project_process(self):

Subscribers

People subscribed via source and target branches

to all changes: