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