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