[IMP] read_group_full
[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
26 from tools.translate import _
27 from osv import fields, osv
28
29 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
30 #class project_project(osv.osv):
31 #    _name = 'project.project'
32 #project_project()
33
34 class project_task_type(osv.osv):
35     _name = 'project.task.type'
36     _description = 'Task Stage'
37     _order = 'sequence'
38     _columns = {
39         'name': fields.char('Stage Name', required=True, size=64, translate=True),
40         'description': fields.text('Description'),
41         'sequence': fields.integer('Sequence'),
42         'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
43         'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
44     }
45     _defaults = {
46         'sequence': 1
47     }
48     _order = 'sequence'
49 project_task_type()
50
51 class project(osv.osv):
52     _name = "project.project"
53     _description = "Project"
54     _inherits = {'account.analytic.account': "analytic_account_id"}
55
56     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
57         if user == 1:
58             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
59         if context and context.get('user_preference'):
60                 cr.execute("""SELECT project.id FROM project_project project
61                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
62                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
63                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
64                 return [(r[0]) for r in cr.fetchall()]
65         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
66             context=context, count=count)
67
68     def _complete_name(self, cr, uid, ids, name, args, context=None):
69         res = {}
70         for m in self.browse(cr, uid, ids, context=context):
71             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
72         return res
73
74     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
75         partner_obj = self.pool.get('res.partner')
76         if not part:
77             return {'value':{'contact_id': False, 'pricelist_id': False}}
78         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
79         pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
80         pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
81         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
82
83     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
84         res = {}.fromkeys(ids, 0.0)
85         if not ids:
86             return res
87         cr.execute('''SELECT
88                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
89             FROM
90                 project_task
91             WHERE
92                 project_id in %s AND
93                 state<>'cancelled'
94             GROUP BY
95                 project_id''', (tuple(ids),))
96         progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
97         for project in self.browse(cr, uid, ids, context=context):
98             s = progress.get(project.id, (0.0,0.0,0.0,0.0))
99             res[project.id] = {
100                 'planned_hours': s[0],
101                 'effective_hours': s[2],
102                 'total_hours': s[1],
103                 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
104             }
105         return res
106
107     def _get_project_task(self, cr, uid, ids, context=None):
108         result = {}
109         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
110             if task.project_id: result[task.project_id.id] = True
111         return result.keys()
112
113     def _get_project_work(self, cr, uid, ids, context=None):
114         result = {}
115         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
116             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
117         return result.keys()
118
119     def unlink(self, cr, uid, ids, *args, **kwargs):
120         for proj in self.browse(cr, uid, ids):
121             if proj.tasks:
122                 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
123         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
124
125     _columns = {
126         'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
127         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
128         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
129         '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),
130         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
131         '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)]}),
132
133         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
134             help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
135         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
136         'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
137             store = {
138                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
139                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
140             }),
141         'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
142         'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
143             store = {
144                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
145                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
146             }),
147         'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
148         '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)]}),
149         '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)]}),
150         '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)]}),
151         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
152      }
153     def _get_type_common(self, cr, uid, context):
154         ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
155         return ids
156
157     _order = "sequence"
158     _defaults = {
159         'active': True,
160         'priority': 1,
161         'sequence': 10,
162         'type_ids': _get_type_common
163     }
164
165     # TODO: Why not using a SQL contraints ?
166     def _check_dates(self, cr, uid, ids, context=None):
167         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
168             if leave['date_start'] and leave['date']:
169                 if leave['date_start'] > leave['date']:
170                     return False
171         return True
172
173     _constraints = [
174         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
175     ]
176
177     def set_template(self, cr, uid, ids, context=None):
178         res = self.setActive(cr, uid, ids, value=False, context=context)
179         return res
180
181     def set_done(self, cr, uid, ids, context=None):
182         task_obj = self.pool.get('project.task')
183         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
184         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
185         self.write(cr, uid, ids, {'state':'close'}, context=context)
186         for (id, name) in self.name_get(cr, uid, ids):
187             message = _("The project '%s' has been closed.") % name
188             self.log(cr, uid, id, message)
189         return True
190
191     def set_cancel(self, cr, uid, ids, context=None):
192         task_obj = self.pool.get('project.task')
193         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
194         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
195         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
196         return True
197
198     def set_pending(self, cr, uid, ids, context=None):
199         self.write(cr, uid, ids, {'state':'pending'}, context=context)
200         return True
201
202     def set_open(self, cr, uid, ids, context=None):
203         self.write(cr, uid, ids, {'state':'open'}, context=context)
204         return True
205
206     def reset_project(self, cr, uid, ids, context=None):
207         res = self.setActive(cr, uid, ids, value=True, context=context)
208         for (id, name) in self.name_get(cr, uid, ids):
209             message = _("The project '%s' has been opened.") % name
210             self.log(cr, uid, id, message)
211         return res
212
213     def copy(self, cr, uid, id, default={}, context=None):
214         if context is None:
215             context = {}
216
217         default = default or {}
218         context['active_test'] = False
219         default['state'] = 'open'
220         proj = self.browse(cr, uid, id, context=context)
221         if not default.get('name', False):
222             default['name'] = proj.name + _(' (copy)')
223
224         res = super(project, self).copy(cr, uid, id, default, context)
225         return res
226
227
228     def template_copy(self, cr, uid, id, default={}, context=None):
229         task_obj = self.pool.get('project.task')
230         proj = self.browse(cr, uid, id, context=context)
231
232         default['tasks'] = [] #avoid to copy all the task automaticly
233         res = self.copy(cr, uid, id, default=default, context=context)
234
235         #copy all the task manually
236         map_task_id = {}
237         for task in proj.tasks:
238             map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
239
240         self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
241         task_obj.duplicate_task(cr, uid, map_task_id, context=context)
242
243         return res
244
245     def duplicate_template(self, cr, uid, ids, context=None):
246         if context is None:
247             context = {}
248         data_obj = self.pool.get('ir.model.data')
249         result = []
250         for proj in self.browse(cr, uid, ids, context=context):
251             parent_id = context.get('parent_id', False)
252             context.update({'analytic_project_copy': True})
253             new_date_start = time.strftime('%Y-%m-%d')
254             new_date_end = False
255             if proj.date_start and proj.date:
256                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
257                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
258                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
259             context.update({'copy':True})
260             new_id = self.template_copy(cr, uid, proj.id, default = {
261                                     'name': proj.name +_(' (copy)'),
262                                     'state':'open',
263                                     'date_start':new_date_start,
264                                     'date':new_date_end,
265                                     'parent_id':parent_id}, context=context)
266             result.append(new_id)
267
268             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
269             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
270             if child_ids:
271                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
272
273         if result and len(result):
274             res_id = result[0]
275             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
276             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
277             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
278             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
279             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
280             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
281             return {
282                 'name': _('Projects'),
283                 'view_type': 'form',
284                 'view_mode': 'form,tree',
285                 'res_model': 'project.project',
286                 'view_id': False,
287                 'res_id': res_id,
288                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
289                 'type': 'ir.actions.act_window',
290                 'search_view_id': search_view['res_id'],
291                 'nodestroy': True
292             }
293
294     # set active value for a project, its sub projects and its tasks
295     def setActive(self, cr, uid, ids, value=True, context=None):
296         task_obj = self.pool.get('project.task')
297         for proj in self.browse(cr, uid, ids, context=None):
298             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
299             cr.execute('select id from project_task where project_id=%s', (proj.id,))
300             tasks_id = [x[0] for x in cr.fetchall()]
301             if tasks_id:
302                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
303             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
304             if child_ids:
305                 self.setActive(cr, uid, child_ids, value, context=None)
306         return True
307
308 project()
309
310 class users(osv.osv):
311     _inherit = 'res.users'
312     _columns = {
313         'context_project_id': fields.many2one('project.project', 'Project')
314     }
315 users()
316
317 class task(osv.osv):
318     _name = "project.task"
319     _description = "Task"
320     _log_create = True
321     _date_name = "date_start"
322
323     def _read_group_type_id(self, cr, uid, ids, domain, context=None):
324         context = context or {}
325         stage_obj = self.pool.get('project.task.type')
326         stage_ids = stage_obj.search(cr, uid, ['|',('id','in',ids)] + [('project_default','=',1)], context=context)
327         return stage_obj.name_get(cr, uid, stage_ids, context=context)
328
329     def _read_group_user_id(self, cr, uid, ids, domain, context={}):
330         context = context or {}
331         if type(context.get('project_id', None)) not in (int, long):
332             return None
333         proj = self.browse(cr, uid, context['project_id'], context=context)
334         ids += map(lambda x: x.id, proj.members)
335         stage_obj = self.pool.get('res.users')
336         stage_ids = stage_obj.search(cr, uid, [('id','in',ids)], context=context)
337         return stage_obj.name_get(cr, uid, ids, context=context)
338
339     _group_by_full = {
340         'type_id': _read_group_type_id,
341         'user_id': _read_group_user_id
342     }
343
344
345     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
346         obj_project = self.pool.get('project.project')
347         for domain in args:
348             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
349                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
350                 if id and isinstance(id, (long, int)):
351                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
352                         args.append(('active', '=', False))
353         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
354
355     def _str_get(self, task, level=0, border='***', context=None):
356         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'+ \
357             border[0]+' '+(task.name or '')+'\n'+ \
358             (task.description or '')+'\n\n'
359
360     # Compute: effective_hours, total_hours, progress
361     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
362         res = {}
363         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
364         hours = dict(cr.fetchall())
365         for task in self.browse(cr, uid, ids, context=context):
366             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
367             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
368             res[task.id]['progress'] = 0.0
369             if (task.remaining_hours + hours.get(task.id, 0.0)):
370                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
371             if task.state in ('done','cancelled'):
372                 res[task.id]['progress'] = 100.0
373         return res
374
375
376     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
377         if remaining and not planned:
378             return {'value':{'planned_hours': remaining}}
379         return {}
380
381     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
382         return {'value':{'remaining_hours': planned - effective}}
383
384     def onchange_project(self, cr, uid, id, project_id):
385         if not project_id:
386             return {}
387         data = self.pool.get('project.project').browse(cr, uid, [project_id])
388         partner_id=data and data[0].parent_id.partner_id
389         if partner_id:
390             return {'value':{'partner_id':partner_id.id}}
391         return {}
392
393     def _default_project(self, cr, uid, context=None):
394         if context is None:
395             context = {}
396         if 'project_id' in context and context['project_id']:
397             return int(context['project_id'])
398         return False
399
400     def duplicate_task(self, cr, uid, map_ids, context=None):
401         for new in map_ids.values():
402             task = self.browse(cr, uid, new, context)
403             child_ids = [ ch.id for ch in task.child_ids]
404             if task.child_ids:
405                 for child in task.child_ids:
406                     if child.id in map_ids.keys():
407                         child_ids.remove(child.id)
408                         child_ids.append(map_ids[child.id])
409
410             parent_ids = [ ch.id for ch in task.parent_ids]
411             if task.parent_ids:
412                 for parent in task.parent_ids:
413                     if parent.id in map_ids.keys():
414                         parent_ids.remove(parent.id)
415                         parent_ids.append(map_ids[parent.id])
416             #FIXME why there is already the copy and the old one
417             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
418
419     def copy_data(self, cr, uid, id, default={}, context=None):
420         default = default or {}
421         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
422         if not default.get('remaining_hours', False):
423             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
424         default['active'] = True
425         default['type_id'] = False
426         if not default.get('name', False):
427             default['name'] = self.browse(cr, uid, id, context=context).name or ''
428             if not context.get('copy',False):
429                 new_name = _("%s (copy)")%default.get('name','')
430                 default.update({'name':new_name})
431         return super(task, self).copy_data(cr, uid, id, default, context)
432
433
434     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
435         res = {}
436         for task in self.browse(cr, uid, ids, context=context):
437             res[task.id] = True
438             if task.project_id:
439                 if task.project_id.active == False or task.project_id.state == 'template':
440                     res[task.id] = False
441         return res
442
443     def _get_task(self, cr, uid, ids, context=None):
444         result = {}
445         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
446             if work.task_id: result[work.task_id.id] = True
447         return result.keys()
448
449     _columns = {
450         'active': fields.function(_is_template, 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."),
451         'name': fields.char('Task Summary', size=128, required=True),
452         'description': fields.text('Description'),
453         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
454         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
455         'type_id': fields.many2one('project.task.type', 'Stage'),
456         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
457                                   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.\
458                                   \n If the task is over, the states is set to \'Done\'.'),
459         'kanban_state': fields.selection([('blocked', 'Blocked'),('normal', 'Normal'),('done', 'Done')], 'Kanban State', readonly=True, required=False),
460         'create_date': fields.datetime('Create Date', readonly=True,select=True),
461         'date_start': fields.datetime('Starting Date',select=True),
462         'date_end': fields.datetime('Ending Date',select=True),
463         'date_deadline': fields.date('Deadline',select=True),
464         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
465         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
466         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
467         'notes': fields.text('Notes'),
468         '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.'),
469         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
470             store = {
471                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
472                 'project.task.work': (_get_task, ['hours'], 10),
473             }),
474         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
475         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
476             store = {
477                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
478                 'project.task.work': (_get_task, ['hours'], 10),
479             }),
480         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
481             store = {
482                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
483                 'project.task.work': (_get_task, ['hours'], 10),
484             }),
485         'delay_hours': fields.function(_hours_get, 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.",
486             store = {
487                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
488                 'project.task.work': (_get_task, ['hours'], 10),
489             }),
490         'user_id': fields.many2one('res.users', 'Assigned to'),
491         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
492         'partner_id': fields.many2one('res.partner', 'Partner'),
493         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
494         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
495         'company_id': fields.many2one('res.company', 'Company'),
496         'id': fields.integer('ID', readonly=True),
497         'color': fields.integer('Color Index'),
498         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
499     }
500
501     _defaults = {
502         'state': 'draft',
503         'kanban_state': 'normal',
504         'priority': '2',
505         'progress': 0,
506         'sequence': 10,
507         'active': True,
508         'project_id': _default_project,
509         'user_id': lambda obj, cr, uid, context: uid,
510         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
511     }
512
513     _order = "priority, sequence, date_start, name, id"
514
515     def set_priority(self, cr, uid, ids, priority):
516         """Set task priority
517         """
518         return self.write(cr, uid, ids, {'priority' : priority})
519
520     def set_high_priority(self, cr, uid, ids, *args):
521         """Set task priority to high
522         """
523         return self.set_priority(cr, uid, ids, '1')
524
525     def set_normal_priority(self, cr, uid, ids, *args):
526         """Set task priority to normal
527         """
528         return self.set_priority(cr, uid, ids, '3')
529
530     def _check_recursion(self, cr, uid, ids, context=None):
531         for id in ids:
532             visited_branch = set()
533             visited_node = set()
534             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
535             if not res:
536                 return False
537
538         return True
539
540     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
541         if id in visited_branch: #Cycle
542             return False
543
544         if id in visited_node: #Already tested don't work one more time for nothing
545             return True
546
547         visited_branch.add(id)
548         visited_node.add(id)
549
550         #visit child using DFS
551         task = self.browse(cr, uid, id, context=context)
552         for child in task.child_ids:
553             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
554             if not res:
555                 return False
556
557         visited_branch.remove(id)
558         return True
559
560     def _check_dates(self, cr, uid, ids, context=None):
561         if context == None:
562             context = {}
563         obj_task = self.browse(cr, uid, ids[0], context=context)
564         start = obj_task.date_start or False
565         end = obj_task.date_end or False
566         if start and end :
567             if start > end:
568                 return False
569         return True
570
571     _constraints = [
572         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
573         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
574     ]
575     #
576     # Override view according to the company definition
577     #
578     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
579         users_obj = self.pool.get('res.users')
580
581         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
582         # this should be safe (no context passed to avoid side-effects)
583         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
584         tm = obj_tm and obj_tm.name or 'Hours'
585
586         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
587
588         if tm in ['Hours','Hour']:
589             return res
590
591         eview = etree.fromstring(res['arch'])
592
593         def _check_rec(eview):
594             if eview.attrib.get('widget','') == 'float_time':
595                 eview.set('widget','float')
596             for child in eview:
597                 _check_rec(child)
598             return True
599
600         _check_rec(eview)
601
602         res['arch'] = etree.tostring(eview)
603
604         for f in res['fields']:
605             if 'Hours' in res['fields'][f]['string']:
606                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
607         return res
608
609     def _check_child_task(self, cr, uid, ids, context=None):
610         if context == None:
611             context = {}
612         tasks = self.browse(cr, uid, ids, context=context)
613         for task in tasks:
614             if task.child_ids:
615                 for child in task.child_ids:
616                     if child.state in ['draft', 'open', 'pending']:
617                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
618         return True
619
620     def action_close(self, cr, uid, ids, context=None):
621         # This action open wizard to send email to partner or project manager after close task.
622         if context == None:
623             context = {}
624         task_id = len(ids) and ids[0] or False
625         self._check_child_task(cr, uid, ids, context=context)
626         if not task_id: return False
627         task = self.browse(cr, uid, task_id, context=context)
628         project = task.project_id
629         res = self.do_close(cr, uid, [task_id], context=context)
630         if project.warn_manager or project.warn_customer:
631             return {
632                 'name': _('Send Email after close task'),
633                 'view_type': 'form',
634                 'view_mode': 'form',
635                 'res_model': 'mail.compose.message',
636                 'type': 'ir.actions.act_window',
637                 'target': 'new',
638                 'nodestroy': True,
639                 'context': {'active_id': task.id,
640                             'active_model': 'project.task'}
641            }
642         return res
643
644     def do_close(self, cr, uid, ids, context={}):
645         """
646         Close Task
647         """
648         request = self.pool.get('res.request')
649         for task in self.browse(cr, uid, ids, context=context):
650             vals = {}
651             project = task.project_id
652             if project:
653                 # Send request to project manager
654                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
655                     request.create(cr, uid, {
656                         'name': _("Task '%s' closed") % task.name,
657                         'state': 'waiting',
658                         'act_from': uid,
659                         'act_to': project.user_id.id,
660                         'ref_partner_id': task.partner_id.id,
661                         'ref_doc1': 'project.task,%d'% (task.id,),
662                         'ref_doc2': 'project.project,%d'% (project.id,),
663                     }, context=context)
664
665             for parent_id in task.parent_ids:
666                 if parent_id.state in ('pending','draft'):
667                     reopen = True
668                     for child in parent_id.child_ids:
669                         if child.id != task.id and child.state not in ('done','cancelled'):
670                             reopen = False
671                     if reopen:
672                         self.do_reopen(cr, uid, [parent_id.id], context=context)
673             vals.update({'state': 'done'})
674             vals.update({'remaining_hours': 0.0})
675             if not task.date_end:
676                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
677             self.write(cr, uid, [task.id],vals, context=context)
678             message = _("The task '%s' is done") % (task.name,)
679             self.log(cr, uid, task.id, message)
680         return True
681
682     def do_reopen(self, cr, uid, ids, context=None):
683         request = self.pool.get('res.request')
684
685         for task in self.browse(cr, uid, ids, context=context):
686             project = task.project_id
687             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
688                 request.create(cr, uid, {
689                     'name': _("Task '%s' set in progress") % task.name,
690                     'state': 'waiting',
691                     'act_from': uid,
692                     'act_to': project.user_id.id,
693                     'ref_partner_id': task.partner_id.id,
694                     'ref_doc1': 'project.task,%d' % task.id,
695                     'ref_doc2': 'project.project,%d' % project.id,
696                 }, context=context)
697
698             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
699         return True
700
701     def do_cancel(self, cr, uid, ids, context={}):
702         request = self.pool.get('res.request')
703         tasks = self.browse(cr, uid, ids, context=context)
704         self._check_child_task(cr, uid, ids, context=context)
705         for task in tasks:
706             project = task.project_id
707             if project.warn_manager and project.user_id and (project.user_id.id != uid):
708                 request.create(cr, uid, {
709                     'name': _("Task '%s' cancelled") % task.name,
710                     'state': 'waiting',
711                     'act_from': uid,
712                     'act_to': project.user_id.id,
713                     'ref_partner_id': task.partner_id.id,
714                     'ref_doc1': 'project.task,%d' % task.id,
715                     'ref_doc2': 'project.project,%d' % project.id,
716                 }, context=context)
717             message = _("The task '%s' is cancelled.") % (task.name,)
718             self.log(cr, uid, task.id, message)
719             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
720         return True
721
722     def do_open(self, cr, uid, ids, context={}):
723         tasks= self.browse(cr, uid, ids, context=context)
724         for t in tasks:
725             data = {'state': 'open'}
726             if not t.date_start:
727                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
728             self.write(cr, uid, [t.id], data, context=context)
729             message = _("The task '%s' is opened.") % (t.name,)
730             self.log(cr, uid, t.id, message)
731         return True
732
733     def do_draft(self, cr, uid, ids, context={}):
734         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
735         return True
736
737     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
738         """
739         Delegate Task to another users.
740         """
741         task = self.browse(cr, uid, task_id, context=context)
742         self.copy(cr, uid, task.id, {
743             'name': delegate_data['name'],
744             'user_id': delegate_data['user_id'],
745             'planned_hours': delegate_data['planned_hours'],
746             'remaining_hours': delegate_data['planned_hours'],
747             'parent_ids': [(6, 0, [task.id])],
748             'state': 'draft',
749             'description': delegate_data['new_task_description'] or '',
750             'child_ids': [],
751             'work_ids': []
752         }, context=context)
753         newname = delegate_data['prefix'] or ''
754         self.write(cr, uid, [task.id], {
755             'remaining_hours': delegate_data['planned_hours_me'],
756             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
757             'name': newname,
758         }, context=context)
759         if delegate_data['state'] == 'pending':
760             self.do_pending(cr, uid, [task.id], context)
761         else:
762             self.do_close(cr, uid, [task.id], context=context)
763         user_pool = self.pool.get('res.users')
764         delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
765         message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
766         self.log(cr, uid, task.id, message)
767         return True
768
769     def do_pending(self, cr, uid, ids, context={}):
770         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
771         for (id, name) in self.name_get(cr, uid, ids):
772             message = _("The task '%s' is pending.") % name
773             self.log(cr, uid, id, message)
774         return True
775
776     def set_remaining_time_1(self, cr, uid, ids, context=None):
777         self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
778         return True
779
780     def set_remaining_time_2(self, cr, uid, ids, context=None):
781         self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
782         return True
783
784     def set_remaining_time_5(self, cr, uid, ids, context=None):
785         self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
786         return True
787
788     def set_remaining_time_10(self, cr, uid, ids, context=None):
789         self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
790         return True
791
792     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
793         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
794
795     def set_kanban_state_normal(self, cr, uid, ids, context=None):
796         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
797
798     def set_kanban_state_done(self, cr, uid, ids, context=None):
799         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
800
801     def _change_type(self, cr, uid, ids, next, *args):
802         """
803             go to the next stage
804             if next is False, go to previous stage
805         """
806         for task in self.browse(cr, uid, ids):
807             if  task.project_id.type_ids:
808                 typeid = task.type_id.id
809                 types_seq={}
810                 for type in task.project_id.type_ids :
811                     types_seq[type.id] = type.sequence
812                 if next:
813                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
814                 else:
815                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
816                 sorted_types = [x[0] for x in types]
817                 if not typeid:
818                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
819                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
820                     index = sorted_types.index(typeid)
821                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
822         return True
823
824     def next_type(self, cr, uid, ids, *args):
825         return self._change_type(cr, uid, ids, True, *args)
826
827     def prev_type(self, cr, uid, ids, *args):
828         return self._change_type(cr, uid, ids, False, *args)
829
830     def unlink(self, cr, uid, ids, context=None):
831         if context == None:
832             context = {}
833         self._check_child_task(cr, uid, ids, context=context)
834         res = super(task, self).unlink(cr, uid, ids, context)
835         return res
836
837 task()
838
839 class project_work(osv.osv):
840     _name = "project.task.work"
841     _description = "Project Task Work"
842     _columns = {
843         'name': fields.char('Work summary', size=128),
844         'date': fields.datetime('Date', select="1"),
845         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
846         'hours': fields.float('Time Spent'),
847         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
848         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
849     }
850
851     _defaults = {
852         'user_id': lambda obj, cr, uid, context: uid,
853         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
854     }
855
856     _order = "date desc"
857     def create(self, cr, uid, vals, *args, **kwargs):
858         if 'hours' in vals and (not vals['hours']):
859             vals['hours'] = 0.00
860         if 'task_id' in vals:
861             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
862         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
863
864     def write(self, cr, uid, ids, vals, context=None):
865         if 'hours' in vals and (not vals['hours']):
866             vals['hours'] = 0.00
867         if 'hours' in vals:
868             for work in self.browse(cr, uid, ids, context=context):
869                 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
870         return super(project_work,self).write(cr, uid, ids, vals, context)
871
872     def unlink(self, cr, uid, ids, *args, **kwargs):
873         for work in self.browse(cr, uid, ids):
874             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
875         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
876 project_work()
877
878 class account_analytic_account(osv.osv):
879
880     _inherit = 'account.analytic.account'
881     _description = 'Analytic Account'
882
883     def create(self, cr, uid, vals, context=None):
884         if context is None:
885             context = {}
886         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
887             vals['child_ids'] = []
888         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
889     
890     def unlink(self, cr, uid, ids, *args, **kwargs):
891         project_obj = self.pool.get('project.project')
892         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
893         if analytic_ids:
894             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
895         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
896
897 account_analytic_account()
898
899 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: