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

Proposed by Sunil Sharma(OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-addons/trunk-project-copy-tde-imp-tpa-imp-ssh
Merge into: lp:openobject-addons
Diff against target: 779 lines (+377/-128)
11 files modified
portal_project/tests/test_access_rights.py (+2/-2)
project/project.py (+164/-116)
project/project_view.xml (+8/-2)
project/tests/__init__.py (+2/-0)
project/tests/common.py (+2/-2)
project/tests/test_contract_task_copy.py (+59/-0)
project/tests/test_project_flow.py (+2/-2)
project_issue/project_issue.py (+43/-3)
project_issue/project_issue_view.xml (+1/-1)
project_issue/tests/__init__.py (+28/-0)
project_issue/tests/test_contract_issue_copy.py (+66/-0)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/trunk-project-copy-tde-imp-tpa-imp-ssh
Reviewer Review Type Date Requested Status
Thibault Delavallée (OpenERP) Pending
Review via email: mp+205931@code.launchpad.net

Description of the change

Hello,

  [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

  [ADD] project_long_term: added code to copy phase
  [ADD] added pyhon test cases for copy of contract templates task, phase, issues

Thanks
Sunil Sharma (SSH)

To post a comment you must log in.
8922. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8923. By Sunil Sharma(OpenERP)

[imp]:improve import file name

8924. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8925. By Sunil Sharma(OpenERP)

[imp]:improve import file name for project_long_term

8926. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8927. By Sunil Sharma(OpenERP)

[imp] improve copy_data method because test case warning generating

8928. By Sunil Sharma(OpenERP)

[imp]:improve issue view

8929. By Sunil Sharma(OpenERP)

[mrg]:lp:openobject-addons

8930. By Sunil Sharma(OpenERP)

[mrg]:lp:openobject-addons

8931. By Sunil Sharma(OpenERP)

[MRG]:openobject-addons

8932. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8933. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8934. By Sunil Sharma(OpenERP)

[REM]:remove test case for project long term

8935. By Sunil Sharma(OpenERP)

[REM]:remove file

8936. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8937. By Sunil Sharma(OpenERP)

[imp]:improve project as template then duplicate project state set as template

8938. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8939. By Sunil Sharma(OpenERP)

[imp]:improve code for project set as template then duplicate project set as template

8940. By Sunil Sharma(OpenERP)

[imp]:improve test cases for project

8941. By Sunil Sharma(OpenERP)

[rem]:remove change project set as template then copy set as template

8942. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8943. By Sunil Sharma(OpenERP)

[mrg]:lp:openobject-addons

8944. By Sunil Sharma(OpenERP)

[imp]:remove unused comments

Unmerged revisions

8944. By Sunil Sharma(OpenERP)

[imp]:remove unused comments

8943. By Sunil Sharma(OpenERP)

[mrg]:lp:openobject-addons

8942. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8941. By Sunil Sharma(OpenERP)

[rem]:remove change project set as template then copy set as template

8940. By Sunil Sharma(OpenERP)

[imp]:improve test cases for project

8939. By Sunil Sharma(OpenERP)

[imp]:improve code for project set as template then duplicate project set as template

8938. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8937. By Sunil Sharma(OpenERP)

[imp]:improve project as template then duplicate project state set as template

8936. By Sunil Sharma(OpenERP)

[MRG]:lp:openobject-addons

8935. By Sunil Sharma(OpenERP)

[REM]:remove file

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 2014-01-16 10:39:10 +0000
3+++ portal_project/tests/test_access_rights.py 2014-05-15 13:02:45 +0000
4@@ -19,13 +19,13 @@
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.exceptions import AccessError
11 from openerp.osv.orm import except_orm
12 from openerp.tools import mute_logger
13
14
15-class TestPortalProjectBase(TestProjectBase):
16+class TestPortalProjectBase(TestProject):
17
18 def setUp(self):
19 super(TestPortalProjectBase, self).setUp()
20
21=== modified file 'project/project.py'
22--- project/project.py 2014-05-13 11:18:37 +0000
23+++ project/project.py 2014-05-15 13:02:45 +0000
24@@ -19,7 +19,7 @@
25 #
26 ##############################################################################
27
28-from datetime import datetime, date
29+from datetime import date, datetime
30 from lxml import etree
31 import time
32
33@@ -180,6 +180,7 @@
34 res = {}
35 attachment = self.pool.get('ir.attachment')
36 task = self.pool.get('project.task')
37+ context['active_test'] = False
38 for id in ids:
39 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)
40 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)
41@@ -187,9 +188,16 @@
42 res[id] = (project_attachments or 0) + (task_attachments or 0)
43 return res
44 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
45- res={}
46- for tasks in self.browse(cr, uid, ids, context):
47- res[tasks.id] = len(tasks.task_ids)
48+ """ :deprecated: this method will be removed with OpenERP v8. Use tasks
49+ fields instead. """
50+ if context is None:
51+ context = {}
52+ res = dict.fromkeys(ids, 0)
53+ ctx = context.copy()
54+ ctx['active_test'] = False
55+ task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=ctx)
56+ for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
57+ res[task.project_id.id] += 1
58 return res
59 def _get_alias_models(self, cr, uid, context=None):
60 """ Overriden in project_issue to offer more options """
61@@ -202,7 +210,10 @@
62 ('followers', 'Private project: followers Only')]
63
64 def attachment_tree_view(self, cr, uid, ids, context):
65- task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
66+ if context is None:
67+ context = {}
68+ context['active_test'] = False
69+ task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=context)
70 domain = [
71 '|',
72 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
73@@ -217,7 +228,7 @@
74 'view_mode': 'kanban,tree,form',
75 'view_type': 'form',
76 'limit': 80,
77- 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)
78+ 'context': "{'default_res_model': '%s','default_res_id': %d, 'active_test': False}" % (self._name, res_id)
79 }
80
81 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
82@@ -256,9 +267,8 @@
83 }),
84 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
85 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
86- 'task_count': fields.function(_task_count, type='integer', string="Tasks",),
87- 'task_ids': fields.one2many('project.task', 'project_id',
88- domain=[('stage_id.fold', '=', False)]),
89+ 'task_count': fields.function(_task_count, type='integer', string="Open Tasks",
90+ deprecated="This field will be removed in OpenERP v8. Use tasks one2many field instead."),
91 'color': fields.integer('Color Index'),
92 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
93 help="Internal email associated with this project. Incoming emails are automatically synchronized"
94@@ -310,7 +320,7 @@
95 ]
96
97 def set_template(self, cr, uid, ids, context=None):
98- return self.setActive(cr, uid, ids, value=False, context=context)
99+ return self.set_template_state(cr, uid, ids, True, context=context)
100
101 def set_done(self, cr, uid, ids, context=None):
102 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
103@@ -325,104 +335,117 @@
104 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
105
106 def reset_project(self, cr, uid, ids, context=None):
107- return self.setActive(cr, uid, ids, value=True, context=context)
108-
109- def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
110- """ copy and map tasks from old to new project """
111- if context is None:
112- context = {}
113- map_task_id = {}
114- task_obj = self.pool.get('project.task')
115- proj = self.browse(cr, uid, old_project_id, context=context)
116- for task in proj.tasks:
117- map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
118- self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
119- task_obj.duplicate_task(cr, uid, map_task_id, context=context)
120- return True
121-
122- def copy(self, cr, uid, id, default=None, context=None):
123- if context is None:
124- context = {}
125+ return self.set_template_state(cr, uid, ids, False, context=context)
126+
127+ def copy_data(self, cr, uid, id, default=None, context=None):
128 if default is None:
129 default = {}
130-
131- context['active_test'] = False
132- default['state'] = 'open'
133+ current_project = self.browse(cr, uid, id, context=context)
134 default['line_ids'] = []
135- default['tasks'] = []
136-
137- # Don't prepare (expensive) data to copy children (analytic accounts),
138- # they are discarded in analytic.copy(), and handled in duplicate_template()
139- default['child_ids'] = []
140-
141- proj = self.browse(cr, uid, id, context=context)
142- if not default.get('name', False):
143- default.update(name=_("%s (copy)") % (proj.name))
144+ # update name
145+ if not default.get('name'):
146+ default.update(name=_("%s (copy %s)") % (current_project.name, fields.datetime.now()))
147+
148+ # handle tasks
149+ if current_project.state == 'template':
150+ default['tasks'] = self.pool['project.task'].duplicate_task(cr, uid, current_project.tasks, context=context)
151+ else:
152+ default['tasks'] = []
153+ return super(project, self).copy_data(cr, uid, id, default, context)
154+
155+ def copy(self, cr, uid, id, default=None, context=None):
156+ if context is None:
157+ context = {}
158+ context['active_test'] = False
159 res = super(project, self).copy(cr, uid, id, default, context)
160- self.map_tasks(cr, uid, id, res, context=context)
161+ current_project = self.browse(cr, uid, id, context=context)
162+ # handle attachments: copy them instead of just keeping the links
163+ if current_project.state == 'template':
164+ attach_obj = self.pool['ir.attachment']
165+ attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
166+ for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
167+ attach_obj.copy(cr, uid, attach.id, {
168+ 'name': '%s %s' % (attach.name, fields.datetime.now()),
169+ 'res_id': res,
170+ 'res_model': 'project.project',
171+ 'res_name': default['name']
172+ }, context=context)
173 return res
174
175 def duplicate_template(self, cr, uid, ids, context=None):
176+ return self.create_from_template(cr, uid, ids, context=context)
177+
178+ def create_from_template(self, cr, uid, ids, context=None):
179 if context is None:
180 context = {}
181+ context['copy'] = True # compatibility with some old ugly code in account
182 data_obj = self.pool.get('ir.model.data')
183- result = []
184- for proj in self.browse(cr, uid, ids, context=context):
185- parent_id = context.get('parent_id', False)
186- context.update({'analytic_project_copy': True})
187- new_date_start = time.strftime('%Y-%m-%d')
188- new_date_end = False
189- if proj.date_start and proj.date:
190- start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
191- end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
192- new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
193- context.update({'copy':True})
194- new_id = self.copy(cr, uid, proj.id, default = {
195- 'name':_("%s (copy)") % (proj.name),
196- 'state':'open',
197- 'date_start':new_date_start,
198- 'date':new_date_end,
199- 'parent_id':parent_id}, context=context)
200- result.append(new_id)
201-
202- child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
203- parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
204+
205+ new_projects = []
206+ for template in self.browse(cr, uid, ids, context=context):
207+ date_start = date.today()
208+ date_end = False
209+ if template.date and template.date_start:
210+ 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())
211+ project_id = self.copy(cr, uid, template.id, default={
212+ 'state': 'open',
213+ 'date_start': date_start,
214+ 'date': date_end,
215+ 'parent_id': context.get('parent_id', False),
216+ }, context=context)
217+ new_projects.append(project_id)
218+
219+ child_ids = self.search(cr, uid, [('parent_id', '=', template.analytic_account_id.id)], context=context)
220 if child_ids:
221- self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
222+ project_account_id = self.browse(cr, uid, project_id, ['analytic_account_id'], context=context).analytic_account_id.id
223+ self.duplicate_template(cr, uid, child_ids, context={'parent_id': project_account_id})
224
225- if result and len(result):
226- res_id = result[0]
227- form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
228- form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
229- tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
230- tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
231- search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
232- search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
233+ if new_projects:
234+ try:
235+ form_view_id = data_obj.get_object_reference(cr, uid, 'project', 'edit_project')[1]
236+ except ValueError:
237+ form_view_id = False
238+ try:
239+ tree_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project')[1]
240+ except ValueError:
241+ tree_view_id = False
242+ try:
243+ search_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project_project_filter')[1]
244+ except ValueError:
245+ search_view_id = False
246 return {
247 'name': _('Projects'),
248 'view_type': 'form',
249 'view_mode': 'form,tree',
250 'res_model': 'project.project',
251 'view_id': False,
252- 'res_id': res_id,
253- 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
254+ 'res_id': new_projects[0],
255+ 'views': [(form_view_id, 'form'), (tree_view_id, 'tree')],
256 'type': 'ir.actions.act_window',
257- 'search_view_id': search_view['res_id'],
258+ 'search_view_id': search_view_id,
259 'nodestroy': True
260 }
261+ return False
262
263 # set active value for a project, its sub projects and its tasks
264 def setActive(self, cr, uid, ids, value=True, context=None):
265- task_obj = self.pool.get('project.task')
266- for proj in self.browse(cr, uid, ids, context=None):
267- self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
268- cr.execute('select id from project_task where project_id=%s', (proj.id,))
269- tasks_id = [x[0] for x in cr.fetchall()]
270- if tasks_id:
271- task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
272- child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
273- if child_ids:
274- self.setActive(cr, uid, child_ids, value, context=None)
275+ return self.set_template_state(cr, uid, ids, not value, context=context)
276+
277+ def set_template_state(self, cr, uid, ids, template_state=False, context=None):
278+ """ (Re)set the project as template.
279+ :param boolean template_state: True => 'template', False => 'open'
280+ """
281+ if context is None:
282+ context = {}
283+ active_ctx = dict({'active_test': False})
284+ state = 'template' if template_state else 'open'
285+ self.write(cr, uid, ids, {'state': state}, context=context)
286+ 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
287+ self.pool['project.task'].write(cr, uid, task_ids, {'active': state == 'open'}, context=context)
288+ parent_ids = [project.analytic_account_id.id for project in self.browse(cr, uid, ids, context=context)]
289+ child_project_ids = self.search(cr, uid, [('parent_id', 'in', parent_ids)], context=context)
290+ if child_project_ids:
291+ return self.set_template_state(cr, uid, child_project_ids, state, context=context)
292 return True
293
294 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
295@@ -688,39 +711,58 @@
296 return {'value': vals}
297
298 def duplicate_task(self, cr, uid, map_ids, context=None):
299- mapper = lambda t: map_ids.get(t.id, t.id)
300- for task in self.browse(cr, uid, map_ids.values(), context):
301- new_child_ids = set(map(mapper, task.child_ids))
302- new_parent_ids = set(map(mapper, task.parent_ids))
303- if new_child_ids or new_parent_ids:
304- task.write({'parent_ids': [(6,0,list(new_parent_ids))],
305- 'child_ids': [(6,0,list(new_child_ids))]})
306+ mapping = {}
307+ task_obj = self.pool['project.task']
308+ for task in map_ids:
309+ mapping[task.id] = task_obj.copy(cr, uid, task.id, context=context)
310+ task_obj.update_parents_and_childs(cr, uid, mapping.values(), mapping, context=context)
311+ return [(6, 0, mapping.values())]
312+
313+ def update_parents_and_childs(self, cr, uid, ids, mapping, context=None):
314+ for task in self.browse(cr, uid, ids, context=context):
315+ new_parent_ids = [mapping[parent.id] for parent in task.parent_ids if parent.id in mapping]
316+ new_parent_ids += [parent.id for parent in task.parent_ids if parent.id not in mapping and parent.id not in mapping.values()]
317+ new_child_ids = [mapping[child.id] for child in task.child_ids if child.id in mapping]
318+ new_child_ids += [child.id for child in task.child_ids if child.id not in mapping and child.id not in mapping.values()]
319+ self.write(cr, uid, [task.id], {'parent_ids': [(6, 0, new_parent_ids)], 'child_ids': [(6, 0, new_child_ids)]}, context=context)
320+ return True
321
322 def copy_data(self, cr, uid, id, default=None, context=None):
323 if default is None:
324 default = {}
325- default = default or {}
326- default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
327- if not default.get('remaining_hours', False):
328- default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
329- default['active'] = True
330- if not default.get('name', False):
331- default['name'] = self.browse(cr, uid, id, context=context).name or ''
332- if not context.get('copy',False):
333- new_name = _("%s (copy)") % (default.get('name', ''))
334- default.update({'name':new_name})
335+
336+ if default and 'description_pad' in default:
337+ default.pop('description_pad')
338+
339+ current_task = self.browse(cr, uid, id, context=context)
340+ if not 'stage' in default:
341+ default['stage_id'] = self._get_default_stage_id(cr, uid, context=context)
342+ default.update({
343+ 'active': True,
344+ 'work_ids': [],
345+ 'date_start': False,
346+ 'date_end': False,
347+ 'date_deadline': False,
348+ })
349+ if 'remaining_hours' not in default:
350+ default['remaining_hours'] = float(current_task.planned_hours)
351+ if not default.get('name'):
352+ default['name'] = _("%s (copy)") % current_task.name
353 return super(task, self).copy_data(cr, uid, id, default, context)
354-
355+
356 def copy(self, cr, uid, id, default=None, context=None):
357- if context is None:
358- context = {}
359- if default is None:
360- default = {}
361- if not context.get('copy', False):
362- stage = self._get_default_stage_id(cr, uid, context=context)
363- if stage:
364- default['stage_id'] = stage
365- return super(task, self).copy(cr, uid, id, default, context)
366+ res = super(task, self).copy(cr, uid, id, default, context)
367+ # handle attachments: copy them instead of just keeping the links
368+ attach_obj = self.pool['ir.attachment']
369+ attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
370+ for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
371+ attach_obj.copy(cr, uid, attach.id, {
372+ 'name': '%s %s' % (attach.name, fields.datetime.now()),
373+ 'res_id': res,
374+ 'res_model': 'project.task',
375+ 'res_name': self.browse(cr, uid, res, context=context).name,
376+ }, context=context)
377+ return res
378
379 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
380 res = {}
381@@ -1174,6 +1216,7 @@
382 _columns = {
383 'use_tasks': fields.boolean('Tasks',help="If checked, this contract will be available in the project menu and you will be able to manage tasks or track issues"),
384 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
385+ 'project_id': fields.many2one('project.project', 'Project', ondelete='set null'),
386 }
387
388 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
389@@ -1195,15 +1238,19 @@
390 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.
391 '''
392 project_pool = self.pool.get('project.project')
393- project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])
394+ project_id = project_pool.search(cr, uid, [('analytic_account_id', '=', analytic_account_id)])
395 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
396 project_values = {
397 'name': vals.get('name'),
398 'analytic_account_id': analytic_account_id,
399- 'type': vals.get('type','contract'),
400+ 'type': vals.get('type', 'contract'),
401+ 'state': 'template' if vals.get('type') == 'template' else 'open',
402 }
403+ template_project = project_pool.search(cr, uid, [('analytic_account_id','=', vals.get('template_id'))], context=context)
404+ if template_project:
405+ return project_pool.copy(cr, uid, template_project[0], project_values, context=context)
406 return project_pool.create(cr, uid, project_values, context=context)
407- return False
408+ return project_id and project_id[0]
409
410 def create(self, cr, uid, vals, context=None):
411 if context is None:
412@@ -1211,7 +1258,8 @@
413 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
414 vals['child_ids'] = []
415 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
416- self.project_create(cr, uid, analytic_account_id, vals, context=context)
417+ project_id = self.project_create(cr, uid, analytic_account_id, vals, context=context)
418+ self.write(cr, uid, [analytic_account_id], {'project_id': project_id})
419 return analytic_account_id
420
421 def write(self, cr, uid, ids, vals, context=None):
422@@ -1223,7 +1271,7 @@
423 vals_for_project['name'] = account.name
424 if not vals.get('type'):
425 vals_for_project['type'] = account.type
426- self.project_create(cr, uid, account.id, vals_for_project, context=context)
427+ vals['project_id'] = self.project_create(cr, uid, account.id, vals_for_project, context=context)
428 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)
429
430 def unlink(self, cr, uid, ids, *args, **kwargs):
431
432=== modified file 'project/project_view.xml'
433--- project/project_view.xml 2014-05-08 15:34:32 +0000
434+++ project/project_view.xml 2014-05-15 13:02:45 +0000
435@@ -244,7 +244,7 @@
436 <field name="date"/>
437 <field name="color"/>
438 <field name="task_count"/>
439- <field name="task_ids"/>
440+ <field name="tasks"/>
441 <field name="alias_id"/>
442 <field name="doc_count"/>
443 <templates>
444@@ -265,7 +265,7 @@
445 </div>
446 <div class="oe_kanban_project_list">
447 <a t-if="record.use_tasks.raw_value" name="%(act_project_project_2_project_task_all)d" type="action" style="margin-right: 10px">
448- <t t-raw="record.task_ids.raw_value.length"/> Tasks
449+ <t t-raw="record.tasks.raw_value.length"/> Tasks
450 </a>
451 </div>
452 <div class="oe_kanban_project_list">
453@@ -608,6 +608,12 @@
454 <field name="use_tasks"/>
455 <label for="use_tasks"/>
456 </xpath>
457+ <field name="company_id" position="after">
458+ <p attrs="{'invisible': [('project_id','=',False)]}" colspan="4">
459+ To manage tasks and/or issues, go to the related project:
460+ <field name="project_id" readonly="1" required="0" class="oe_inline" nolabel="1"/>.
461+ </p>
462+ </field>
463 </field>
464 </record>
465
466
467=== modified file 'project/tests/__init__.py'
468--- project/tests/__init__.py 2013-07-10 10:15:08 +0000
469+++ project/tests/__init__.py 2014-05-15 13:02:45 +0000
470@@ -20,9 +20,11 @@
471 ##############################################################################
472
473 from . import test_project_flow
474+from . import test_contract_task_copy
475
476 checks = [
477 test_project_flow,
478+ test_contract_task_copy,
479 ]
480
481 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
482
483=== renamed file 'project/tests/test_project_base.py' => 'project/tests/common.py'
484--- project/tests/test_project_base.py 2013-08-27 13:30:58 +0000
485+++ project/tests/common.py 2014-05-15 13:02:45 +0000
486@@ -22,10 +22,10 @@
487 from openerp.addons.mail.tests.common import TestMail
488
489
490-class TestProjectBase(TestMail):
491+class TestProject(TestMail):
492
493 def setUp(self):
494- super(TestProjectBase, self).setUp()
495+ super(TestProject, self).setUp()
496 cr, uid = self.cr, self.uid
497
498 # Usefull models
499
500=== added file 'project/tests/test_contract_task_copy.py'
501--- project/tests/test_contract_task_copy.py 1970-01-01 00:00:00 +0000
502+++ project/tests/test_contract_task_copy.py 2014-05-15 13:02:45 +0000
503@@ -0,0 +1,59 @@
504+# -*- coding: utf-8 -*-
505+##############################################################################
506+#
507+# OpenERP, Open Source Business Applications
508+# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
509+#
510+# This program is free software: you can redistribute it and/or modify
511+# it under the terms of the GNU Affero General Public License as
512+# published by the Free Software Foundation, either version 3 of the
513+# License, or (at your option) any later version.
514+#
515+# This program is distributed in the hope that it will be useful,
516+# but WITHOUT ANY WARRANTY; without even the implied warranty of
517+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
518+# GNU Affero General Public License for more details.
519+#
520+# You should have received a copy of the GNU Affero General Public License
521+# along with this program. If not, see <http://www.gnu.org/licenses/>.
522+#
523+##############################################################################
524+
525+from openerp.addons.project.tests.common import TestProject
526+from openerp.osv.orm import except_orm
527+
528+class TestContractTaskCopy(TestProject):
529+
530+ def test_contract_task_copy(self):
531+ """Testing Contract task copy"""
532+ cr, uid, = self.cr, self.uid,
533+
534+ # Usefull models
535+ contract_obj = self.registry('account.analytic.account')
536+ project_obj = self.registry('project.project')
537+ task_obj = self.registry('project.task')
538+
539+ # In order to test Contract project create new Contract Template.
540+ contract_template_id = contract_obj.create(cr, uid, {
541+ 'name': 'Contract Template',
542+ 'use_tasks': '1',
543+ 'type': 'template'
544+ })
545+
546+ # Create Task for project of this contract template.
547+ contract_template_set = contract_obj.browse(cr, uid, contract_template_id)
548+ task_id = task_obj.create(cr, uid, {
549+ 'name': 'Project task',
550+ 'project_id': contract_template_set.project_id.id
551+ })
552+
553+ # Create contract based on this template.
554+ contract_id = contract_obj.create(cr, uid, {
555+ 'name': 'Template of Contract',
556+ 'template_id' : contract_template_id,
557+ 'use_tasks': '1',
558+ })
559+
560+ # Check that task for the contract project have been created same as the template.
561+ contract = contract_obj.browse(cr, uid, contract_id)
562+ self.assertTrue(len(contract.project_id.tasks) == 1, "The no of task of contracts does not match.")
563
564=== modified file 'project/tests/test_project_flow.py'
565--- project/tests/test_project_flow.py 2013-12-06 12:57:51 +0000
566+++ project/tests/test_project_flow.py 2014-05-15 13:02:45 +0000
567@@ -19,7 +19,7 @@
568 #
569 ##############################################################################
570
571-from openerp.addons.project.tests.test_project_base import TestProjectBase
572+from openerp.addons.project.tests.common import TestProject
573 from openerp.exceptions import AccessError
574 from openerp.tools import mute_logger
575
576@@ -49,7 +49,7 @@
577 Integrator at Agrolait"""
578
579
580-class TestProjectFlow(TestProjectBase):
581+class TestProjectFlow(TestProject):
582
583 @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
584 def test_00_project_process(self):
585
586=== modified file 'project_issue/project_issue.py'
587--- project_issue/project_issue.py 2014-05-08 15:25:36 +0000
588+++ project_issue/project_issue.py 2014-05-15 13:02:45 +0000
589@@ -312,8 +312,18 @@
590 default = {}
591 default = default.copy()
592 default.update(name=_('%s (copy)') % (issue['name']))
593- return super(project_issue, self).copy(cr, uid, id, default=default,
594- context=context)
595+ res = super(project_issue, self).copy(cr, uid, id, default, context)
596+ # handle attachments: copy them instead of just keeping the links
597+ attach_obj = self.pool['ir.attachment']
598+ attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
599+ for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
600+ attach_obj.copy(cr, uid, attach.id, {
601+ 'name': '%s %s' % (attach.name, fields.datetime.now()),
602+ 'res_id': res,
603+ 'res_model': 'project.issue',
604+ 'res_name': default['name'],
605+ }, context=context)
606+ return res
607
608 def create(self, cr, uid, vals, context=None):
609 if context is None:
610@@ -474,13 +484,32 @@
611 project_id: Issue.search_count(cr,uid, [('project_id', '=', project_id), ('stage_id.fold', '=', False)], context=context)
612 for project_id in ids
613 }
614+
615+ def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
616+ attachment_obj = self.pool['ir.attachment']
617+ issue_obj = self.pool['project.issue']
618+ res = super(project, self)._get_attached_docs(cr, uid, ids, field_name, arg=arg, context=context)
619+ for id in res:
620+ issue_ids = issue_obj.search(cr, uid, [('project_id', '=', id)], context=context)
621+ issue_attachments = attachment_obj.search(cr, uid, [('res_model', '=', 'project.issue'), ('res_id', 'in', issue_ids)], context=context, count=True)
622+ res[id] += issue_attachments
623+ return res
624+
625+ def attachment_tree_view(self, cr, uid, ids, context):
626+ res = super(project, self).attachment_tree_view(cr, uid, ids, context=context)
627+ issue_ids = self.pool['project.issue'].search(cr, uid, [('project_id', 'in', ids)], context=context)
628+ res['domain'].insert(0, '|')
629+ res['domain'] += ['&', ('res_model', '=', 'project.issue'), ('res_id', 'in', issue_ids)]
630+ return res
631+
632 _columns = {
633 'project_escalation_id': fields.many2one('project.project', 'Project Escalation',
634 help='If any issue is escalated from the current Project, it will be listed under the project selected here.',
635 states={'close': [('readonly', True)], 'cancelled': [('readonly', True)]}),
636 'issue_count': fields.function(_issue_count, type='integer', string="Issues",),
637 'issue_ids': fields.one2many('project.issue', 'project_id',
638- domain=[('stage_id.fold', '=', False)])
639+ domain=[('stage_id.fold', '=', False)]),
640+ 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", type='int'),
641 }
642
643 def _check_escalation(self, cr, uid, ids, context=None):
644@@ -494,6 +523,17 @@
645 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
646 ]
647
648+ def copy(self, cr, uid, id, default=None, context=None):
649+ default['issue_ids'] = []
650+ res = super(project, self).copy(cr, uid, id, default, context=context)
651+ issue_obj = self.pool['project.issue']
652+ current_project = self.browse(cr, uid, id, context=context)
653+ # copy issues
654+ if current_project.state == 'template':
655+ issue_ids = issue_obj.search(cr, uid, [('project_id','=', id)], context=context)
656+ for issue in issue_obj.browse(cr, uid, issue_ids, context=context):
657+ issue_obj.copy(cr, uid, issue.id, default={'project_id': res, 'name': issue.name}, context=context)
658+ return res
659
660 class account_analytic_account(osv.Model):
661 _inherit = 'account.analytic.account'
662
663=== modified file 'project_issue/project_issue_view.xml'
664--- project_issue/project_issue_view.xml 2014-05-08 15:34:32 +0000
665+++ project_issue/project_issue_view.xml 2014-05-15 13:02:45 +0000
666@@ -316,7 +316,7 @@
667 <a t-if="record.use_issues.raw_value" style="margin-right: 10px"
668 name="%(act_project_project_2_project_issue_all)d" type="action">
669 <t t-raw="record.issue_ids.raw_value.length"/>
670- <span t-if="record.issue_ids.raw_value.length == 1">Issue</span>
671+ <span t-if="record.issue_ids.raw_value.length &lt;= 1">Issue</span>
672 <span t-if="record.issue_ids.raw_value.length > 1">Issues</span>
673 </a>
674 </xpath>
675
676=== added directory 'project_issue/tests'
677=== added file 'project_issue/tests/__init__.py'
678--- project_issue/tests/__init__.py 1970-01-01 00:00:00 +0000
679+++ project_issue/tests/__init__.py 2014-05-15 13:02:45 +0000
680@@ -0,0 +1,28 @@
681+# -*- coding: utf-8 -*-
682+##############################################################################
683+#
684+# OpenERP, Open Source Business Applications
685+# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
686+#
687+# This program is free software: you can redistribute it and/or modify
688+# it under the terms of the GNU Affero General Public License as
689+# published by the Free Software Foundation, either version 3 of the
690+# License, or (at your option) any later version.
691+#
692+# This program is distributed in the hope that it will be useful,
693+# but WITHOUT ANY WARRANTY; without even the implied warranty of
694+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
695+# GNU Affero General Public License for more details.
696+#
697+# You should have received a copy of the GNU Affero General Public License
698+# along with this program. If not, see <http://www.gnu.org/licenses/>.
699+#
700+##############################################################################
701+
702+from . import test_contract_issue_copy
703+
704+checks = [
705+ test_contract_issue_copy,
706+]
707+
708+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
709
710=== added file 'project_issue/tests/test_contract_issue_copy.py'
711--- project_issue/tests/test_contract_issue_copy.py 1970-01-01 00:00:00 +0000
712+++ project_issue/tests/test_contract_issue_copy.py 2014-05-15 13:02:45 +0000
713@@ -0,0 +1,66 @@
714+# -*- coding: utf-8 -*-
715+##############################################################################
716+#
717+# OpenERP, Open Source Business Applications
718+# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
719+#
720+# This program is free software: you can redistribute it and/or modify
721+# it under the terms of the GNU Affero General Public License as
722+# published by the Free Software Foundation, either version 3 of the
723+# License, or (at your option) any later version.
724+#
725+# This program is distributed in the hope that it will be useful,
726+# but WITHOUT ANY WARRANTY; without even the implied warranty of
727+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
728+# GNU Affero General Public License for more details.
729+#
730+# You should have received a copy of the GNU Affero General Public License
731+# along with this program. If not, see <http://www.gnu.org/licenses/>.
732+#
733+##############################################################################
734+
735+from openerp.addons.mail.tests.common import TestMail
736+from openerp.tools import mute_logger
737+
738+
739+class TestIssue(TestMail):
740+ @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
741+
742+ def setUp(self):
743+ super(TestIssue, self).setUp()
744+
745+ def test_00_issue_process(self):
746+ """ Testing project issue from contract templates"""
747+ cr, uid = self.cr, self.uid
748+
749+ # Usefull models
750+ project_project = self.registry('project.project')
751+ project_issue = self.registry('project.issue')
752+ account_analytic_account = self.registry('account.analytic.account')
753+
754+ # In order to test Contract project create a new Contract Template.
755+ template_id = account_analytic_account.create(cr, uid, {
756+ 'name': 'Template Contract',
757+ 'type': 'template',
758+ 'use_issues': 1,
759+ })
760+
761+ # I create Issue for project of this template.
762+ template_contract = account_analytic_account.browse(cr, uid, template_id)
763+ project_issue_id = project_issue.create(cr, uid, {
764+ 'name': 'Test Issue',
765+ 'project_id': template_contract.project_id.id
766+ })
767+ template_contract.refresh()
768+
769+ self.assertTrue(len(template_contract.project_id.issue_ids) == 1, "project of the contract should have one issue")
770+ # I create a contract based on this template.
771+ contract_id = account_analytic_account.create(cr, uid, {
772+ 'name': 'Contract Project',
773+ 'use_issues': 1,
774+ 'template_id': template_id,
775+ })
776+
777+ # I check that issues for the contract project have been created same as the template.
778+ contract = account_analytic_account.browse(cr, uid, contract_id)
779+ self.assertTrue(len(contract.project_id.issue_ids) == 1, "The no of issue of contracts does not match with the no of issue of contract template.")

Subscribers

People subscribed via source and target branches

to all changes: