[MERGE] bug fix 720631
[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         proj = self.browse(cr, uid, id, context=context)
209         default = default or {}
210         context['active_test'] = False
211         default['state'] = 'open'
212         if not default.get('name', False):
213             default['name'] = proj.name + _(' (copy)')
214         res = super(project, self).copy(cr, uid, id, default, context)
215
216         return res
217
218     def duplicate_template(self, cr, uid, ids, context=None):
219         if context is None:
220             context = {}
221         project_obj = self.pool.get('project.project')
222         data_obj = self.pool.get('ir.model.data')
223         result = []
224         for proj in self.browse(cr, uid, ids, context=context):
225             parent_id = context.get('parent_id', False)
226             context.update({'analytic_project_copy': True})
227             new_date_start = time.strftime('%Y-%m-%d')
228             new_date_end = False
229             if proj.date_start and proj.date:
230                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
231                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
232                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
233             context.update({'copy':True})
234             new_id = project_obj.copy(cr, uid, proj.id, default = {
235                                     'name': proj.name +_(' (copy)'),
236                                     'state':'open',
237                                     'date_start':new_date_start,
238                                     'date':new_date_end,
239                                     'parent_id':parent_id}, context=context)
240             result.append(new_id)
241
242             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
243             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
244             if child_ids:
245                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
246
247         if result and len(result):
248             res_id = result[0]
249             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
250             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
251             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
252             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
253             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
254             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
255             return {
256                 'name': _('Projects'),
257                 'view_type': 'form',
258                 'view_mode': 'form,tree',
259                 'res_model': 'project.project',
260                 'view_id': False,
261                 'res_id': res_id,
262                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
263                 'type': 'ir.actions.act_window',
264                 'search_view_id': search_view['res_id'],
265                 'nodestroy': True
266             }
267
268     # set active value for a project, its sub projects and its tasks
269     def setActive(self, cr, uid, ids, value=True, context=None):
270         task_obj = self.pool.get('project.task')
271         for proj in self.browse(cr, uid, ids, context=None):
272             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
273             cr.execute('select id from project_task where project_id=%s', (proj.id,))
274             tasks_id = [x[0] for x in cr.fetchall()]
275             if tasks_id:
276                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
277             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
278             if child_ids:
279                 self.setActive(cr, uid, child_ids, value, context=None)
280         return True
281
282 project()
283
284 class users(osv.osv):
285     _inherit = 'res.users'
286     _columns = {
287         'context_project_id': fields.many2one('project.project', 'Project')
288     }
289 users()
290
291 class task(osv.osv):
292     _name = "project.task"
293     _description = "Task"
294     _log_create = True
295     _date_name = "date_start"
296
297     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
298         obj_project = self.pool.get('project.project')
299         for domain in args:
300             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
301                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
302                 if id and isinstance(id, (long, int)):
303                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
304                         args.append(('active', '=', False))
305         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
306
307     def _str_get(self, task, level=0, border='***', context=None):
308         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'+ \
309             border[0]+' '+(task.name or '')+'\n'+ \
310             (task.description or '')+'\n\n'
311
312     # Compute: effective_hours, total_hours, progress
313     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
314         res = {}
315         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
316         hours = dict(cr.fetchall())
317         for task in self.browse(cr, uid, ids, context=context):
318             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)}
319             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
320             res[task.id]['progress'] = 0.0
321             if (task.remaining_hours + hours.get(task.id, 0.0)):
322                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
323             if task.state in ('done','cancelled'):
324                 res[task.id]['progress'] = 100.0
325         return res
326
327
328     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
329         if remaining and not planned:
330             return {'value':{'planned_hours': remaining}}
331         return {}
332
333     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
334         return {'value':{'remaining_hours': planned - effective}}
335     
336     def onchange_project(self, cr, uid, id, project_id):
337         if not project_id:
338             return {}
339         data = self.pool.get('project.project').browse(cr, uid, [project_id])
340         partner_id=data and data[0].parent_id.partner_id
341         if partner_id:
342             return {'value':{'partner_id':partner_id.id}}
343         return {}
344     
345     def _default_project(self, cr, uid, context=None):
346         if context is None:
347             context = {}
348         if 'project_id' in context and context['project_id']:
349             return int(context['project_id'])
350         return False
351
352     def copy_data(self, cr, uid, id, default={}, context=None):
353         default = default or {}
354         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
355         if not default.get('remaining_hours', False):
356             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
357         default['active'] = True
358         default['type_id'] = False
359         if not default.get('name', False):
360             default['name'] = self.browse(cr, uid, id, context=context).name or ''
361             if not context.get('copy',False):
362                  new_name = _("%s (copy)")%default.get('name','')
363                  default.update({'name':new_name})            
364         return super(task, self).copy_data(cr, uid, id, default, context)
365
366     def _check_dates(self, cr, uid, ids, context=None):
367         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
368         if task['date_start'] and task['date_end']:
369              if task['date_start'] > task['date_end']:
370                  return False
371         return True
372
373     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
374         res = {}
375         for task in self.browse(cr, uid, ids, context=context):
376             res[task.id] = True
377             if task.project_id:
378                 if task.project_id.active == False or task.project_id.state == 'template':
379                     res[task.id] = False
380         return res
381
382     def _get_task(self, cr, uid, ids, context=None):
383         result = {}
384         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
385             if work.task_id: result[work.task_id.id] = True
386         return result.keys()
387
388     _columns = {
389         '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."),
390         'name': fields.char('Task Summary', size=128, required=True),
391         'description': fields.text('Description'),
392         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
393         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
394         'type_id': fields.many2one('project.task.type', 'Stage'),
395         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
396                                   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.\
397                                   \n If the task is over, the states is set to \'Done\'.'),
398         'create_date': fields.datetime('Create Date', readonly=True,select=True),
399         'date_start': fields.datetime('Starting Date',select=True),
400         'date_end': fields.datetime('Ending Date',select=True),
401         'date_deadline': fields.date('Deadline',select=True),
402         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
403         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
404         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
405         'notes': fields.text('Notes'),
406         '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.'),
407         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
408             store = {
409                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
410                 'project.task.work': (_get_task, ['hours'], 10),
411             }),
412         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
413         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
414             store = {
415                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
416                 'project.task.work': (_get_task, ['hours'], 10),
417             }),
418         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
419             store = {
420                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
421                 'project.task.work': (_get_task, ['hours'], 10),
422             }),
423         '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.",
424             store = {
425                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
426                 'project.task.work': (_get_task, ['hours'], 10),
427             }),
428         'user_id': fields.many2one('res.users', 'Assigned to'),
429         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
430         'partner_id': fields.many2one('res.partner', 'Partner'),
431         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
432         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
433         'company_id': fields.many2one('res.company', 'Company'),
434         'id': fields.integer('ID'),
435     }
436
437     _defaults = {
438         'state': 'draft',
439         'priority': '2',
440         'progress': 0,
441         'sequence': 10,
442         'active': True,
443         'project_id': _default_project,
444         'user_id': lambda obj, cr, uid, context: uid,
445         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
446     }
447
448     _order = "sequence,priority, date_start, name, id"
449
450     def _check_recursion(self, cr, uid, ids, context=None):
451         obj_task = self.browse(cr, uid, ids[0], context=context)
452         parent_ids = [x.id for x in obj_task.parent_ids]
453         children_ids = [x.id for x in obj_task.child_ids]
454
455         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
456             return False
457
458         while(ids):
459             cr.execute('SELECT DISTINCT task_id '\
460                        'FROM project_task_parent_rel '\
461                        'WHERE parent_id IN %s', (tuple(ids),))
462             child_ids = map(lambda x: x[0], cr.fetchall())
463             c_ids = child_ids
464             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
465                 return False
466             while len(c_ids):
467                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
468                 if (list(set(parent_ids).intersection(set(s_ids)))):
469                     return False
470                 c_ids = s_ids
471             ids = child_ids
472         return True
473
474     def _check_dates(self, cr, uid, ids, context=None):
475         if context == None:
476             context = {}
477         obj_task = self.browse(cr, uid, ids[0], context=context)
478         start = obj_task.date_start or False
479         end = obj_task.date_end or False
480         if start and end :
481             if start > end:        
482                 return False
483         return True           
484
485     _constraints = [
486         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
487         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
488     ]
489     #
490     # Override view according to the company definition
491     #
492
493
494     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
495         users_obj = self.pool.get('res.users')
496
497         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
498         # this should be safe (no context passed to avoid side-effects)
499         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
500         tm = obj_tm and obj_tm.name or 'Hours'
501
502         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
503
504         if tm in ['Hours','Hour']:
505             return res
506
507         eview = etree.fromstring(res['arch'])
508
509         def _check_rec(eview):
510             if eview.attrib.get('widget','') == 'float_time':
511                 eview.set('widget','float')
512             for child in eview:
513                 _check_rec(child)
514             return True
515
516         _check_rec(eview)
517
518         res['arch'] = etree.tostring(eview)
519
520         for f in res['fields']:
521             if 'Hours' in res['fields'][f]['string']:
522                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
523         return res
524
525     def action_close(self, cr, uid, ids, context=None):
526         # This action open wizard to send email to partner or project manager after close task.
527         project_id = len(ids) and ids[0] or False
528         if not project_id: return False
529         task = self.browse(cr, uid, project_id, context=context)
530         project = task.project_id
531         res = self.do_close(cr, uid, [project_id], context=context)
532         if project.warn_manager or project.warn_customer:
533            return {
534                 'name': _('Send Email after close task'),
535                 'view_type': 'form',
536                 'view_mode': 'form',
537                 'res_model': 'project.task.close',
538                 'type': 'ir.actions.act_window',
539                 'target': 'new',
540                 'nodestroy': True,
541                 'context': {'active_id': task.id}
542            }
543         return res
544
545     def do_close(self, cr, uid, ids, context=None):
546         """
547         Close Task
548         """
549         request = self.pool.get('res.request')
550         for task in self.browse(cr, uid, ids, context=context):
551             vals = {}
552             project = task.project_id
553             if project:
554                 # Send request to project manager
555                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
556                     request.create(cr, uid, {
557                         'name': _("Task '%s' closed") % task.name,
558                         'state': 'waiting',
559                         'act_from': uid,
560                         'act_to': project.user_id.id,
561                         'ref_partner_id': task.partner_id.id,
562                         'ref_doc1': 'project.task,%d'% (task.id,),
563                         'ref_doc2': 'project.project,%d'% (project.id,),
564                     })
565
566             for parent_id in task.parent_ids:
567                 if parent_id.state in ('pending','draft'):
568                     reopen = True
569                     for child in parent_id.child_ids:
570                         if child.id != task.id and child.state not in ('done','cancelled'):
571                             reopen = False
572                     if reopen:
573                         self.do_reopen(cr, uid, [parent_id.id])
574             vals.update({'state': 'done'})
575             vals.update({'remaining_hours': 0.0})
576             if not task.date_end:
577                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
578             self.write(cr, uid, [task.id],vals)
579             message = _("The task '%s' is done") % (task.name,)
580             self.log(cr, uid, task.id, message)
581         return True
582
583     def do_reopen(self, cr, uid, ids, context=None):
584         request = self.pool.get('res.request')
585
586         for task in self.browse(cr, uid, ids, context=context):
587             project = task.project_id
588             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
589                 request.create(cr, uid, {
590                     'name': _("Task '%s' set in progress") % task.name,
591                     'state': 'waiting',
592                     'act_from': uid,
593                     'act_to': project.user_id.id,
594                     'ref_partner_id': task.partner_id.id,
595                     'ref_doc1': 'project.task,%d' % task.id,
596                     'ref_doc2': 'project.project,%d' % project.id,
597                 })
598
599             self.write(cr, uid, [task.id], {'state': 'open'})
600
601         return True
602
603     def do_cancel(self, cr, uid, ids, *args):
604         request = self.pool.get('res.request')
605         tasks = self.browse(cr, uid, ids)
606         for task in tasks:
607             project = task.project_id
608             if project.warn_manager and project.user_id and (project.user_id.id != uid):
609                 request.create(cr, uid, {
610                     'name': _("Task '%s' cancelled") % task.name,
611                     'state': 'waiting',
612                     'act_from': uid,
613                     'act_to': project.user_id.id,
614                     'ref_partner_id': task.partner_id.id,
615                     'ref_doc1': 'project.task,%d' % task.id,
616                     'ref_doc2': 'project.project,%d' % project.id,
617                 })
618             message = _("The task '%s' is cancelled.") % (task.name,)
619             self.log(cr, uid, task.id, message)
620             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
621         return True
622
623     def do_open(self, cr, uid, ids, *args):
624         tasks= self.browse(cr,uid,ids)
625         for t in tasks:
626             data = {'state': 'open'}
627             if not t.date_start:
628                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
629             self.write(cr, uid, [t.id], data)
630             message = _("The task '%s' is opened.") % (t.name,)
631             self.log(cr, uid, t.id, message)
632         return True
633
634     def do_draft(self, cr, uid, ids, *args):
635         self.write(cr, uid, ids, {'state': 'draft'})
636         return True
637
638     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
639         """
640         Delegate Task to another users.
641         """
642         task = self.browse(cr, uid, task_id, context=context)
643         self.copy(cr, uid, task.id, {
644             'name': delegate_data['name'],
645             'user_id': delegate_data['user_id'],
646             'planned_hours': delegate_data['planned_hours'],
647             'remaining_hours': delegate_data['planned_hours'],
648             'parent_ids': [(6, 0, [task.id])],
649             'state': 'draft',
650             'description': delegate_data['new_task_description'] or '',
651             'child_ids': [],
652             'work_ids': []
653         }, context=context)
654         newname = delegate_data['prefix'] or ''
655         self.write(cr, uid, [task.id], {
656             'remaining_hours': delegate_data['planned_hours_me'],
657             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
658             'name': newname,
659         }, context=context)
660         if delegate_data['state'] == 'pending':
661             self.do_pending(cr, uid, [task.id], context)
662         else:
663             self.do_close(cr, uid, [task.id], context=context)
664         user_pool = self.pool.get('res.users')
665         delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
666         message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
667         self.log(cr, uid, task.id, message)
668         return True
669
670     def do_pending(self, cr, uid, ids, *args):
671         self.write(cr, uid, ids, {'state': 'pending'})
672         for (id, name) in self.name_get(cr, uid, ids):
673             message = _("The task '%s' is pending.") % name
674             self.log(cr, uid, id, message)
675         return True
676
677     def next_type(self, cr, uid, ids, *args):
678         for task in self.browse(cr, uid, ids):
679             typeid = task.type_id.id
680             types = map(lambda x:x.id, task.project_id.type_ids or [])
681             if types:
682                 if not typeid:
683                     self.write(cr, uid, task.id, {'type_id': types[0]})
684                 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
685                     index = types.index(typeid)
686                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
687         return True
688
689     def prev_type(self, cr, uid, ids, *args):
690         for task in self.browse(cr, uid, ids):
691             typeid = task.type_id.id
692             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
693             if types:
694                 if typeid and typeid in types:
695                     index = types.index(typeid)
696                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
697         return True
698
699 task()
700
701 class project_work(osv.osv):
702     _name = "project.task.work"
703     _description = "Project Task Work"
704     _columns = {
705         'name': fields.char('Work summary', size=128),
706         'date': fields.datetime('Date'),
707         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
708         'hours': fields.float('Time Spent'),
709         'user_id': fields.many2one('res.users', 'Done by', required=True),
710         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
711     }
712
713     _defaults = {
714         'user_id': lambda obj, cr, uid, context: uid,
715         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
716     }
717
718     _order = "date desc"
719     def create(self, cr, uid, vals, *args, **kwargs):
720         if 'hours' in vals and (not vals['hours']):
721             vals['hours'] = 0.00
722         if 'task_id' in vals:
723             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
724         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
725
726     def write(self, cr, uid, ids, vals, context=None):
727         if 'hours' in vals and (not vals['hours']):
728             vals['hours'] = 0.00
729         if 'hours' in vals:
730             for work in self.browse(cr, uid, ids, context=context):
731                 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))
732         return super(project_work,self).write(cr, uid, ids, vals, context)
733
734     def unlink(self, cr, uid, ids, *args, **kwargs):
735         for work in self.browse(cr, uid, ids):
736             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
737         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
738 project_work()
739
740 class account_analytic_account(osv.osv):
741
742     _inherit = 'account.analytic.account'
743     _description = 'Analytic Account'
744
745     def create(self, cr, uid, vals, context=None):
746         if context is None:
747             context = {}
748         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
749             vals['child_ids'] = []
750         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
751
752 account_analytic_account()
753
754 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: