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