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