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