a1f02124c9dd9a3bf188ad221e5b95c2529a5f3b
[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 _get_user_and_default_uom_ids(self, cr, uid):
82         users_obj = self.pool.get('res.users')
83         model_data_obj = self.pool.get('ir.model.data')
84         model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
85         default_uom = user_uom = model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
86         obj_tm = users_obj.browse(cr, uid, uid).company_id.project_time_mode_id
87         if obj_tm:
88             user_uom = obj_tm.id
89         return user_uom, default_uom
90
91     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
92         def _get_all_child_projects(ids):
93             """Recursively get child project ids"""
94             child_ids = flatten([project_hierarchy.get(idn, []) for idn in ids])
95             if child_ids:
96                 child_ids = _get_all_child_projects(child_ids)
97             return ids + child_ids
98         # END _get_all_child_projects
99
100         res = {}.fromkeys(ids, 0.0)
101         progress = {}
102         if not ids:
103             return res
104
105         par_child_projects = {}
106         all_projects = list(ids)
107
108         # get project hierarchy:
109         cr.execute('''SELECT prp.id AS pr_parent_id, prpc.id AS pr_child_id
110             FROM account_analytic_account AS p
111             JOIN account_analytic_account AS c ON p.id = c.parent_id
112             JOIN project_project AS prp ON prp.analytic_account_id = p.id
113             JOIN project_project AS prpc ON prpc.analytic_account_id = c.id''')
114
115         project_hierarchy = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(cr.fetchall(), itemgetter(0)))
116
117         for id in ids:
118             child_projects = _get_all_child_projects([id])
119             par_child_projects[id] = child_projects
120             all_projects.extend(child_projects)
121
122         all_projects = dict.fromkeys(all_projects).keys()
123         cr.execute('''SELECT
124                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
125             FROM
126                 project_task
127             WHERE
128                 project_id IN %s AND
129                 state<>'cancelled'
130             GROUP BY
131                 project_id''',(tuple(all_projects),))
132         progress = dict(map(lambda x: (x[0], (x[1], x[2], x[3])), cr.fetchall()))
133
134         user_uom, def_uom = self._get_user_and_default_uom_ids(cr, uid)
135         for project in self.browse(cr, uid, par_child_projects.keys(), context=context):
136             s = [0.0, 0.0, 0.0]
137             tocompute = par_child_projects[project.id]
138             while tocompute:
139                 p = tocompute.pop()
140                 for i in range(3):
141                     s[i] += progress.get(p, (0.0, 0.0, 0.0))[i]
142
143             uom_obj = self.pool.get('product.uom')
144             if user_uom != def_uom:
145                 s[0] = uom_obj._compute_qty(cr, uid, user_uom, s[0], def_uom)
146                 s[1] = uom_obj._compute_qty(cr, uid, user_uom, s[1], def_uom)
147                 s[2] = uom_obj._compute_qty(cr, uid, user_uom, s[2], def_uom)
148
149             if project.state == 'close':
150                 progress_rate = 100.0
151             else:
152                 progress_rate = s[1] and round(min(100.0 * s[2] / s[1], 99.99), 2)
153
154             res[project.id] = {
155                 'planned_hours': s[0],
156                 'effective_hours': s[2],
157                 'total_hours': s[1],
158                 'progress_rate': progress_rate
159             }
160         return res
161
162     def unlink(self, cr, uid, ids, *args, **kwargs):
163         for proj in self.browse(cr, uid, ids):
164             if proj.tasks:
165                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
166         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
167
168     _columns = {
169         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
170         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
171         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
172         '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),
173         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
174         '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)]}),
175         '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)]}),
176         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
177         '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."),
178         '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."),
179         '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."),
180         '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."),
181         '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)]}),
182         '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)]}),
183         '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)]}),
184         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
185      }
186
187     _order = "sequence"
188
189     _defaults = {
190         'active': True,
191         'priority': 1,
192         'sequence': 10,
193         'warn_manager': True,
194     }
195
196     def _check_dates(self, cr, uid, ids):
197          leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
198          if leave['date_start'] and leave['date']:
199              if leave['date_start'] > leave['date']:
200                  return False
201          return True
202
203     _constraints = [
204         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
205     ]
206
207     def set_template(self, cr, uid, ids, context=None):
208         res = self.setActive(cr, uid, ids, value=False, context=context)
209         return res
210
211     def set_done(self, cr, uid, ids, context=None):
212         task_obj = self.pool.get('project.task')
213         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
214         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
215         self.write(cr, uid, ids, {'state':'close'}, context=context)
216         for (id, name) in self.name_get(cr, uid, ids):
217             message = _('Project ') + " '" + name + "' "+ _("is Closed.")
218             self.log(cr, uid, id, message)
219         return True
220
221     def set_cancel(self, cr, uid, ids, context=None):
222         task_obj = self.pool.get('project.task')
223         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
224         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
225         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
226         return True
227
228     def set_pending(self, cr, uid, ids, context=None):
229         self.write(cr, uid, ids, {'state':'pending'}, context=context)
230         return True
231
232     def set_open(self, cr, uid, ids, context=None):
233         self.write(cr, uid, ids, {'state':'open'}, context=context)
234         return True
235
236     def reset_project(self, cr, uid, ids, context=None):
237         res = self.setActive(cr, uid, ids, value=True, context=context)
238         for (id, name) in self.name_get(cr, uid, ids):
239             message = _('Project ') + " '" + name + "' "+ _("is Open.")
240             self.log(cr, uid, id, message)
241         return res
242
243     def copy(self, cr, uid, id, default={}, context=None):
244         if context is None:
245             context = {}
246
247         proj = self.browse(cr, uid, id, context=context)
248         default = default or {}
249         context['active_test'] = False
250         default['state'] = 'open'
251         if not default.get('name', False):
252             default['name'] = proj.name + _(' (copy)')
253         res = super(project, self).copy(cr, uid, id, default, context)
254
255         return res
256
257     def duplicate_template(self, cr, uid, ids, context=None):
258         if context is None:
259             context = {}
260         project_obj = self.pool.get('project.project')
261         data_obj = self.pool.get('ir.model.data')
262         result = []
263         for proj in self.browse(cr, uid, ids, context=context):
264             parent_id = context.get('parent_id', False)
265             context.update({'analytic_project_copy': True})
266             new_date_start = time.strftime('%Y-%m-%d')
267             new_date_end = False
268             if proj.date_start and proj.date:
269                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
270                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
271                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
272             new_id = project_obj.copy(cr, uid, proj.id, default = {
273                                     'name': proj.name +_(' (copy)'),
274                                     'state':'open',
275                                     'date_start':new_date_start,
276                                     'date':new_date_end,
277                                     'parent_id':parent_id}, context=context)
278             result.append(new_id)
279
280             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
281             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
282             if child_ids:
283                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
284
285         if result and len(result):
286             res_id = result[0]
287             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
288             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
289             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
290             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
291             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
292             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
293             return {
294                 'name': _('Projects'),
295                 'view_type': 'form',
296                 'view_mode': 'form,tree',
297                 'res_model': 'project.project',
298                 'view_id': False,
299                 'res_id': res_id,
300                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
301                 'type': 'ir.actions.act_window',
302                 'search_view_id': search_view['res_id'],
303                 'nodestroy': True
304             }
305
306     # set active value for a project, its sub projects and its tasks
307     def setActive(self, cr, uid, ids, value=True, context=None):
308         task_obj = self.pool.get('project.task')
309         for proj in self.browse(cr, uid, ids, context=None):
310             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
311             cr.execute('select id from project_task where project_id=%s', (proj.id,))
312             tasks_id = [x[0] for x in cr.fetchall()]
313             if tasks_id:
314                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
315             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
316             if child_ids:
317                 self.setActive(cr, uid, child_ids, value, context=None)
318         return True
319     
320 project()
321
322 class users(osv.osv):
323     _inherit = 'res.users'
324     _columns = {
325         'context_project_id': fields.many2one('project.project', 'Project')
326     }
327 users()
328
329 class task(osv.osv):
330     _name = "project.task"
331     _description = "Task"
332     _log_create = True
333     _date_name = "date_start"
334
335     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
336         obj_project = self.pool.get('project.project')
337         for domain in args:
338             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
339                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
340                 if id and isinstance(id, (long, int)):
341                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
342                         args.append(('active', '=', False))
343         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
344
345     def _str_get(self, task, level=0, border='***', context=None):
346         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'+ \
347             border[0]+' '+(task.name or '')+'\n'+ \
348             (task.description or '')+'\n\n'
349
350     # Compute: effective_hours, total_hours, progress
351     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
352         project_obj = self.pool.get('project.project')
353         res = {}
354         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
355         hours = dict(cr.fetchall())
356
357         uom_obj = self.pool.get('product.uom')
358         user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
359         if user_uom != default_uom:
360             for task in self.browse(cr, uid, ids, context=context):
361                 if hours.get(task.id, False):
362                     dur_in_user_uom =  uom_obj._compute_qty(cr, uid, default_uom, hours.get(task.id, 0.0), user_uom)
363                     hours[task.id] = dur_in_user_uom
364
365         for task in self.browse(cr, uid, ids, context=context):
366             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
367             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
368             res[task.id]['progress'] = 0.0
369             if (task.remaining_hours + hours.get(task.id, 0.0)):
370                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
371             if task.state in ('done','cancelled'):
372                 res[task.id]['progress'] = 100.0
373         return res
374
375
376     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
377         return {'value':{'remaining_hours': planned - effective}}
378
379     def _default_project(self, cr, uid, context=None):
380         if context is None:
381             context = {}
382         if 'project_id' in context and context['project_id']:
383             return int(context['project_id'])
384         return False
385
386     def copy_data(self, cr, uid, id, default={}, context=None):
387         default = default or {}
388         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
389         if not default.get('remaining_hours', False):
390             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
391         default['active'] = True
392         default['type_id'] = False
393         if not default.get('name', False):
394             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
395         return super(task, self).copy_data(cr, uid, id, default, context)
396
397     def _check_dates(self, cr, uid, ids, context=None):
398         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
399         if task['date_start'] and task['date_end']:
400              if task['date_start'] > task['date_end']:
401                  return False
402         return True
403
404     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
405         res = {}
406         for task in self.browse(cr, uid, ids, context=context):
407             res[task.id] = True
408             if task.project_id:
409                 if task.project_id.active == False or task.project_id.state == 'template':
410                     res[task.id] = False
411         return res
412
413     _columns = {
414         '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."),
415         'name': fields.char('Task Summary', size=128, required=True),
416         'description': fields.text('Description'),
417         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
418         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
419         'type_id': fields.many2one('project.task.type', 'Stage',),
420         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
421                                   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.\
422                                   \n If the task is over, the states is set to \'Done\'.'),
423         'create_date': fields.datetime('Create Date', readonly=True),
424         'date_start': fields.datetime('Starting Date'),
425         'date_end': fields.datetime('Ending Date'),
426         'date_deadline': fields.date('Deadline'),
427         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
428         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
429         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
430         'notes': fields.text('Notes'),
431         '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.'),
432         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
433         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
434         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
435         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", store=True, help="Computed as: Time Spent / Total Time."),
436         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as difference of the time estimated by the project manager and the real time to close the task."),
437
438         'user_id': fields.many2one('res.users', 'Assigned to'),
439         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
440         'partner_id': fields.many2one('res.partner', 'Partner'),
441         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
442         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
443         'company_id': fields.many2one('res.company', 'Company'),
444     }
445
446     _defaults = {
447         'state': 'draft',
448         'priority': '2',
449         'progress': 0,
450         'sequence': 10,
451         'active': True,
452         'project_id': _default_project,
453         'user_id': lambda obj, cr, uid, context: uid,
454         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
455     }
456
457     _order = "sequence, priority, date_start, id"
458
459     def _check_recursion(self, cr, uid, ids):
460         obj_task = self.browse(cr, uid, ids[0])
461         parent_ids = [x.id for x in obj_task.parent_ids]
462         children_ids = [x.id for x in obj_task.child_ids]
463
464         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
465             return False
466
467         while(ids):
468             cr.execute('SELECT DISTINCT task_id '\
469                        'FROM project_task_parent_rel '\
470                        'WHERE parent_id IN %s', (tuple(ids),))
471             child_ids = map(lambda x: x[0], cr.fetchall())
472             c_ids = child_ids
473             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
474                 return False
475             while len(c_ids):
476                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
477                 if (list(set(parent_ids).intersection(set(s_ids)))):
478                     return False
479                 c_ids = s_ids
480             ids = child_ids
481         return True
482
483     _constraints = [
484         (_check_dates, 'Error! Task start-date must be lower then task end-date.', ['date_start', 'date_end']),
485         (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
486     ]
487     #
488     # Override view according to the company definition
489     #
490
491     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
492         users_obj = self.pool.get('res.users')
493         obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
494         tm = obj_tm and obj_tm.name or 'Hours'
495
496         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
497
498         if tm in ['Hours','Hour']:
499             return res
500
501         eview = etree.fromstring(res['arch'])
502
503         def _check_rec(eview):
504             if eview.attrib.get('widget','') == 'float_time':
505                 eview.set('widget','float')
506             for child in eview:
507                 _check_rec(child)
508             return True
509
510         _check_rec(eview)
511
512         res['arch'] = etree.tostring(eview)
513
514         for f in res['fields']:
515             if 'Hours' in res['fields'][f]['string']:
516                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
517         return res
518
519     def action_close(self, cr, uid, ids, context=None):
520         # This action open wizard to send email to partner or project manager after close task.
521         project_id = len(ids) and ids[0] or False
522         if not project_id: return False
523         task = self.browse(cr, uid, project_id, context=context)
524         project = task.project_id
525         res = self.do_close(cr, uid, [project_id], context=context) 
526         if project.warn_manager or project.warn_customer:
527            return {
528                 'name': _('Send Email after close task'),
529                 'view_type': 'form',
530                 'view_mode': 'form',
531                 'res_model': 'project.task.close',
532                 'type': 'ir.actions.act_window',
533                 'target': 'new',
534                 'nodestroy': True,
535                 'context': {'active_id': task.id}
536            } 
537         return res
538
539     def do_close(self, cr, uid, ids, context=None):
540         """
541         Close Task
542         """
543         if context is None:
544             context = {}
545         request = self.pool.get('res.request')
546         for task in self.browse(cr, uid, ids, context=context):
547             project = task.project_id
548             if project:
549                 # Send request to project manager
550                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
551                     request.create(cr, uid, {
552                         'name': _("Task '%s' closed") % task.name,
553                         'state': 'waiting',
554                         'act_from': uid,
555                         'act_to': project.user_id.id,
556                         'ref_partner_id': task.partner_id.id,
557                         'ref_doc1': 'project.task,%d'% (task.id,),
558                         'ref_doc2': 'project.project,%d'% (project.id,),
559                     })
560
561             for parent_id in task.parent_ids:
562                 if parent_id.state in ('pending','draft'):
563                     reopen = True
564                     for child in parent_id.child_ids:
565                         if child.id != task.id and child.state not in ('done','cancelled'):
566                             reopen = False
567                     if reopen:
568                         self.do_reopen(cr, uid, [parent_id.id])
569             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
570             message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
571             self.log(cr, uid, task.id, message)
572         return True
573
574     def do_reopen(self, cr, uid, ids, context=None):
575         if context is None:
576             context = {}
577         request = self.pool.get('res.request')
578         for task in self.browse(cr, uid, ids, context=context):
579             project = task.project_id
580             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
581                 request.create(cr, uid, {
582                     'name': _("Task '%s' set in progress") % task.name,
583                     'state': 'waiting',
584                     'act_from': uid,
585                     'act_to': project.user_id.id,
586                     'ref_partner_id': task.partner_id.id,
587                     'ref_doc1': 'project.task,%d' % task.id,
588                     'ref_doc2': 'project.project,%d' % project.id,
589                 })
590
591             self.write(cr, uid, [task.id], {'state': 'open'})
592         return True
593
594     def do_cancel(self, cr, uid, ids, *args):
595         request = self.pool.get('res.request')
596         tasks = self.browse(cr, uid, ids)
597         for task in tasks:
598             project = task.project_id
599             if project.warn_manager and project.user_id and (project.user_id.id != uid):
600                 request.create(cr, uid, {
601                     'name': _("Task '%s' cancelled") % task.name,
602                     'state': 'waiting',
603                     'act_from': uid,
604                     'act_to': project.user_id.id,
605                     'ref_partner_id': task.partner_id.id,
606                     'ref_doc1': 'project.task,%d' % task.id,
607                     'ref_doc2': 'project.project,%d' % project.id,
608                 })
609             message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
610             self.log(cr, uid, task.id, message)
611             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
612         return True
613
614     def do_open(self, cr, uid, ids, *args):
615         tasks= self.browse(cr,uid,ids)
616         for t in tasks:
617             self.write(cr, uid, [t.id], {'state': 'open',  'date_start': time.strftime('%Y-%m-%d %H:%M:%S'),})
618             message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
619             self.log(cr, uid, t.id, message)
620         return True
621
622     def do_draft(self, cr, uid, ids, *args):
623         self.write(cr, uid, ids, {'state': 'draft'})
624         return True
625
626     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
627         """
628         Delegate Task to another users.
629         """
630         if context is None:
631             context = {}
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)
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)
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)
654         user_pool = self.pool.get('res.users')
655         delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
656         message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_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 = _('Task ') + " '" + name + "' "+ _("is Pending.")
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.type_ids)
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': time.strftime('%Y-%m-%d %H:%M:%S')
706     }
707
708     _order = "date desc"
709
710     def create(self, cr, uid, vals, *args, **kwargs):
711         project_obj = self.pool.get('project.project')
712         uom_obj = self.pool.get('product.uom')
713         if vals.get('hours', False):
714             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
715             duration = vals['hours']
716             if user_uom != default_uom:
717                 duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
718             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
719         return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
720
721     def write(self, cr, uid, ids, vals, context=None):
722         project_obj = self.pool.get('project.project')
723         uom_obj = self.pool.get('product.uom')
724         if vals.get('hours', False):
725             old_hours = self.browse(cr, uid, ids, context=context)
726             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
727             duration = vals['hours']
728             for old in old_hours:
729                 if vals.get('hours') != old.hours:
730                     # this code is only needed when we update the hours of the project
731                     # TODO: it may still a second calculation if the task.id is changed
732                     # at this task.
733                     if user_uom == default_uom:
734                         for work in self.browse(cr, uid, ids, context=context):
735                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
736                     else:
737                         for work in self.browse(cr, uid, ids, context=context):
738                             duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
739                             del_work =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
740                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
741         return super(project_work,self).write(cr, uid, ids, vals, context=context)
742
743     def unlink(self, cr, uid, ids, *args, **kwargs):
744         context = kwargs.get('context', {})
745         project_obj = self.pool.get('project.project')
746         uom_obj = self.pool.get('product.uom')
747         user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
748         if user_uom == default_uom:
749             for work in self.browse(cr, uid, ids, context):
750                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
751         else:
752             for work in self.browse(cr, uid, ids, context):
753                 duration =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
754                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
755         return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
756
757 project_work()
758
759 class account_analytic_account(osv.osv):
760
761     _inherit = 'account.analytic.account'
762     _description = 'Analytic Account'
763
764     def create(self, cr, uid, vals, context=None):
765         if context is None:
766             context = {}
767         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
768             vals['child_ids'] = []
769         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
770
771 account_analytic_account()
772
773 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: