[MERGE] Merge with lp:openobject-addons
[odoo/odoo.git] / addons / project / project.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from lxml import etree
23 import time
24 from datetime import datetime, date
25 from operator import itemgetter
26 from itertools import groupby
27
28 from tools.misc import flatten
29 from tools.translate import _
30 from osv import fields, osv
31
32
33 class project_task_type(osv.osv):
34     _name = 'project.task.type'
35     _description = 'Task Stage'
36     _order = 'sequence'
37     _columns = {
38         'name': fields.char('Stage Name', required=True, size=64, translate=True),
39         'description': fields.text('Description'),
40         'sequence': fields.integer('Sequence'),
41     }
42
43     _defaults = {
44         'sequence': 1
45     }
46
47 project_task_type()
48
49 class project(osv.osv):
50     _name = "project.project"
51     _description = "Project"
52     _inherits = {'account.analytic.account': "analytic_account_id"}
53
54     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
55         if user == 1:
56             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
57         if context and context.has_key('user_prefence') and context['user_prefence']:
58                 cr.execute("""SELECT project.id FROM project_project project
59                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
60                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
61                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
62                 return [(r[0]) for r in cr.fetchall()]
63         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
64             context=context, count=count)
65
66     def _complete_name(self, cr, uid, ids, name, args, context=None):
67         res = {}
68         for m in self.browse(cr, uid, ids, context=context):
69             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
70         return res
71
72     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
73         partner_obj = self.pool.get('res.partner')
74         if not part:
75             return {'value':{'contact_id': False, 'pricelist_id': False}}
76         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
77         pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
78         pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
79         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
80
81     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
82         res = {}.fromkeys(ids, 0.0)
83         progress = {}
84         if not ids:
85             return res
86         cr.execute('''SELECT
87                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
88             FROM
89                 project_task
90             WHERE
91                 project_id in %s AND
92                 state<>'cancelled'
93             GROUP BY
94                 project_id''', (tuple(ids),))
95         progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
96         for project in self.browse(cr, uid, ids, context=context):
97             s = progress.get(project.id, (0.0,0.0,0.0,0.0))
98             res[project.id] = {
99                 'planned_hours': s[0],
100                 'effective_hours': s[2],
101                 'total_hours': s[1],
102                 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
103             }
104         return res
105
106     def _get_project_task(self, cr, uid, ids, context=None):
107         result = {}
108         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
109             if task.project_id: result[task.project_id.id] = True
110         return result.keys()
111
112     def _get_project_work(self, cr, uid, ids, context=None):
113         result = {}
114         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
115             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
116         return result.keys()
117
118     def unlink(self, cr, uid, ids, *args, **kwargs):
119         for proj in self.browse(cr, uid, ids):
120             if proj.tasks:
121                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
122         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
123
124     _columns = {
125         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
126         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
127         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
128         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True),
129         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
130         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
131         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
132         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
133         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
134             store = {
135                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
136                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
137             }),
138         'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
139             store = {
140                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
141                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
142             }),
143         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
144             store = {
145                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
146                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
147             }),
148         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
149             store = {
150                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
151                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
152                 'project.task.work': (_get_project_work, ['hours'], 10),
153             }),
154         'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
155         'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
156         'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
157         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
158      }
159     _order = "sequence"
160     _defaults = {
161         'active': True,
162         'priority': 1,
163         'sequence': 10,
164     }
165
166     # TODO: Why not using a SQL contraints ?
167     def _check_dates(self, cr, uid, ids, context=None):
168         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
169              if leave['date_start'] and leave['date']:
170                  if leave['date_start'] > leave['date']:
171                      return False
172         return True
173
174     _constraints = [
175         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
176     ]
177
178     def set_template(self, cr, uid, ids, context=None):
179         res = self.setActive(cr, uid, ids, value=False, context=context)
180         return res
181
182     def set_done(self, cr, uid, ids, context=None):
183         task_obj = self.pool.get('project.task')
184         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
185         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186         self.write(cr, uid, ids, {'state':'close'}, context=context)
187         for (id, name) in self.name_get(cr, uid, ids):
188             message = _("The project '%s' has been closed.") % name
189             self.log(cr, uid, id, message)
190         return True
191
192     def set_cancel(self, cr, uid, ids, context=None):
193         task_obj = self.pool.get('project.task')
194         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
195         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
196         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
197         return True
198
199     def set_pending(self, cr, uid, ids, context=None):
200         self.write(cr, uid, ids, {'state':'pending'}, context=context)
201         return True
202
203     def set_open(self, cr, uid, ids, context=None):
204         self.write(cr, uid, ids, {'state':'open'}, context=context)
205         return True
206
207     def reset_project(self, cr, uid, ids, context=None):
208         res = self.setActive(cr, uid, ids, value=True, context=context)
209         for (id, name) in self.name_get(cr, uid, ids):
210             message = _("The project '%s' has been opened.") % name
211             self.log(cr, uid, id, message)
212         return res
213
214     def copy(self, cr, uid, id, default={}, context=None):
215         if context is None:
216             context = {}
217
218         proj = self.browse(cr, uid, id, context=context)
219         default = default or {}
220         context['active_test'] = False
221         default['state'] = 'open'
222         if not default.get('name', False):
223             default['name'] = proj.name + _(' (copy)')
224         res = super(project, self).copy(cr, uid, id, default, context)
225
226         return res
227
228     def duplicate_template(self, cr, uid, ids, context=None):
229         if context is None:
230             context = {}
231         project_obj = self.pool.get('project.project')
232         data_obj = self.pool.get('ir.model.data')
233         result = []
234         for proj in self.browse(cr, uid, ids, context=context):
235             parent_id = context.get('parent_id', False)
236             context.update({'analytic_project_copy': True})
237             new_date_start = time.strftime('%Y-%m-%d')
238             new_date_end = False
239             if proj.date_start and proj.date:
240                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
241                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
242                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
243             new_id = project_obj.copy(cr, uid, proj.id, default = {
244                                     'name': proj.name +_(' (copy)'),
245                                     'state':'open',
246                                     'date_start':new_date_start,
247                                     'date':new_date_end,
248                                     'parent_id':parent_id}, context=context)
249             result.append(new_id)
250
251             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
252             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
253             if child_ids:
254                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
255
256         if result and len(result):
257             res_id = result[0]
258             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
259             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
260             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
261             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
262             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
263             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
264             return {
265                 'name': _('Projects'),
266                 'view_type': 'form',
267                 'view_mode': 'form,tree',
268                 'res_model': 'project.project',
269                 'view_id': False,
270                 'res_id': res_id,
271                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
272                 'type': 'ir.actions.act_window',
273                 'search_view_id': search_view['res_id'],
274                 'nodestroy': True
275             }
276
277     # set active value for a project, its sub projects and its tasks
278     def setActive(self, cr, uid, ids, value=True, context=None):
279         task_obj = self.pool.get('project.task')
280         for proj in self.browse(cr, uid, ids, context=None):
281             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
282             cr.execute('select id from project_task where project_id=%s', (proj.id,))
283             tasks_id = [x[0] for x in cr.fetchall()]
284             if tasks_id:
285                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
286             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
287             if child_ids:
288                 self.setActive(cr, uid, child_ids, value, context=None)
289         return True
290
291 project()
292
293 class users(osv.osv):
294     _inherit = 'res.users'
295     _columns = {
296         'context_project_id': fields.many2one('project.project', 'Project')
297     }
298 users()
299
300 class task(osv.osv):
301     _name = "project.task"
302     _description = "Task"
303     _log_create = True
304     _date_name = "date_start"
305
306     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
307         obj_project = self.pool.get('project.project')
308         for domain in args:
309             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
310                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
311                 if id and isinstance(id, (long, int)):
312                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
313                         args.append(('active', '=', False))
314         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
315
316     def _str_get(self, task, level=0, border='***', context=None):
317         return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
318             border[0]+' '+(task.name or '')+'\n'+ \
319             (task.description or '')+'\n\n'
320
321     # Compute: effective_hours, total_hours, progress
322     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
323         project_obj = self.pool.get('project.project')
324         res = {}
325         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
326         hours = dict(cr.fetchall())
327         for task in self.browse(cr, uid, ids, context=context):
328             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
329             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
330             res[task.id]['progress'] = 0.0
331             if (task.remaining_hours + hours.get(task.id, 0.0)):
332                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
333             if task.state in ('done','cancelled'):
334                 res[task.id]['progress'] = 100.0
335         return res
336
337
338     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
339         if remaining and not planned:
340             return {'value':{'planned_hours': remaining}}
341         return {}
342
343     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
344         return {'value':{'remaining_hours': planned - effective}}
345     
346     def onchange_project(self, cr, uid, id, project_id):
347         if not project_id:
348             return {}
349         data = self.pool.get('project.project').browse(cr, uid, [project_id])
350         partner_id=data and data[0].parent_id.partner_id
351         if partner_id:
352             return {'value':{'partner_id':partner_id.id}}
353         return {}
354     
355     def _default_project(self, cr, uid, context=None):
356         if context is None:
357             context = {}
358         if 'project_id' in context and context['project_id']:
359             return int(context['project_id'])
360         return False
361
362     def copy_data(self, cr, uid, id, default={}, context=None):
363         default = default or {}
364         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
365         if not default.get('remaining_hours', False):
366             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
367         default['active'] = True
368         default['type_id'] = False
369         if not default.get('name', False):
370             default['name'] = self.browse(cr, uid, id, context=context).name
371         return super(task, self).copy_data(cr, uid, id, default, context)
372
373     def _check_dates(self, cr, uid, ids, context=None):
374         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
375         if task['date_start'] and task['date_end']:
376              if task['date_start'] > task['date_end']:
377                  return False
378         return True
379
380     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
381         res = {}
382         for task in self.browse(cr, uid, ids, context=context):
383             res[task.id] = True
384             if task.project_id:
385                 if task.project_id.active == False or task.project_id.state == 'template':
386                     res[task.id] = False
387         return res
388
389     def _get_task(self, cr, uid, ids, context=None):
390         result = {}
391         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
392             if work.task_id: result[work.task_id.id] = True
393         return result.keys()
394
395     _columns = {
396         'active': fields.function(_is_template, method=True, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
397         'name': fields.char('Task Summary', size=128, required=True),
398         'description': fields.text('Description'),
399         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
400         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
401         'type_id': fields.many2one('project.task.type', 'Stage'),
402         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
403                                   help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
404                                   \n If the task is over, the states is set to \'Done\'.'),
405         'create_date': fields.datetime('Create Date', readonly=True),
406         'date_start': fields.datetime('Starting Date'),
407         'date_end': fields.datetime('Ending Date'),
408         'date_deadline': fields.date('Deadline'),
409         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
410         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
411         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
412         'notes': fields.text('Notes'),
413         'planned_hours': fields.float('Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
414         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
415             store = {
416                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
417                 'project.task.work': (_get_task, ['hours'], 10),
418             }),
419         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
420         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
421             store = {
422                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
423                 'project.task.work': (_get_task, ['hours'], 10),
424             }),
425         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
426             store = {
427                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
428                 'project.task.work': (_get_task, ['hours'], 10),
429             }),
430         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
431             store = {
432                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
433                 'project.task.work': (_get_task, ['hours'], 10),
434             }),
435         'user_id': fields.many2one('res.users', 'Assigned to'),
436         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
437         'partner_id': fields.many2one('res.partner', 'Partner'),
438         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
439         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
440         'company_id': fields.many2one('res.company', 'Company'),
441         'id': fields.integer('ID'),
442     }
443
444     _defaults = {
445         'state': 'draft',
446         'priority': '2',
447         'progress': 0,
448         'sequence': 10,
449         'active': True,
450         'project_id': _default_project,
451         'user_id': lambda obj, cr, uid, context: uid,
452         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
453     }
454
455     _order = "sequence, priority, date_start, id"
456
457     def _check_recursion(self, cr, uid, ids, context=None):
458         obj_task = self.browse(cr, uid, ids[0], context=context)
459         parent_ids = [x.id for x in obj_task.parent_ids]
460         children_ids = [x.id for x in obj_task.child_ids]
461
462         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
463             return False
464
465         while(ids):
466             cr.execute('SELECT DISTINCT task_id '\
467                        'FROM project_task_parent_rel '\
468                        'WHERE parent_id IN %s', (tuple(ids),))
469             child_ids = map(lambda x: x[0], cr.fetchall())
470             c_ids = child_ids
471             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
472                 return False
473             while len(c_ids):
474                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
475                 if (list(set(parent_ids).intersection(set(s_ids)))):
476                     return False
477                 c_ids = s_ids
478             ids = child_ids
479         return True
480
481     _constraints = [
482         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids'])
483     ]
484     #
485     # Override view according to the company definition
486     #
487
488
489     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
490         users_obj = self.pool.get('res.users')
491
492         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
493         # this should be safe (no context passed to avoid side-effects)
494         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
495         tm = obj_tm and obj_tm.name or 'Hours'
496
497         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
498
499         if tm in ['Hours','Hour']:
500             return res
501
502         eview = etree.fromstring(res['arch'])
503
504         def _check_rec(eview):
505             if eview.attrib.get('widget','') == 'float_time':
506                 eview.set('widget','float')
507             for child in eview:
508                 _check_rec(child)
509             return True
510
511         _check_rec(eview)
512
513         res['arch'] = etree.tostring(eview)
514
515         for f in res['fields']:
516             if 'Hours' in res['fields'][f]['string']:
517                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
518         return res
519
520     def action_close(self, cr, uid, ids, context=None):
521         # This action open wizard to send email to partner or project manager after close task.
522         project_id = len(ids) and ids[0] or False
523         if not project_id: return False
524         task = self.browse(cr, uid, project_id, context=context)
525         project = task.project_id
526         res = self.do_close(cr, uid, [project_id], context=context)
527         if project.warn_manager or project.warn_customer:
528            return {
529                 'name': _('Send Email after close task'),
530                 'view_type': 'form',
531                 'view_mode': 'form',
532                 'res_model': 'project.task.close',
533                 'type': 'ir.actions.act_window',
534                 'target': 'new',
535                 'nodestroy': True,
536                 'context': {'active_id': task.id}
537            }
538         return res
539
540     def do_close(self, cr, uid, ids, context=None):
541         """
542         Close Task
543         """
544         request = self.pool.get('res.request')
545         for task in self.browse(cr, uid, ids, context=context):
546             project = task.project_id
547             if project:
548                 # Send request to project manager
549                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
550                     request.create(cr, uid, {
551                         'name': _("Task '%s' closed") % task.name,
552                         'state': 'waiting',
553                         'act_from': uid,
554                         'act_to': project.user_id.id,
555                         'ref_partner_id': task.partner_id.id,
556                         'ref_doc1': 'project.task,%d'% (task.id,),
557                         'ref_doc2': 'project.project,%d'% (project.id,),
558                     })
559
560             for parent_id in task.parent_ids:
561                 if parent_id.state in ('pending','draft'):
562                     reopen = True
563                     for child in parent_id.child_ids:
564                         if child.id != task.id and child.state not in ('done','cancelled'):
565                             reopen = False
566                     if reopen:
567                         self.do_reopen(cr, uid, [parent_id.id])
568             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
569             message = _("The task '%s' is done") % (task.name,)
570             self.log(cr, uid, task.id, message)
571         return True
572
573     def do_reopen(self, cr, uid, ids, context=None):
574         request = self.pool.get('res.request')
575
576         for task in self.browse(cr, uid, ids, context=context):
577             project = task.project_id
578             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
579                 request.create(cr, uid, {
580                     'name': _("Task '%s' set in progress") % task.name,
581                     'state': 'waiting',
582                     'act_from': uid,
583                     'act_to': project.user_id.id,
584                     'ref_partner_id': task.partner_id.id,
585                     'ref_doc1': 'project.task,%d' % task.id,
586                     'ref_doc2': 'project.project,%d' % project.id,
587                 })
588
589             self.write(cr, uid, [task.id], {'state': 'open'})
590
591         return True
592
593     def do_cancel(self, cr, uid, ids, *args):
594         request = self.pool.get('res.request')
595         tasks = self.browse(cr, uid, ids)
596         for task in tasks:
597             project = task.project_id
598             if project.warn_manager and project.user_id and (project.user_id.id != uid):
599                 request.create(cr, uid, {
600                     'name': _("Task '%s' cancelled") % task.name,
601                     'state': 'waiting',
602                     'act_from': uid,
603                     'act_to': project.user_id.id,
604                     'ref_partner_id': task.partner_id.id,
605                     'ref_doc1': 'project.task,%d' % task.id,
606                     'ref_doc2': 'project.project,%d' % project.id,
607                 })
608             message = _("The task '%s' is cancelled.") % (task.name,)
609             self.log(cr, uid, task.id, message)
610             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
611         return True
612
613     def do_open(self, cr, uid, ids, *args):
614         tasks= self.browse(cr,uid,ids)
615         for t in tasks:
616             data = {'state': 'open'}
617             if not t.date_start:
618                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
619             self.write(cr, uid, [t.id], data)
620             message = _("The task '%s' is opened.") % (t.name,)
621             self.log(cr, uid, t.id, message)
622         return True
623
624     def do_draft(self, cr, uid, ids, *args):
625         self.write(cr, uid, ids, {'state': 'draft'})
626         return True
627
628     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
629         """
630         Delegate Task to another users.
631         """
632         task = self.browse(cr, uid, task_id, context=context)
633         new_task_id = self.copy(cr, uid, task.id, {
634             'name': delegate_data['name'],
635             'user_id': delegate_data['user_id'],
636             'planned_hours': delegate_data['planned_hours'],
637             'remaining_hours': delegate_data['planned_hours'],
638             'parent_ids': [(6, 0, [task.id])],
639             'state': 'draft',
640             'description': delegate_data['new_task_description'] or '',
641             'child_ids': [],
642             'work_ids': []
643         }, context=context)
644         newname = delegate_data['prefix'] or ''
645         self.write(cr, uid, [task.id], {
646             'remaining_hours': delegate_data['planned_hours_me'],
647             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
648             'name': newname,
649         }, context=context)
650         if delegate_data['state'] == 'pending':
651             self.do_pending(cr, uid, [task.id], context)
652         else:
653             self.do_close(cr, uid, [task.id], context=context)
654         user_pool = self.pool.get('res.users')
655         delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
656         message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
657         self.log(cr, uid, task.id, message)
658         return True
659
660     def do_pending(self, cr, uid, ids, *args):
661         self.write(cr, uid, ids, {'state': 'pending'})
662         for (id, name) in self.name_get(cr, uid, ids):
663             message = _("The task '%s' is pending.") % name
664             self.log(cr, uid, id, message)
665         return True
666
667     def next_type(self, cr, uid, ids, *args):
668         for task in self.browse(cr, uid, ids):
669             typeid = task.type_id.id
670             types = map(lambda x:x.id, task.project_id.type_ids or [])
671             if types:
672                 if not typeid:
673                     self.write(cr, uid, task.id, {'type_id': types[0]})
674                 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
675                     index = types.index(typeid)
676                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
677         return True
678
679     def prev_type(self, cr, uid, ids, *args):
680         for task in self.browse(cr, uid, ids):
681             typeid = task.type_id.id
682             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
683             if types:
684                 if typeid and typeid in types:
685                     index = types.index(typeid)
686                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
687         return True
688
689 task()
690
691 class project_work(osv.osv):
692     _name = "project.task.work"
693     _description = "Project Task Work"
694     _columns = {
695         'name': fields.char('Work summary', size=128),
696         'date': fields.datetime('Date'),
697         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
698         'hours': fields.float('Time Spent'),
699         'user_id': fields.many2one('res.users', 'Done by', required=True),
700         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
701     }
702
703     _defaults = {
704         'user_id': lambda obj, cr, uid, context: uid,
705         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
706     }
707
708     _order = "date desc"
709     def create(self, cr, uid, vals, *args, **kwargs):
710         if 'hours' in vals and (not vals['hours']):
711             vals['hours'] = 0.00
712         if 'task_id' in vals:
713             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
714         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
715
716     def write(self, cr, uid, ids, vals, context=None):
717         if 'hours' in vals and (not vals['hours']):
718             vals['hours'] = 0.00
719         if 'hours' in vals:
720             for work in self.browse(cr, uid, ids, context=context):
721                 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
722         return super(project_work,self).write(cr, uid, ids, vals, context)
723
724     def unlink(self, cr, uid, ids, *args, **kwargs):
725         for work in self.browse(cr, uid, ids):
726             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
727         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
728 project_work()
729
730 class account_analytic_account(osv.osv):
731
732     _inherit = 'account.analytic.account'
733     _description = 'Analytic Account'
734
735     def create(self, cr, uid, vals, context=None):
736         if context is None:
737             context = {}
738         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
739             vals['child_ids'] = []
740         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
741
742 account_analytic_account()
743
744 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: