[MERGE] Fix wrong helptext for field active
[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         if context is None:
108             context = {}
109         result = {}
110         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
111             if task.project_id: result[task.project_id.id] = True
112         return result.keys()
113
114     def _get_project_work(self, cr, uid, ids, context=None):
115         if context is None:
116             context = {}
117         result = {}
118         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
119             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
120         return result.keys()
121
122     def unlink(self, cr, uid, ids, *args, **kwargs):
123         for proj in self.browse(cr, uid, ids):
124             if proj.tasks:
125                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
126         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
127
128     _columns = {
129         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
130         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
131         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
132         '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),
133         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
134         '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)]}),
135         '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)]}),
136         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
137         '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.",
138             store = {
139                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
140                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
141             }),
142         '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.",
143             store = {
144                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
145                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
146             }),
147         '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.",
148             store = {
149                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
150                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
151             }),
152         '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.",
153             store = {
154                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
155                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
156                 'project.task.work': (_get_project_work, ['hours'], 10),
157             }),
158         '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)]}),
159         '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)]}),
160         '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)]}),
161         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
162      }
163     _order = "sequence"
164     _defaults = {
165         'active': True,
166         'priority': 1,
167         'sequence': 10,
168     }
169
170     # TODO: Why not using a SQL contraints ?
171     def _check_dates(self, cr, uid, ids):
172          for leave in self.read(cr, uid, ids, ['date_start', 'date']):
173              if leave['date_start'] and leave['date']:
174                  if leave['date_start'] > leave['date']:
175                      return False
176          return True
177
178     _constraints = [
179         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
180     ]
181
182     def set_template(self, cr, uid, ids, context=None):
183         res = self.setActive(cr, uid, ids, value=False, context=context)
184         return res
185
186     def set_done(self, cr, uid, ids, context=None):
187         task_obj = self.pool.get('project.task')
188         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
189         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190         self.write(cr, uid, ids, {'state':'close'}, context=context)
191         for (id, name) in self.name_get(cr, uid, ids):
192             message = _("The project '%s' has been closed.") % name
193             self.log(cr, uid, id, message)
194         return True
195
196     def set_cancel(self, cr, uid, ids, context=None):
197         task_obj = self.pool.get('project.task')
198         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
199         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
200         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
201         return True
202
203     def set_pending(self, cr, uid, ids, context=None):
204         self.write(cr, uid, ids, {'state':'pending'}, context=context)
205         return True
206
207     def set_open(self, cr, uid, ids, context=None):
208         self.write(cr, uid, ids, {'state':'open'}, context=context)
209         return True
210
211     def reset_project(self, cr, uid, ids, context=None):
212         res = self.setActive(cr, uid, ids, value=True, context=context)
213         for (id, name) in self.name_get(cr, uid, ids):
214             message = _("The project '%s' has been opened.") % name
215             self.log(cr, uid, id, message)
216         return res
217
218     def copy(self, cr, uid, id, default={}, context=None):
219         if context is None:
220             context = {}
221
222         proj = self.browse(cr, uid, id, context=context)
223         default = default or {}
224         context['active_test'] = False
225         default['state'] = 'open'
226         if not default.get('name', False):
227             default['name'] = proj.name + _(' (copy)')
228         res = super(project, self).copy(cr, uid, id, default, context)
229
230         return res
231
232     def duplicate_template(self, cr, uid, ids, context=None):
233         if context is None:
234             context = {}
235         project_obj = self.pool.get('project.project')
236         data_obj = self.pool.get('ir.model.data')
237         result = []
238         for proj in self.browse(cr, uid, ids, context=context):
239             parent_id = context.get('parent_id', False)
240             context.update({'analytic_project_copy': True})
241             new_date_start = time.strftime('%Y-%m-%d')
242             new_date_end = False
243             if proj.date_start and proj.date:
244                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
245                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
246                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
247             new_id = project_obj.copy(cr, uid, proj.id, default = {
248                                     'name': proj.name +_(' (copy)'),
249                                     'state':'open',
250                                     'date_start':new_date_start,
251                                     'date':new_date_end,
252                                     'parent_id':parent_id}, context=context)
253             result.append(new_id)
254
255             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
256             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
257             if child_ids:
258                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
259
260         if result and len(result):
261             res_id = result[0]
262             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
263             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
264             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
265             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
266             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
267             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
268             return {
269                 'name': _('Projects'),
270                 'view_type': 'form',
271                 'view_mode': 'form,tree',
272                 'res_model': 'project.project',
273                 'view_id': False,
274                 'res_id': res_id,
275                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
276                 'type': 'ir.actions.act_window',
277                 'search_view_id': search_view['res_id'],
278                 'nodestroy': True
279             }
280
281     # set active value for a project, its sub projects and its tasks
282     def setActive(self, cr, uid, ids, value=True, context=None):
283         task_obj = self.pool.get('project.task')
284         for proj in self.browse(cr, uid, ids, context=None):
285             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
286             cr.execute('select id from project_task where project_id=%s', (proj.id,))
287             tasks_id = [x[0] for x in cr.fetchall()]
288             if tasks_id:
289                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
290             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
291             if child_ids:
292                 self.setActive(cr, uid, child_ids, value, context=None)
293         return True
294
295 project()
296
297 class users(osv.osv):
298     _inherit = 'res.users'
299     _columns = {
300         'context_project_id': fields.many2one('project.project', 'Project')
301     }
302 users()
303
304 class task(osv.osv):
305     _name = "project.task"
306     _description = "Task"
307     _log_create = True
308     _date_name = "date_start"
309
310     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
311         obj_project = self.pool.get('project.project')
312         for domain in args:
313             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
314                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
315                 if id and isinstance(id, (long, int)):
316                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
317                         args.append(('active', '=', False))
318         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
319
320     def _str_get(self, task, level=0, border='***', context=None):
321         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'+ \
322             border[0]+' '+(task.name or '')+'\n'+ \
323             (task.description or '')+'\n\n'
324
325     # Compute: effective_hours, total_hours, progress
326     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
327         project_obj = self.pool.get('project.project')
328         res = {}
329         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
330         hours = dict(cr.fetchall())
331         for task in self.browse(cr, uid, ids, context=context):
332             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)}
333             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
334             res[task.id]['progress'] = 0.0
335             if (task.remaining_hours + hours.get(task.id, 0.0)):
336                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
337             if task.state in ('done','cancelled'):
338                 res[task.id]['progress'] = 100.0
339         return res
340
341
342     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
343         if remaining and not planned:
344             return {'value':{'planned_hours': remaining}}
345         return {}
346
347     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
348         return {'value':{'remaining_hours': planned - effective}}
349
350     def _default_project(self, cr, uid, context=None):
351         if context is None:
352             context = {}
353         if 'project_id' in context and context['project_id']:
354             return int(context['project_id'])
355         return False
356
357     def copy_data(self, cr, uid, id, default={}, context=None):
358         default = default or {}
359         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
360         if not default.get('remaining_hours', False):
361             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
362         default['active'] = True
363         default['type_id'] = False
364         if not default.get('name', False):
365             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
366         return super(task, self).copy_data(cr, uid, id, default, context)
367
368     def _check_dates(self, cr, uid, ids, context=None):
369         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
370         if task['date_start'] and task['date_end']:
371              if task['date_start'] > task['date_end']:
372                  return False
373         return True
374
375     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
376         res = {}
377         for task in self.browse(cr, uid, ids, context=context):
378             res[task.id] = True
379             if task.project_id:
380                 if task.project_id.active == False or task.project_id.state == 'template':
381                     res[task.id] = False
382         return res
383
384     def _get_task(self, cr, uid, ids, context=None):
385         if context is None:
386             context = {}
387         result = {}
388         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
389             if work.task_id: result[work.task_id.id] = True
390         return result.keys()
391
392     _columns = {
393         '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."),
394         'name': fields.char('Task Summary', size=128, required=True),
395         'description': fields.text('Description'),
396         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
397         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
398         'type_id': fields.many2one('project.task.type', 'Stage'),
399         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
400                                   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.\
401                                   \n If the task is over, the states is set to \'Done\'.'),
402         'create_date': fields.datetime('Create Date', readonly=True),
403         'date_start': fields.datetime('Starting Date'),
404         'date_end': fields.datetime('Ending Date'),
405         'date_deadline': fields.date('Deadline'),
406         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
407         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
408         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
409         'notes': fields.text('Notes'),
410         '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.'),
411         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
412             store = {
413                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
414                 'project.task.work': (_get_task, ['hours'], 10),
415             }),
416         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
417         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
418             store = {
419                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
420                 'project.task.work': (_get_task, ['hours'], 10),
421             }),
422         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
423             store = {
424                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
425                 'project.task.work': (_get_task, ['hours'], 10),
426             }),
427         '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.",
428             store = {
429                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
430                 'project.task.work': (_get_task, ['hours'], 10),
431             }),
432         'user_id': fields.many2one('res.users', 'Assigned to'),
433         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
434         'partner_id': fields.many2one('res.partner', 'Partner'),
435         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
436         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
437         'company_id': fields.many2one('res.company', 'Company'),
438         'id': fields.integer('ID'),
439     }
440
441     _defaults = {
442         'state': 'draft',
443         'priority': '2',
444         'progress': 0,
445         'sequence': 10,
446         'active': True,
447         'project_id': _default_project,
448         'user_id': lambda obj, cr, uid, context: uid,
449         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
450     }
451
452     _order = "sequence, priority, date_start, id"
453
454     def _check_recursion(self, cr, uid, ids):
455         obj_task = self.browse(cr, uid, ids[0])
456         parent_ids = [x.id for x in obj_task.parent_ids]
457         children_ids = [x.id for x in obj_task.child_ids]
458
459         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
460             return False
461
462         while(ids):
463             cr.execute('SELECT DISTINCT task_id '\
464                        'FROM project_task_parent_rel '\
465                        'WHERE parent_id IN %s', (tuple(ids),))
466             child_ids = map(lambda x: x[0], cr.fetchall())
467             c_ids = child_ids
468             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
469                 return False
470             while len(c_ids):
471                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
472                 if (list(set(parent_ids).intersection(set(s_ids)))):
473                     return False
474                 c_ids = s_ids
475             ids = child_ids
476         return True
477
478     _constraints = [
479         (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
480     ]
481     #
482     # Override view according to the company definition
483     #
484
485     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
486         users_obj = self.pool.get('res.users')
487
488         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
489         # this should be safe (no context passed to avoid side-effects)
490         obj_tm = users_obj.browse(cr, 1, uid).company_id.project_time_mode_id
491         tm = obj_tm and obj_tm.name or 'Hours'
492
493         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
494
495         if tm in ['Hours','Hour']:
496             return res
497
498         eview = etree.fromstring(res['arch'])
499
500         def _check_rec(eview):
501             if eview.attrib.get('widget','') == 'float_time':
502                 eview.set('widget','float')
503             for child in eview:
504                 _check_rec(child)
505             return True
506
507         _check_rec(eview)
508
509         res['arch'] = etree.tostring(eview)
510
511         for f in res['fields']:
512             if 'Hours' in res['fields'][f]['string']:
513                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
514         return res
515
516     def action_close(self, cr, uid, ids, context=None):
517         # This action open wizard to send email to partner or project manager after close task.
518         project_id = len(ids) and ids[0] or False
519         if not project_id: return False
520         task = self.browse(cr, uid, project_id, context=context)
521         project = task.project_id
522         res = self.do_close(cr, uid, [project_id], context=context)
523         if project.warn_manager or project.warn_customer:
524            return {
525                 'name': _('Send Email after close task'),
526                 'view_type': 'form',
527                 'view_mode': 'form',
528                 'res_model': 'project.task.close',
529                 'type': 'ir.actions.act_window',
530                 'target': 'new',
531                 'nodestroy': True,
532                 'context': {'active_id': task.id}
533            }
534         return res
535
536     def do_close(self, cr, uid, ids, context=None):
537         """
538         Close Task
539         """
540         if context is None:
541             context = {}
542         request = self.pool.get('res.request')
543         for task in self.browse(cr, uid, ids, context=context):
544             project = task.project_id
545             if project:
546                 # Send request to project manager
547                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
548                     request.create(cr, uid, {
549                         'name': _("Task '%s' closed") % task.name,
550                         'state': 'waiting',
551                         'act_from': uid,
552                         'act_to': project.user_id.id,
553                         'ref_partner_id': task.partner_id.id,
554                         'ref_doc1': 'project.task,%d'% (task.id,),
555                         'ref_doc2': 'project.project,%d'% (project.id,),
556                     })
557
558             for parent_id in task.parent_ids:
559                 if parent_id.state in ('pending','draft'):
560                     reopen = True
561                     for child in parent_id.child_ids:
562                         if child.id != task.id and child.state not in ('done','cancelled'):
563                             reopen = False
564                     if reopen:
565                         self.do_reopen(cr, uid, [parent_id.id])
566             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
567             message = _("The task '%s' is done") % (task.name,)
568             self.log(cr, uid, task.id, message)
569         return True
570
571     def do_reopen(self, cr, uid, ids, context=None):
572         if context is None:
573             context = {}
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         if context is None:
633             context = {}
634         task = self.browse(cr, uid, task_id, context=context)
635         new_task_id = self.copy(cr, uid, task.id, {
636             'name': delegate_data['name'],
637             'user_id': delegate_data['user_id'],
638             'planned_hours': delegate_data['planned_hours'],
639             'remaining_hours': delegate_data['planned_hours'],
640             'parent_ids': [(6, 0, [task.id])],
641             'state': 'draft',
642             'description': delegate_data['new_task_description'] or '',
643             'child_ids': [],
644             'work_ids': []
645         }, context)
646         newname = delegate_data['prefix'] or ''
647         self.write(cr, uid, [task.id], {
648             'remaining_hours': delegate_data['planned_hours_me'],
649             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
650             'name': newname,
651         }, context)
652         if delegate_data['state'] == 'pending':
653             self.do_pending(cr, uid, [task.id], context)
654         else:
655             self.do_close(cr, uid, [task.id], context)
656         user_pool = self.pool.get('res.users')
657         delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
658         message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
659         self.log(cr, uid, task.id, message)
660         return True
661
662     def do_pending(self, cr, uid, ids, *args):
663         self.write(cr, uid, ids, {'state': 'pending'})
664         for (id, name) in self.name_get(cr, uid, ids):
665             message = _("The task '%s' is pending.") % name
666             self.log(cr, uid, id, message)
667         return True
668
669     def next_type(self, cr, uid, ids, *args):
670         for task in self.browse(cr, uid, ids):
671             typeid = task.type_id.id
672             types = map(lambda x:x.id, task.project_id.type_ids or [])
673             if types:
674                 if not typeid:
675                     self.write(cr, uid, task.id, {'type_id': types[0]})
676                 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
677                     index = types.index(typeid)
678                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
679         return True
680
681     def prev_type(self, cr, uid, ids, *args):
682         for task in self.browse(cr, uid, ids):
683             typeid = task.type_id.id
684             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
685             if types:
686                 if typeid and typeid in types:
687                     index = types.index(typeid)
688                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
689         return True
690
691 task()
692
693 class project_work(osv.osv):
694     _name = "project.task.work"
695     _description = "Project Task Work"
696     _columns = {
697         'name': fields.char('Work summary', size=128),
698         'date': fields.datetime('Date'),
699         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
700         'hours': fields.float('Time Spent'),
701         'user_id': fields.many2one('res.users', 'Done by', required=True),
702         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
703     }
704
705     _defaults = {
706         'user_id': lambda obj, cr, uid, context: uid,
707         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
708     }
709
710     _order = "date desc"
711     def create(self, cr, uid, vals, *args, **kwargs):
712         if 'hours' in vals and (not vals['hours']):
713             vals['hours'] = 0.00
714         if 'task_id' in vals:
715             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
716         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
717
718     def write(self, cr, uid, ids,vals,context={}):
719         if 'hours' in vals and (not vals['hours']):
720             vals['hours'] = 0.00
721         if 'hours' in vals:
722             for work in self.browse(cr, uid, ids, context):
723                 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))
724         return super(project_work,self).write(cr, uid, ids, vals, context)
725
726     def unlink(self, cr, uid, ids, *args, **kwargs):
727         for work in self.browse(cr, uid, ids):
728             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
729         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
730 project_work()
731
732 class account_analytic_account(osv.osv):
733
734     _inherit = 'account.analytic.account'
735     _description = 'Analytic Account'
736
737     def create(self, cr, uid, vals, context=None):
738         if context is None:
739             context = {}
740         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
741             vals['child_ids'] = []
742         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
743
744 account_analytic_account()
745
746 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: