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