[IMP] sale->configuration :Add Email Menu
[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
403             for ctimespent in task.work_ids:
404                 timespent=timespent+ctimespent.hours
405             res[task.id]['delay_hours'] = task.planned_hours - timespent
406
407             res[task.id]['progress'] = 0.0
408             if (task.remaining_hours + hours.get(task.id, 0.0)):
409                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
410             if task.state in ('done','cancelled'):
411                 res[task.id]['progress'] = 100.0
412         return res
413
414
415     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
416         if remaining and not planned:
417             return {'value':{'planned_hours': remaining}}
418         return {}
419
420     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
421         return {'value':{'remaining_hours': planned - effective}}
422
423     def _default_project(self, cr, uid, context=None):
424         if context is None:
425             context = {}
426         if 'project_id' in context and context['project_id']:
427             return int(context['project_id'])
428         return False
429
430     def copy_data(self, cr, uid, id, default={}, context=None):
431         default = default or {}
432         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
433         if not default.get('remaining_hours', False):
434             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
435         default['active'] = True
436         default['type_id'] = False
437         if not default.get('name', False):
438             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
439         return super(task, self).copy_data(cr, uid, id, default, context)
440
441     def _check_dates(self, cr, uid, ids, context=None):
442         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
443         if task['date_start'] and task['date_end']:
444              if task['date_start'] > task['date_end']:
445                  return False
446         return True
447
448     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
449         res = {}
450         for task in self.browse(cr, uid, ids, context=context):
451             res[task.id] = True
452             if task.project_id:
453                 if task.project_id.active == False or task.project_id.state == 'template':
454                     res[task.id] = False
455         return res
456
457     def _get_task(self, cr, uid, ids, context=None):
458         if context is None:
459             context = {}
460         result = {}
461         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
462             if work.task_id: result[work.task_id.id] = True
463         return result.keys()
464
465     _columns = {
466         '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."),
467         'name': fields.char('Task Summary', size=128, required=True),
468         'description': fields.text('Description'),
469         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
470         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
471         'type_id': fields.many2one('project.task.type', 'Type',),
472         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
473                                   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.\
474                                   \n If the task is over, the states is set to \'Done\'.'),
475         'create_date': fields.datetime('Create Date', readonly=True),
476         'date_start': fields.datetime('Starting Date'),
477         'date_end': fields.datetime('Ending Date'),
478         'date_deadline': fields.date('Deadline'),
479         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
480         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
481         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
482         'notes': fields.text('Notes'),
483         '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.'),
484         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
485             store = {
486                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
487                 'project.task.work': (_get_task, ['hours'], 10),
488             }),
489         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
490         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
491             store = {
492                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
493                 'project.task.work': (_get_task, ['hours'], 10),
494             }),
495         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
496             store = {
497                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
498                 'project.task.work': (_get_task, ['hours'], 10),
499             }),
500         '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.",
501             store = {
502                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
503                 'project.task.work': (_get_task, ['hours'], 10),
504             }),
505         'user_id': fields.many2one('res.users', 'Assigned to'),
506         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
507         'partner_id': fields.many2one('res.partner', 'Partner'),
508         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
509         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
510         'company_id': fields.many2one('res.company', 'Company'),
511         'id': fields.integer('ID'),
512     }
513
514     _defaults = {
515         'state': 'draft',
516         'priority': '2',
517         'progress': 0,
518         'sequence': 10,
519         'active': True,
520         'project_id': _default_project,
521         'user_id': lambda obj, cr, uid, context: uid,
522         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
523     }
524
525     _order = "sequence, priority, date_start, id"
526
527     def _check_recursion(self, cr, uid, ids):
528         obj_task = self.browse(cr, uid, ids[0])
529         parent_ids = [x.id for x in obj_task.parent_ids]
530         children_ids = [x.id for x in obj_task.child_ids]
531
532         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
533             return False
534
535         while(ids):
536             cr.execute('SELECT DISTINCT task_id '\
537                        'FROM project_task_parent_rel '\
538                        'WHERE parent_id IN %s', (tuple(ids),))
539             child_ids = map(lambda x: x[0], cr.fetchall())
540             c_ids = child_ids
541             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
542                 return False
543             while len(c_ids):
544                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
545                 if (list(set(parent_ids).intersection(set(s_ids)))):
546                     return False
547                 c_ids = s_ids
548             ids = child_ids
549         return True
550
551     _constraints = [
552         (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
553     ]
554     #
555     # Override view according to the company definition
556     #
557
558     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
559         users_obj = self.pool.get('res.users')
560         obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
561         tm = obj_tm and obj_tm.name or 'Hours'
562
563         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
564
565         if tm in ['Hours','Hour']:
566             return res
567
568         eview = etree.fromstring(res['arch'])
569
570         def _check_rec(eview):
571             if eview.attrib.get('widget','') == 'float_time':
572                 eview.set('widget','float')
573             for child in eview:
574                 _check_rec(child)
575             return True
576
577         _check_rec(eview)
578
579         res['arch'] = etree.tostring(eview)
580
581         for f in res['fields']:
582             if 'Hours' in res['fields'][f]['string']:
583                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
584         return res
585
586     def action_close(self, cr, uid, ids, context=None):
587         # This action open wizard to send email to partner or project manager after close task.
588         project_id = len(ids) and ids[0] or False
589         if not project_id: return False
590         task = self.browse(cr, uid, project_id, context=context)
591         project = task.project_id
592         res = self.do_close(cr, uid, [project_id], context=context)
593         if project.warn_manager or project.warn_customer:
594            return {
595                 'name': _('Send Email after close task'),
596                 'view_type': 'form',
597                 'view_mode': 'form',
598                 'res_model': 'project.task.close',
599                 'type': 'ir.actions.act_window',
600                 'target': 'new',
601                 'nodestroy': True,
602                 'context': {'active_id': task.id}
603            }
604         return res
605
606     def do_close(self, cr, uid, ids, context=None):
607         """
608         Close Task
609         """
610         if context is None:
611             context = {}
612         request = self.pool.get('res.request')
613         for task in self.browse(cr, uid, ids, context=context):
614             project = task.project_id
615             if project:
616                 # Send request to project manager
617                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
618                     request.create(cr, uid, {
619                         'name': _("Task '%s' closed") % task.name,
620                         'state': 'waiting',
621                         'act_from': uid,
622                         'act_to': project.user_id.id,
623                         'ref_partner_id': task.partner_id.id,
624                         'ref_doc1': 'project.task,%d'% (task.id,),
625                         'ref_doc2': 'project.project,%d'% (project.id,),
626                     })
627
628             for parent_id in task.parent_ids:
629                 if parent_id.state in ('pending','draft'):
630                     reopen = True
631                     for child in parent_id.child_ids:
632                         if child.id != task.id and child.state not in ('done','cancelled'):
633                             reopen = False
634                     if reopen:
635                         self.do_reopen(cr, uid, [parent_id.id])
636             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
637             message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
638             self.log(cr, uid, task.id, message)
639         return True
640
641     def do_reopen(self, cr, uid, ids, context=None):
642         if context is None:
643             context = {}
644         request = self.pool.get('res.request')
645
646         for task in self.browse(cr, uid, ids, context=context):
647             project = task.project_id
648             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
649                 request.create(cr, uid, {
650                     'name': _("Task '%s' set in progress") % task.name,
651                     'state': 'waiting',
652                     'act_from': uid,
653                     'act_to': project.user_id.id,
654                     'ref_partner_id': task.partner_id.id,
655                     'ref_doc1': 'project.task,%d' % task.id,
656                     'ref_doc2': 'project.project,%d' % project.id,
657                 })
658
659             self.write(cr, uid, [task.id], {'state': 'open'})
660
661         return True
662
663     def do_cancel(self, cr, uid, ids, *args):
664         request = self.pool.get('res.request')
665         tasks = self.browse(cr, uid, ids)
666         for task in tasks:
667             project = task.project_id
668             if project.warn_manager and project.user_id and (project.user_id.id != uid):
669                 request.create(cr, uid, {
670                     'name': _("Task '%s' cancelled") % task.name,
671                     'state': 'waiting',
672                     'act_from': uid,
673                     'act_to': project.user_id.id,
674                     'ref_partner_id': task.partner_id.id,
675                     'ref_doc1': 'project.task,%d' % task.id,
676                     'ref_doc2': 'project.project,%d' % project.id,
677                 })
678             message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
679             self.log(cr, uid, task.id, message)
680             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
681         return True
682
683     def do_open(self, cr, uid, ids, *args):
684         tasks= self.browse(cr,uid,ids)
685         for t in tasks:
686             data = {'state': 'open'}
687             if not t.date_start:
688                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
689             self.write(cr, uid, [t.id], data)
690             message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
691             self.log(cr, uid, t.id, message)
692         return True
693
694     def do_draft(self, cr, uid, ids, *args):
695         self.write(cr, uid, ids, {'state': 'draft'})
696         return True
697
698     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
699         """
700         Delegate Task to another users.
701         """
702         if context is None:
703             context = {}
704         task = self.browse(cr, uid, task_id, context=context)
705         new_task_id = self.copy(cr, uid, task.id, {
706             'name': delegate_data['name'],
707             'user_id': delegate_data['user_id'],
708             'planned_hours': delegate_data['planned_hours'],
709             'remaining_hours': delegate_data['planned_hours'],
710             'parent_ids': [(6, 0, [task.id])],
711             'state': 'draft',
712             'description': delegate_data['new_task_description'] or '',
713             'child_ids': [],
714             'work_ids': []
715         }, context)
716         newname = delegate_data['prefix'] or ''
717         self.write(cr, uid, [task.id], {
718             'remaining_hours': delegate_data['planned_hours_me'],
719             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
720             'name': newname,
721         }, context)
722         if delegate_data['state'] == 'pending':
723             self.do_pending(cr, uid, [task.id], context)
724         else:
725             self.do_close(cr, uid, [task.id], context)
726         user_pool = self.pool.get('res.users')
727         delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
728         message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_user.name +"' "
729         self.log(cr, uid, task.id, message)
730         return True
731
732     def do_pending(self, cr, uid, ids, *args):
733         self.write(cr, uid, ids, {'state': 'pending'})
734         for (id, name) in self.name_get(cr, uid, ids):
735             message = _('Task ') + " '" + name + "' "+ _("is Pending.")
736             self.log(cr, uid, id, message)
737         return True
738
739     def next_type(self, cr, uid, ids, *args):
740         for task in self.browse(cr, uid, ids):
741             typeid = task.type_id.id
742             types = map(lambda x:x.id, task.project_id.type_ids or [])
743             if types:
744                 if not typeid:
745                     self.write(cr, uid, task.id, {'type_id': types[0]})
746                 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
747                     index = types.index(typeid)
748                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
749         return True
750
751     def prev_type(self, cr, uid, ids, *args):
752         for task in self.browse(cr, uid, ids):
753             typeid = task.type_id.id
754             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
755             if types:
756                 if typeid and typeid in types:
757                     index = types.index(typeid)
758                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
759         return True
760
761 task()
762
763 class project_work(osv.osv):
764     _name = "project.task.work"
765     _description = "Project Task Work"
766     _columns = {
767         'name': fields.char('Work summary', size=128),
768         'date': fields.datetime('Date'),
769         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
770         'hours': fields.float('Time Spent'),
771         'user_id': fields.many2one('res.users', 'Done by', required=True),
772         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
773     }
774
775     _defaults = {
776         'user_id': lambda obj, cr, uid, context: uid,
777         'date': time.strftime('%Y-%m-%d %H:%M:%S')
778     }
779
780     _order = "date desc"
781
782     def create(self, cr, uid, vals, *args, **kwargs):
783         project_obj = self.pool.get('project.project')
784         uom_obj = self.pool.get('product.uom')
785         if vals.get('hours', False):
786             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
787             duration = vals['hours']
788             if user_uom != default_uom:
789                 duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
790             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
791         return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
792
793     def write(self, cr, uid, ids, vals, context=None):
794         project_obj = self.pool.get('project.project')
795         uom_obj = self.pool.get('product.uom')
796         if vals.get('hours', False):
797             old_hours = self.browse(cr, uid, ids, context=context)
798             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
799             duration = vals['hours']
800             for old in old_hours:
801                 if vals.get('hours') != old.hours:
802                     # this code is only needed when we update the hours of the project
803                     # TODO: it may still a second calculation if the task.id is changed
804                     # at this task.
805                     if user_uom == default_uom:
806                         for work in self.browse(cr, uid, ids, context=context):
807                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
808                     else:
809                         for work in self.browse(cr, uid, ids, context=context):
810                             duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
811                             del_work =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
812                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
813         return super(project_work,self).write(cr, uid, ids, vals, context=context)
814
815     def unlink(self, cr, uid, ids, *args, **kwargs):
816         context = kwargs.get('context', {})
817         project_obj = self.pool.get('project.project')
818         uom_obj = self.pool.get('product.uom')
819         user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
820         if user_uom == default_uom:
821             for work in self.browse(cr, uid, ids, context):
822                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
823         else:
824             for work in self.browse(cr, uid, ids, context):
825                 duration =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
826                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
827         return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
828
829 project_work()
830
831 class account_analytic_account(osv.osv):
832
833     _inherit = 'account.analytic.account'
834     _description = 'Analytic Account'
835
836     def create(self, cr, uid, vals, context=None):
837         if context is None:
838             context = {}
839         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
840             vals['child_ids'] = []
841         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
842
843 account_analytic_account()
844
845 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: