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
=== modified file 'portal_project/tests/test_access_rights.py'
--- portal_project/tests/test_access_rights.py 2014-01-16 10:39:10 +0000
+++ portal_project/tests/test_access_rights.py 2014-05-15 13:02:45 +0000
@@ -19,13 +19,13 @@
19#19#
20##############################################################################20##############################################################################
2121
22from openerp.addons.project.tests.test_project_base import TestProjectBase22from openerp.addons.project.tests.common import TestProject
23from openerp.exceptions import AccessError23from openerp.exceptions import AccessError
24from openerp.osv.orm import except_orm24from openerp.osv.orm import except_orm
25from openerp.tools import mute_logger25from openerp.tools import mute_logger
2626
2727
28class TestPortalProjectBase(TestProjectBase):28class TestPortalProjectBase(TestProject):
2929
30 def setUp(self):30 def setUp(self):
31 super(TestPortalProjectBase, self).setUp()31 super(TestPortalProjectBase, self).setUp()
3232
=== modified file 'project/project.py'
--- project/project.py 2014-05-13 11:18:37 +0000
+++ project/project.py 2014-05-15 13:02:45 +0000
@@ -19,7 +19,7 @@
19#19#
20##############################################################################20##############################################################################
2121
22from datetime import datetime, date22from datetime import date, datetime
23from lxml import etree23from lxml import etree
24import time24import time
2525
@@ -180,6 +180,7 @@
180 res = {}180 res = {}
181 attachment = self.pool.get('ir.attachment')181 attachment = self.pool.get('ir.attachment')
182 task = self.pool.get('project.task')182 task = self.pool.get('project.task')
183 context['active_test'] = False
183 for id in ids:184 for id in ids:
184 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)185 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)
185 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)186 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)
@@ -187,9 +188,16 @@
187 res[id] = (project_attachments or 0) + (task_attachments or 0)188 res[id] = (project_attachments or 0) + (task_attachments or 0)
188 return res189 return res
189 def _task_count(self, cr, uid, ids, field_name, arg, context=None):190 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
190 res={}191 """ :deprecated: this method will be removed with OpenERP v8. Use tasks
191 for tasks in self.browse(cr, uid, ids, context):192 fields instead. """
192 res[tasks.id] = len(tasks.task_ids)193 if context is None:
194 context = {}
195 res = dict.fromkeys(ids, 0)
196 ctx = context.copy()
197 ctx['active_test'] = False
198 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=ctx)
199 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
200 res[task.project_id.id] += 1
193 return res201 return res
194 def _get_alias_models(self, cr, uid, context=None):202 def _get_alias_models(self, cr, uid, context=None):
195 """ Overriden in project_issue to offer more options """203 """ Overriden in project_issue to offer more options """
@@ -202,7 +210,10 @@
202 ('followers', 'Private project: followers Only')]210 ('followers', 'Private project: followers Only')]
203211
204 def attachment_tree_view(self, cr, uid, ids, context):212 def attachment_tree_view(self, cr, uid, ids, context):
205 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])213 if context is None:
214 context = {}
215 context['active_test'] = False
216 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=context)
206 domain = [217 domain = [
207 '|',218 '|',
208 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),219 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
@@ -217,7 +228,7 @@
217 'view_mode': 'kanban,tree,form',228 'view_mode': 'kanban,tree,form',
218 'view_type': 'form',229 'view_type': 'form',
219 'limit': 80,230 'limit': 80,
220 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)231 'context': "{'default_res_model': '%s','default_res_id': %d, 'active_test': False}" % (self._name, res_id)
221 }232 }
222233
223 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field234 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
@@ -256,9 +267,8 @@
256 }),267 }),
257 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),268 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
258 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),269 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
259 'task_count': fields.function(_task_count, type='integer', string="Tasks",),270 'task_count': fields.function(_task_count, type='integer', string="Open Tasks",
260 'task_ids': fields.one2many('project.task', 'project_id',271 deprecated="This field will be removed in OpenERP v8. Use tasks one2many field instead."),
261 domain=[('stage_id.fold', '=', False)]),
262 'color': fields.integer('Color Index'),272 'color': fields.integer('Color Index'),
263 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,273 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
264 help="Internal email associated with this project. Incoming emails are automatically synchronized"274 help="Internal email associated with this project. Incoming emails are automatically synchronized"
@@ -310,7 +320,7 @@
310 ]320 ]
311321
312 def set_template(self, cr, uid, ids, context=None):322 def set_template(self, cr, uid, ids, context=None):
313 return self.setActive(cr, uid, ids, value=False, context=context)323 return self.set_template_state(cr, uid, ids, True, context=context)
314324
315 def set_done(self, cr, uid, ids, context=None):325 def set_done(self, cr, uid, ids, context=None):
316 return self.write(cr, uid, ids, {'state': 'close'}, context=context)326 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
@@ -325,104 +335,117 @@
325 return self.write(cr, uid, ids, {'state': 'open'}, context=context)335 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
326336
327 def reset_project(self, cr, uid, ids, context=None):337 def reset_project(self, cr, uid, ids, context=None):
328 return self.setActive(cr, uid, ids, value=True, context=context)338 return self.set_template_state(cr, uid, ids, False, context=context)
329339
330 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):340 def copy_data(self, cr, uid, id, default=None, context=None):
331 """ copy and map tasks from old to new project """
332 if context is None:
333 context = {}
334 map_task_id = {}
335 task_obj = self.pool.get('project.task')
336 proj = self.browse(cr, uid, old_project_id, context=context)
337 for task in proj.tasks:
338 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
339 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
340 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
341 return True
342
343 def copy(self, cr, uid, id, default=None, context=None):
344 if context is None:
345 context = {}
346 if default is None:341 if default is None:
347 default = {}342 default = {}
348343 current_project = self.browse(cr, uid, id, context=context)
349 context['active_test'] = False
350 default['state'] = 'open'
351 default['line_ids'] = []344 default['line_ids'] = []
352 default['tasks'] = []345 # update name
353346 if not default.get('name'):
354 # Don't prepare (expensive) data to copy children (analytic accounts),347 default.update(name=_("%s (copy %s)") % (current_project.name, fields.datetime.now()))
355 # they are discarded in analytic.copy(), and handled in duplicate_template() 348
356 default['child_ids'] = []349 # handle tasks
357350 if current_project.state == 'template':
358 proj = self.browse(cr, uid, id, context=context)351 default['tasks'] = self.pool['project.task'].duplicate_task(cr, uid, current_project.tasks, context=context)
359 if not default.get('name', False):352 else:
360 default.update(name=_("%s (copy)") % (proj.name))353 default['tasks'] = []
354 return super(project, self).copy_data(cr, uid, id, default, context)
355
356 def copy(self, cr, uid, id, default=None, context=None):
357 if context is None:
358 context = {}
359 context['active_test'] = False
361 res = super(project, self).copy(cr, uid, id, default, context)360 res = super(project, self).copy(cr, uid, id, default, context)
362 self.map_tasks(cr, uid, id, res, context=context)361 current_project = self.browse(cr, uid, id, context=context)
362 # handle attachments: copy them instead of just keeping the links
363 if current_project.state == 'template':
364 attach_obj = self.pool['ir.attachment']
365 attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
366 for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
367 attach_obj.copy(cr, uid, attach.id, {
368 'name': '%s %s' % (attach.name, fields.datetime.now()),
369 'res_id': res,
370 'res_model': 'project.project',
371 'res_name': default['name']
372 }, context=context)
363 return res373 return res
364374
365 def duplicate_template(self, cr, uid, ids, context=None):375 def duplicate_template(self, cr, uid, ids, context=None):
376 return self.create_from_template(cr, uid, ids, context=context)
377
378 def create_from_template(self, cr, uid, ids, context=None):
366 if context is None:379 if context is None:
367 context = {}380 context = {}
381 context['copy'] = True # compatibility with some old ugly code in account
368 data_obj = self.pool.get('ir.model.data')382 data_obj = self.pool.get('ir.model.data')
369 result = []383
370 for proj in self.browse(cr, uid, ids, context=context):384 new_projects = []
371 parent_id = context.get('parent_id', False)385 for template in self.browse(cr, uid, ids, context=context):
372 context.update({'analytic_project_copy': True})386 date_start = date.today()
373 new_date_start = time.strftime('%Y-%m-%d')387 date_end = False
374 new_date_end = False388 if template.date and template.date_start:
375 if proj.date_start and proj.date:389 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())
376 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])390 project_id = self.copy(cr, uid, template.id, default={
377 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])391 'state': 'open',
378 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')392 'date_start': date_start,
379 context.update({'copy':True})393 'date': date_end,
380 new_id = self.copy(cr, uid, proj.id, default = {394 'parent_id': context.get('parent_id', False),
381 'name':_("%s (copy)") % (proj.name),395 }, context=context)
382 'state':'open',396 new_projects.append(project_id)
383 'date_start':new_date_start,397
384 'date':new_date_end,398 child_ids = self.search(cr, uid, [('parent_id', '=', template.analytic_account_id.id)], context=context)
385 'parent_id':parent_id}, context=context)
386 result.append(new_id)
387
388 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
389 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
390 if child_ids:399 if child_ids:
391 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})400 project_account_id = self.browse(cr, uid, project_id, ['analytic_account_id'], context=context).analytic_account_id.id
401 self.duplicate_template(cr, uid, child_ids, context={'parent_id': project_account_id})
392402
393 if result and len(result):403 if new_projects:
394 res_id = result[0]404 try:
395 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')405 form_view_id = data_obj.get_object_reference(cr, uid, 'project', 'edit_project')[1]
396 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])406 except ValueError:
397 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')407 form_view_id = False
398 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])408 try:
399 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')409 tree_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project')[1]
400 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])410 except ValueError:
411 tree_view_id = False
412 try:
413 search_view_id = data_obj.get_object_reference(cr, uid, 'project', 'view_project_project_filter')[1]
414 except ValueError:
415 search_view_id = False
401 return {416 return {
402 'name': _('Projects'),417 'name': _('Projects'),
403 'view_type': 'form',418 'view_type': 'form',
404 'view_mode': 'form,tree',419 'view_mode': 'form,tree',
405 'res_model': 'project.project',420 'res_model': 'project.project',
406 'view_id': False,421 'view_id': False,
407 'res_id': res_id,422 'res_id': new_projects[0],
408 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],423 'views': [(form_view_id, 'form'), (tree_view_id, 'tree')],
409 'type': 'ir.actions.act_window',424 'type': 'ir.actions.act_window',
410 'search_view_id': search_view['res_id'],425 'search_view_id': search_view_id,
411 'nodestroy': True426 'nodestroy': True
412 }427 }
428 return False
413429
414 # set active value for a project, its sub projects and its tasks430 # set active value for a project, its sub projects and its tasks
415 def setActive(self, cr, uid, ids, value=True, context=None):431 def setActive(self, cr, uid, ids, value=True, context=None):
416 task_obj = self.pool.get('project.task')432 return self.set_template_state(cr, uid, ids, not value, context=context)
417 for proj in self.browse(cr, uid, ids, context=None):433
418 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)434 def set_template_state(self, cr, uid, ids, template_state=False, context=None):
419 cr.execute('select id from project_task where project_id=%s', (proj.id,))435 """ (Re)set the project as template.
420 tasks_id = [x[0] for x in cr.fetchall()]436 :param boolean template_state: True => 'template', False => 'open'
421 if tasks_id:437 """
422 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)438 if context is None:
423 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])439 context = {}
424 if child_ids:440 active_ctx = dict({'active_test': False})
425 self.setActive(cr, uid, child_ids, value, context=None)441 state = 'template' if template_state else 'open'
442 self.write(cr, uid, ids, {'state': state}, context=context)
443 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
444 self.pool['project.task'].write(cr, uid, task_ids, {'active': state == 'open'}, context=context)
445 parent_ids = [project.analytic_account_id.id for project in self.browse(cr, uid, ids, context=context)]
446 child_project_ids = self.search(cr, uid, [('parent_id', 'in', parent_ids)], context=context)
447 if child_project_ids:
448 return self.set_template_state(cr, uid, child_project_ids, state, context=context)
426 return True449 return True
427450
428 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):451 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
@@ -688,39 +711,58 @@
688 return {'value': vals}711 return {'value': vals}
689712
690 def duplicate_task(self, cr, uid, map_ids, context=None):713 def duplicate_task(self, cr, uid, map_ids, context=None):
691 mapper = lambda t: map_ids.get(t.id, t.id)714 mapping = {}
692 for task in self.browse(cr, uid, map_ids.values(), context):715 task_obj = self.pool['project.task']
693 new_child_ids = set(map(mapper, task.child_ids))716 for task in map_ids:
694 new_parent_ids = set(map(mapper, task.parent_ids))717 mapping[task.id] = task_obj.copy(cr, uid, task.id, context=context)
695 if new_child_ids or new_parent_ids:718 task_obj.update_parents_and_childs(cr, uid, mapping.values(), mapping, context=context)
696 task.write({'parent_ids': [(6,0,list(new_parent_ids))],719 return [(6, 0, mapping.values())]
697 'child_ids': [(6,0,list(new_child_ids))]})720
721 def update_parents_and_childs(self, cr, uid, ids, mapping, context=None):
722 for task in self.browse(cr, uid, ids, context=context):
723 new_parent_ids = [mapping[parent.id] for parent in task.parent_ids if parent.id in mapping]
724 new_parent_ids += [parent.id for parent in task.parent_ids if parent.id not in mapping and parent.id not in mapping.values()]
725 new_child_ids = [mapping[child.id] for child in task.child_ids if child.id in mapping]
726 new_child_ids += [child.id for child in task.child_ids if child.id not in mapping and child.id not in mapping.values()]
727 self.write(cr, uid, [task.id], {'parent_ids': [(6, 0, new_parent_ids)], 'child_ids': [(6, 0, new_child_ids)]}, context=context)
728 return True
698729
699 def copy_data(self, cr, uid, id, default=None, context=None):730 def copy_data(self, cr, uid, id, default=None, context=None):
700 if default is None:731 if default is None:
701 default = {}732 default = {}
702 default = default or {}733
703 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})734 if default and 'description_pad' in default:
704 if not default.get('remaining_hours', False):735 default.pop('description_pad')
705 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])736
706 default['active'] = True737 current_task = self.browse(cr, uid, id, context=context)
707 if not default.get('name', False):738 if not 'stage' in default:
708 default['name'] = self.browse(cr, uid, id, context=context).name or ''739 default['stage_id'] = self._get_default_stage_id(cr, uid, context=context)
709 if not context.get('copy',False):740 default.update({
710 new_name = _("%s (copy)") % (default.get('name', ''))741 'active': True,
711 default.update({'name':new_name})742 'work_ids': [],
743 'date_start': False,
744 'date_end': False,
745 'date_deadline': False,
746 })
747 if 'remaining_hours' not in default:
748 default['remaining_hours'] = float(current_task.planned_hours)
749 if not default.get('name'):
750 default['name'] = _("%s (copy)") % current_task.name
712 return super(task, self).copy_data(cr, uid, id, default, context)751 return super(task, self).copy_data(cr, uid, id, default, context)
713 752
714 def copy(self, cr, uid, id, default=None, context=None):753 def copy(self, cr, uid, id, default=None, context=None):
715 if context is None:754 res = super(task, self).copy(cr, uid, id, default, context)
716 context = {}755 # handle attachments: copy them instead of just keeping the links
717 if default is None:756 attach_obj = self.pool['ir.attachment']
718 default = {}757 attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
719 if not context.get('copy', False):758 for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
720 stage = self._get_default_stage_id(cr, uid, context=context)759 attach_obj.copy(cr, uid, attach.id, {
721 if stage:760 'name': '%s %s' % (attach.name, fields.datetime.now()),
722 default['stage_id'] = stage761 'res_id': res,
723 return super(task, self).copy(cr, uid, id, default, context)762 'res_model': 'project.task',
763 'res_name': self.browse(cr, uid, res, context=context).name,
764 }, context=context)
765 return res
724766
725 def _is_template(self, cr, uid, ids, field_name, arg, context=None):767 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
726 res = {}768 res = {}
@@ -1174,6 +1216,7 @@
1174 _columns = {1216 _columns = {
1175 '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"),1217 '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"),
1176 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),1218 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1219 'project_id': fields.many2one('project.project', 'Project', ondelete='set null'),
1177 }1220 }
11781221
1179 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):1222 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
@@ -1195,15 +1238,19 @@
1195 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.1238 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.
1196 '''1239 '''
1197 project_pool = self.pool.get('project.project')1240 project_pool = self.pool.get('project.project')
1198 project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])1241 project_id = project_pool.search(cr, uid, [('analytic_account_id', '=', analytic_account_id)])
1199 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):1242 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
1200 project_values = {1243 project_values = {
1201 'name': vals.get('name'),1244 'name': vals.get('name'),
1202 'analytic_account_id': analytic_account_id,1245 'analytic_account_id': analytic_account_id,
1203 'type': vals.get('type','contract'),1246 'type': vals.get('type', 'contract'),
1247 'state': 'template' if vals.get('type') == 'template' else 'open',
1204 }1248 }
1249 template_project = project_pool.search(cr, uid, [('analytic_account_id','=', vals.get('template_id'))], context=context)
1250 if template_project:
1251 return project_pool.copy(cr, uid, template_project[0], project_values, context=context)
1205 return project_pool.create(cr, uid, project_values, context=context)1252 return project_pool.create(cr, uid, project_values, context=context)
1206 return False1253 return project_id and project_id[0]
12071254
1208 def create(self, cr, uid, vals, context=None):1255 def create(self, cr, uid, vals, context=None):
1209 if context is None:1256 if context is None:
@@ -1211,7 +1258,8 @@
1211 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):1258 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1212 vals['child_ids'] = []1259 vals['child_ids'] = []
1213 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)1260 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1214 self.project_create(cr, uid, analytic_account_id, vals, context=context)1261 project_id = self.project_create(cr, uid, analytic_account_id, vals, context=context)
1262 self.write(cr, uid, [analytic_account_id], {'project_id': project_id})
1215 return analytic_account_id1263 return analytic_account_id
12161264
1217 def write(self, cr, uid, ids, vals, context=None):1265 def write(self, cr, uid, ids, vals, context=None):
@@ -1223,7 +1271,7 @@
1223 vals_for_project['name'] = account.name1271 vals_for_project['name'] = account.name
1224 if not vals.get('type'):1272 if not vals.get('type'):
1225 vals_for_project['type'] = account.type1273 vals_for_project['type'] = account.type
1226 self.project_create(cr, uid, account.id, vals_for_project, context=context)1274 vals['project_id'] = self.project_create(cr, uid, account.id, vals_for_project, context=context)
1227 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)1275 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)
12281276
1229 def unlink(self, cr, uid, ids, *args, **kwargs):1277 def unlink(self, cr, uid, ids, *args, **kwargs):
12301278
=== modified file 'project/project_view.xml'
--- project/project_view.xml 2014-05-08 15:34:32 +0000
+++ project/project_view.xml 2014-05-15 13:02:45 +0000
@@ -244,7 +244,7 @@
244 <field name="date"/>244 <field name="date"/>
245 <field name="color"/>245 <field name="color"/>
246 <field name="task_count"/>246 <field name="task_count"/>
247 <field name="task_ids"/>247 <field name="tasks"/>
248 <field name="alias_id"/>248 <field name="alias_id"/>
249 <field name="doc_count"/>249 <field name="doc_count"/>
250 <templates>250 <templates>
@@ -265,7 +265,7 @@
265 </div>265 </div>
266 <div class="oe_kanban_project_list">266 <div class="oe_kanban_project_list">
267 <a t-if="record.use_tasks.raw_value" name="%(act_project_project_2_project_task_all)d" type="action" style="margin-right: 10px">267 <a t-if="record.use_tasks.raw_value" name="%(act_project_project_2_project_task_all)d" type="action" style="margin-right: 10px">
268 <t t-raw="record.task_ids.raw_value.length"/> Tasks268 <t t-raw="record.tasks.raw_value.length"/> Tasks
269 </a>269 </a>
270 </div>270 </div>
271 <div class="oe_kanban_project_list">271 <div class="oe_kanban_project_list">
@@ -608,6 +608,12 @@
608 <field name="use_tasks"/>608 <field name="use_tasks"/>
609 <label for="use_tasks"/>609 <label for="use_tasks"/>
610 </xpath>610 </xpath>
611 <field name="company_id" position="after">
612 <p attrs="{'invisible': [('project_id','=',False)]}" colspan="4">
613 To manage tasks and/or issues, go to the related project:
614 <field name="project_id" readonly="1" required="0" class="oe_inline" nolabel="1"/>.
615 </p>
616 </field>
611 </field>617 </field>
612 </record>618 </record>
613619
614620
=== modified file 'project/tests/__init__.py'
--- project/tests/__init__.py 2013-07-10 10:15:08 +0000
+++ project/tests/__init__.py 2014-05-15 13:02:45 +0000
@@ -20,9 +20,11 @@
20##############################################################################20##############################################################################
2121
22from . import test_project_flow22from . import test_project_flow
23from . import test_contract_task_copy
2324
24checks = [25checks = [
25 test_project_flow,26 test_project_flow,
27 test_contract_task_copy,
26]28]
2729
28# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:30# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
2931
=== renamed file 'project/tests/test_project_base.py' => 'project/tests/common.py'
--- project/tests/test_project_base.py 2013-08-27 13:30:58 +0000
+++ project/tests/common.py 2014-05-15 13:02:45 +0000
@@ -22,10 +22,10 @@
22from openerp.addons.mail.tests.common import TestMail22from openerp.addons.mail.tests.common import TestMail
2323
2424
25class TestProjectBase(TestMail):25class TestProject(TestMail):
2626
27 def setUp(self):27 def setUp(self):
28 super(TestProjectBase, self).setUp()28 super(TestProject, self).setUp()
29 cr, uid = self.cr, self.uid29 cr, uid = self.cr, self.uid
3030
31 # Usefull models31 # Usefull models
3232
=== added file 'project/tests/test_contract_task_copy.py'
--- project/tests/test_contract_task_copy.py 1970-01-01 00:00:00 +0000
+++ project/tests/test_contract_task_copy.py 2014-05-15 13:02:45 +0000
@@ -0,0 +1,59 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.addons.project.tests.common import TestProject
23from openerp.osv.orm import except_orm
24
25class TestContractTaskCopy(TestProject):
26
27 def test_contract_task_copy(self):
28 """Testing Contract task copy"""
29 cr, uid, = self.cr, self.uid,
30
31 # Usefull models
32 contract_obj = self.registry('account.analytic.account')
33 project_obj = self.registry('project.project')
34 task_obj = self.registry('project.task')
35
36 # In order to test Contract project create new Contract Template.
37 contract_template_id = contract_obj.create(cr, uid, {
38 'name': 'Contract Template',
39 'use_tasks': '1',
40 'type': 'template'
41 })
42
43 # Create Task for project of this contract template.
44 contract_template_set = contract_obj.browse(cr, uid, contract_template_id)
45 task_id = task_obj.create(cr, uid, {
46 'name': 'Project task',
47 'project_id': contract_template_set.project_id.id
48 })
49
50 # Create contract based on this template.
51 contract_id = contract_obj.create(cr, uid, {
52 'name': 'Template of Contract',
53 'template_id' : contract_template_id,
54 'use_tasks': '1',
55 })
56
57 # Check that task for the contract project have been created same as the template.
58 contract = contract_obj.browse(cr, uid, contract_id)
59 self.assertTrue(len(contract.project_id.tasks) == 1, "The no of task of contracts does not match.")
060
=== modified file 'project/tests/test_project_flow.py'
--- project/tests/test_project_flow.py 2013-12-06 12:57:51 +0000
+++ project/tests/test_project_flow.py 2014-05-15 13:02:45 +0000
@@ -19,7 +19,7 @@
19#19#
20##############################################################################20##############################################################################
2121
22from openerp.addons.project.tests.test_project_base import TestProjectBase22from openerp.addons.project.tests.common import TestProject
23from openerp.exceptions import AccessError23from openerp.exceptions import AccessError
24from openerp.tools import mute_logger24from openerp.tools import mute_logger
2525
@@ -49,7 +49,7 @@
49Integrator at Agrolait"""49Integrator at Agrolait"""
5050
5151
52class TestProjectFlow(TestProjectBase):52class TestProjectFlow(TestProject):
5353
54 @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')54 @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
55 def test_00_project_process(self):55 def test_00_project_process(self):
5656
=== modified file 'project_issue/project_issue.py'
--- project_issue/project_issue.py 2014-05-08 15:25:36 +0000
+++ project_issue/project_issue.py 2014-05-15 13:02:45 +0000
@@ -312,8 +312,18 @@
312 default = {}312 default = {}
313 default = default.copy()313 default = default.copy()
314 default.update(name=_('%s (copy)') % (issue['name']))314 default.update(name=_('%s (copy)') % (issue['name']))
315 return super(project_issue, self).copy(cr, uid, id, default=default,315 res = super(project_issue, self).copy(cr, uid, id, default, context)
316 context=context)316 # handle attachments: copy them instead of just keeping the links
317 attach_obj = self.pool['ir.attachment']
318 attach_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id)], context=context)
319 for attach in attach_obj.browse(cr, uid, attach_ids, context=context):
320 attach_obj.copy(cr, uid, attach.id, {
321 'name': '%s %s' % (attach.name, fields.datetime.now()),
322 'res_id': res,
323 'res_model': 'project.issue',
324 'res_name': default['name'],
325 }, context=context)
326 return res
317327
318 def create(self, cr, uid, vals, context=None):328 def create(self, cr, uid, vals, context=None):
319 if context is None:329 if context is None:
@@ -474,13 +484,32 @@
474 project_id: Issue.search_count(cr,uid, [('project_id', '=', project_id), ('stage_id.fold', '=', False)], context=context)484 project_id: Issue.search_count(cr,uid, [('project_id', '=', project_id), ('stage_id.fold', '=', False)], context=context)
475 for project_id in ids485 for project_id in ids
476 }486 }
487
488 def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
489 attachment_obj = self.pool['ir.attachment']
490 issue_obj = self.pool['project.issue']
491 res = super(project, self)._get_attached_docs(cr, uid, ids, field_name, arg=arg, context=context)
492 for id in res:
493 issue_ids = issue_obj.search(cr, uid, [('project_id', '=', id)], context=context)
494 issue_attachments = attachment_obj.search(cr, uid, [('res_model', '=', 'project.issue'), ('res_id', 'in', issue_ids)], context=context, count=True)
495 res[id] += issue_attachments
496 return res
497
498 def attachment_tree_view(self, cr, uid, ids, context):
499 res = super(project, self).attachment_tree_view(cr, uid, ids, context=context)
500 issue_ids = self.pool['project.issue'].search(cr, uid, [('project_id', 'in', ids)], context=context)
501 res['domain'].insert(0, '|')
502 res['domain'] += ['&', ('res_model', '=', 'project.issue'), ('res_id', 'in', issue_ids)]
503 return res
504
477 _columns = {505 _columns = {
478 'project_escalation_id': fields.many2one('project.project', 'Project Escalation',506 'project_escalation_id': fields.many2one('project.project', 'Project Escalation',
479 help='If any issue is escalated from the current Project, it will be listed under the project selected here.',507 help='If any issue is escalated from the current Project, it will be listed under the project selected here.',
480 states={'close': [('readonly', True)], 'cancelled': [('readonly', True)]}),508 states={'close': [('readonly', True)], 'cancelled': [('readonly', True)]}),
481 'issue_count': fields.function(_issue_count, type='integer', string="Issues",),509 'issue_count': fields.function(_issue_count, type='integer', string="Issues",),
482 'issue_ids': fields.one2many('project.issue', 'project_id',510 'issue_ids': fields.one2many('project.issue', 'project_id',
483 domain=[('stage_id.fold', '=', False)])511 domain=[('stage_id.fold', '=', False)]),
512 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", type='int'),
484 }513 }
485514
486 def _check_escalation(self, cr, uid, ids, context=None):515 def _check_escalation(self, cr, uid, ids, context=None):
@@ -494,6 +523,17 @@
494 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])523 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
495 ]524 ]
496525
526 def copy(self, cr, uid, id, default=None, context=None):
527 default['issue_ids'] = []
528 res = super(project, self).copy(cr, uid, id, default, context=context)
529 issue_obj = self.pool['project.issue']
530 current_project = self.browse(cr, uid, id, context=context)
531 # copy issues
532 if current_project.state == 'template':
533 issue_ids = issue_obj.search(cr, uid, [('project_id','=', id)], context=context)
534 for issue in issue_obj.browse(cr, uid, issue_ids, context=context):
535 issue_obj.copy(cr, uid, issue.id, default={'project_id': res, 'name': issue.name}, context=context)
536 return res
497537
498class account_analytic_account(osv.Model):538class account_analytic_account(osv.Model):
499 _inherit = 'account.analytic.account'539 _inherit = 'account.analytic.account'
500540
=== modified file 'project_issue/project_issue_view.xml'
--- project_issue/project_issue_view.xml 2014-05-08 15:34:32 +0000
+++ project_issue/project_issue_view.xml 2014-05-15 13:02:45 +0000
@@ -316,7 +316,7 @@
316 <a t-if="record.use_issues.raw_value" style="margin-right: 10px"316 <a t-if="record.use_issues.raw_value" style="margin-right: 10px"
317 name="%(act_project_project_2_project_issue_all)d" type="action">317 name="%(act_project_project_2_project_issue_all)d" type="action">
318 <t t-raw="record.issue_ids.raw_value.length"/>318 <t t-raw="record.issue_ids.raw_value.length"/>
319 <span t-if="record.issue_ids.raw_value.length == 1">Issue</span>319 <span t-if="record.issue_ids.raw_value.length &lt;= 1">Issue</span>
320 <span t-if="record.issue_ids.raw_value.length > 1">Issues</span>320 <span t-if="record.issue_ids.raw_value.length > 1">Issues</span>
321 </a>321 </a>
322 </xpath>322 </xpath>
323323
=== added directory 'project_issue/tests'
=== added file 'project_issue/tests/__init__.py'
--- project_issue/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ project_issue/tests/__init__.py 2014-05-15 13:02:45 +0000
@@ -0,0 +1,28 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import test_contract_issue_copy
23
24checks = [
25 test_contract_issue_copy,
26]
27
28# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
029
=== added file 'project_issue/tests/test_contract_issue_copy.py'
--- project_issue/tests/test_contract_issue_copy.py 1970-01-01 00:00:00 +0000
+++ project_issue/tests/test_contract_issue_copy.py 2014-05-15 13:02:45 +0000
@@ -0,0 +1,66 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Business Applications
5# Copyright (c) 2013-TODAY OpenERP S.A. <http://www.openerp.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.addons.mail.tests.common import TestMail
23from openerp.tools import mute_logger
24
25
26class TestIssue(TestMail):
27 @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
28
29 def setUp(self):
30 super(TestIssue, self).setUp()
31
32 def test_00_issue_process(self):
33 """ Testing project issue from contract templates"""
34 cr, uid = self.cr, self.uid
35
36 # Usefull models
37 project_project = self.registry('project.project')
38 project_issue = self.registry('project.issue')
39 account_analytic_account = self.registry('account.analytic.account')
40
41 # In order to test Contract project create a new Contract Template.
42 template_id = account_analytic_account.create(cr, uid, {
43 'name': 'Template Contract',
44 'type': 'template',
45 'use_issues': 1,
46 })
47
48 # I create Issue for project of this template.
49 template_contract = account_analytic_account.browse(cr, uid, template_id)
50 project_issue_id = project_issue.create(cr, uid, {
51 'name': 'Test Issue',
52 'project_id': template_contract.project_id.id
53 })
54 template_contract.refresh()
55
56 self.assertTrue(len(template_contract.project_id.issue_ids) == 1, "project of the contract should have one issue")
57 # I create a contract based on this template.
58 contract_id = account_analytic_account.create(cr, uid, {
59 'name': 'Contract Project',
60 'use_issues': 1,
61 'template_id': template_id,
62 })
63
64 # I check that issues for the contract project have been created same as the template.
65 contract = account_analytic_account.browse(cr, uid, contract_id)
66 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: