9b588e886594c74435158a16472ed34ef7a2f520
[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 from resource.faces import task as Task
29
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 #    _name = 'project.project'
33 #project_project()
34
35 class project_task_type(osv.osv):
36     _name = 'project.task.type'
37     _description = 'Task Stage'
38     _order = 'sequence'
39     _columns = {
40         'name': fields.char('Stage Name', required=True, size=64, translate=True),
41         'description': fields.text('Description'),
42         'sequence': fields.integer('Sequence'),
43         'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44         'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
45     }
46     _defaults = {
47         'sequence': 1
48     }
49     _order = 'sequence'
50 project_task_type()
51
52 class project(osv.osv):
53     _name = "project.project"
54     _description = "Project"
55     _inherits = {'account.analytic.account': "analytic_account_id"}
56
57     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
58         if user == 1:
59             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
60         if context and context.get('user_preference'):
61                 cr.execute("""SELECT project.id FROM project_project project
62                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
63                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
64                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
65                 return [(r[0]) for r in cr.fetchall()]
66         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
67             context=context, count=count)
68
69     def _complete_name(self, cr, uid, ids, name, args, context=None):
70         res = {}
71         for m in self.browse(cr, uid, ids, context=context):
72             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
73         return res
74
75     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
76         partner_obj = self.pool.get('res.partner')
77         if not part:
78             return {'value':{'contact_id': False}}
79         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
80         val = {'contact_id': addr['contact']}
81         if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82             pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83             pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84             val['pricelist_id'] = pricelist_id
85         return {'value': val}
86
87     def _get_childs(self, cr, uid, ids, childs,context=None):
88         cr.execute("""SELECT id FROM project_project WHERE analytic_account_id IN (
89                         SELECT id FROM account_analytic_account WHERE parent_id = (
90                                 SELECT  analytic_account_id FROM project_project project
91                                         LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
92                                         WHERE project.id = %s
93                             )
94                         )"""%(ids)) 
95         for child in cr.fetchall():
96             if child[0] not in childs: childs.append(child[0])
97             self._get_childs( cr, uid, child[0], childs,context)
98         return childs
99
100
101     def _get_parents(self, cr, uid, ids, parents,context=None):
102         for project in self.read(cr, uid, ids, ['id', 'parent_id'],context):
103             if project.get('parent_id'):
104                 cr.execute('''SELECT id FROM project_project WHERE analytic_account_id = '%s' '''%project.get('parent_id')[0])
105                 for child in cr.fetchall():
106                     if child[0] not in parents: parents.append(child[0])
107                     child_rec= self.read(cr, uid, child[0], ['id', 'parent_id'],context)
108                     if child_rec.get('parent_id'):
109                         parents = self._get_parents(cr, uid, [child[0]], parents,context)
110         return parents
111
112     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
113         res = {}.fromkeys(ids, 0.0)
114         if not ids:
115             return res
116         parents = self._get_parents(cr, uid, ids, ids,context)
117         cr.execute('''SELECT
118                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
119             FROM
120                 project_task
121             WHERE
122                 project_id in %s AND
123                 state<>'cancelled'
124             GROUP BY
125                 project_id''', (tuple(ids),))
126         progress = dict(map(lambda x: (x[0], (x[1] or 0.0 ,x[2] or 0.0 ,x[3] or 0.0 ,x[4] or 0.0)), cr.fetchall()))
127         for project in self.browse(cr, uid, parents, context=context):
128             childs = []
129             childs = self._get_childs(cr, uid, project.id, childs,context)
130             s = progress.get(project.id, (0.0,0.0,0.0,0.0))
131             res[project.id] = {
132                 'planned_hours': s[0],
133                 'effective_hours': s[2],
134                 'total_hours': s[1],
135                 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
136             }
137             
138             if childs:
139                 cr.execute('''SELECT
140                     project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
141                 FROM
142                     project_task
143                 WHERE
144                     project_id in %s AND
145                     state<>'cancelled'
146                 GROUP BY
147                     project_id''', (tuple(childs),))
148                 child_progress = dict(map(lambda x: (x[0], (x[1] or 0.0 ,x[2] or 0.0 ,x[3] or 0.0 ,x[4] or 0.0)), cr.fetchall()))
149                 planned_hours, effective_hours, total_hours, rnd= 0.0, 0.0,0.0, 0.0
150                 for child in childs:
151                     ch_vals = child_progress.get(child, (0.0,0.0,0.0,0.0))
152                     planned_hours, effective_hours, total_hours = planned_hours+ch_vals[0], effective_hours+ch_vals[2] , total_hours+ch_vals[1]
153                 if res.get(project.id).get('planned_hours')+ planned_hours > 0:
154                     rnd = round(( res.get(project.id).get('effective_hours')+effective_hours)/(res.get(project.id).get('planned_hours')+ planned_hours)*100,2) or 0.0
155                 res[project.id] = {
156                     'planned_hours': res.get(project.id).get('planned_hours')+ planned_hours,
157                     'effective_hours': res.get(project.id).get('effective_hours')+ effective_hours,
158                     'total_hours': res.get(project.id).get('total_hours')+ total_hours,
159                     'progress_rate':  rnd
160                 }
161         return res
162
163     def _get_project_task(self, cr, uid, ids, context=None):
164         result = {}
165         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
166             if task.project_id: 
167                 result[task.project_id.id] = True
168                 if task.project_id.parent_id:
169                     cr.execute('''SELECT id FROM project_project WHERE analytic_account_id = '%s' '''%task.project_id.parent_id.id)
170                     for parent in cr.fetchall():
171                         result[parent[0]] = True
172         return result.keys()
173
174     def _get_project_work(self, cr, uid, ids, context=None):
175         result = {}
176         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
177             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
178         return result.keys()
179
180     def unlink(self, cr, uid, ids, *args, **kwargs):
181         for proj in self.browse(cr, uid, ids):
182             if proj.tasks:
183                 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
184         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
185
186     _columns = {
187         'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
188         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
189         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
190         '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),
191         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
192         '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)]}),
193
194         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
195             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)]}),
196         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
197         'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
198             store = {
199                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks', 'parent_id', 'child_ids'], 10),
200                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
201             }),
202         'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
203         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
204         'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
205             store = {
206                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks','parent_id', 'child_ids'], 10),
207                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
208             }),
209         'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
210         '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)]}),
211         '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)]}),
212         '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)]}),
213         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
214      }
215     def _get_type_common(self, cr, uid, context):
216         ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
217         return ids
218
219     _order = "sequence"
220     _defaults = {
221         'active': True,
222         'priority': 1,
223         'sequence': 10,
224         'type_ids': _get_type_common
225     }
226
227     # TODO: Why not using a SQL contraints ?
228     def _check_dates(self, cr, uid, ids, context=None):
229         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
230             if leave['date_start'] and leave['date']:
231                 if leave['date_start'] > leave['date']:
232                     return False
233         return True
234
235     _constraints = [
236         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
237     ]
238
239     def set_template(self, cr, uid, ids, context=None):
240         res = self.setActive(cr, uid, ids, value=False, context=context)
241         return res
242
243     def set_done(self, cr, uid, ids, context=None):
244         task_obj = self.pool.get('project.task')
245         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
246         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
247         self.write(cr, uid, ids, {'state':'close'}, context=context)
248         for (id, name) in self.name_get(cr, uid, ids):
249             message = _("The project '%s' has been closed.") % name
250             self.log(cr, uid, id, message)
251         return True
252
253     def set_cancel(self, cr, uid, ids, context=None):
254         task_obj = self.pool.get('project.task')
255         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
256         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
257         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
258         return True
259
260     def set_pending(self, cr, uid, ids, context=None):
261         self.write(cr, uid, ids, {'state':'pending'}, context=context)
262         return True
263
264     def set_open(self, cr, uid, ids, context=None):
265         self.write(cr, uid, ids, {'state':'open'}, context=context)
266         return True
267
268     def reset_project(self, cr, uid, ids, context=None):
269         res = self.setActive(cr, uid, ids, value=True, context=context)
270         for (id, name) in self.name_get(cr, uid, ids):
271             message = _("The project '%s' has been opened.") % name
272             self.log(cr, uid, id, message)
273         return res
274
275     def copy(self, cr, uid, id, default={}, context=None):
276         if context is None:
277             context = {}
278
279         default = default or {}
280         context['active_test'] = False
281         default['state'] = 'open'
282         proj = self.browse(cr, uid, id, context=context)
283         if not default.get('name', False):
284             default['name'] = proj.name + _(' (copy)')
285
286         res = super(project, self).copy(cr, uid, id, default, context)
287         return res
288
289
290     def template_copy(self, cr, uid, id, default={}, context=None):
291         task_obj = self.pool.get('project.task')
292         proj = self.browse(cr, uid, id, context=context)
293
294         default['tasks'] = [] #avoid to copy all the task automaticly
295         res = self.copy(cr, uid, id, default=default, context=context)
296
297         #copy all the task manually
298         map_task_id = {}
299         for task in proj.tasks:
300             map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
301
302         self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
303         task_obj.duplicate_task(cr, uid, map_task_id, context=context)
304
305         return res
306
307     def duplicate_template(self, cr, uid, ids, context=None):
308         if context is None:
309             context = {}
310         data_obj = self.pool.get('ir.model.data')
311         result = []
312         for proj in self.browse(cr, uid, ids, context=context):
313             parent_id = context.get('parent_id', False)
314             context.update({'analytic_project_copy': True})
315             new_date_start = time.strftime('%Y-%m-%d')
316             new_date_end = False
317             if proj.date_start and proj.date:
318                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
319                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
320                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
321             context.update({'copy':True})
322             new_id = self.template_copy(cr, uid, proj.id, default = {
323                                     'name': proj.name +_(' (copy)'),
324                                     'state':'open',
325                                     'date_start':new_date_start,
326                                     'date':new_date_end,
327                                     'parent_id':parent_id}, context=context)
328             result.append(new_id)
329
330             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
331             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
332             if child_ids:
333                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
334
335         if result and len(result):
336             res_id = result[0]
337             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
338             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
339             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
340             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
341             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
342             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
343             return {
344                 'name': _('Projects'),
345                 'view_type': 'form',
346                 'view_mode': 'form,tree',
347                 'res_model': 'project.project',
348                 'view_id': False,
349                 'res_id': res_id,
350                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
351                 'type': 'ir.actions.act_window',
352                 'search_view_id': search_view['res_id'],
353                 'nodestroy': True
354             }
355
356     # set active value for a project, its sub projects and its tasks
357     def setActive(self, cr, uid, ids, value=True, context=None):
358         task_obj = self.pool.get('project.task')
359         for proj in self.browse(cr, uid, ids, context=None):
360             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
361             cr.execute('select id from project_task where project_id=%s', (proj.id,))
362             tasks_id = [x[0] for x in cr.fetchall()]
363             if tasks_id:
364                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
365             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
366             if child_ids:
367                 self.setActive(cr, uid, child_ids, value, context=None)
368         return True
369
370     def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
371         context = context or {}
372         if type(ids) in (long, int,):
373             ids = [ids]
374         projects = self.browse(cr, uid, ids, context=context)
375
376         for project in projects:
377             if (not project.members) and force_members:
378                 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
379
380         resource_pool = self.pool.get('resource.resource')
381
382         result = "from resource.faces import *\n"
383         result += "import datetime\n"
384         for project in self.browse(cr, uid, ids, context=context):
385             u_ids = [i.id for i in project.members]
386             if project.user_id and (project.user_id.id not in u_ids):
387                 u_ids.append(project.user_id.id)
388             for task in project.tasks:
389                 if task.state in ('done','cancelled'):
390                     continue
391                 if task.user_id and (task.user_id.id not in u_ids):
392                     u_ids.append(task.user_id.id)
393             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
394             resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
395             for key, vals in resource_objs.items():
396                 result +='''
397 class User_%s(Resource):
398     efficiency = %s
399 ''' % (key,  vals.get('efficiency', False))
400
401         result += '''
402 def Project():
403         '''
404         return result
405
406     def _schedule_project(self, cr, uid, project, context=None):
407         resource_pool = self.pool.get('resource.resource')
408         calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
409         working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
410         # TODO: check if we need working_..., default values are ok.
411         puids = [x.id for x in project.members]
412         if project.user_id:
413             puids.append(project.user_id.id)
414         result = """
415   def Project_%d():
416     start = \'%s\'
417     working_days = %s
418     resource = %s
419 """       % (
420             project.id, 
421             project.date_start, working_days,
422             '|'.join(['User_'+str(x) for x in puids])
423         )
424         vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
425         if vacation:
426             result+= """
427     vacation = %s
428 """ %   ( vacation, )
429         return result
430
431     #TODO: DO Resource allocation and compute availability
432     def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
433         if context ==  None:
434             context = {}
435         allocation = {}
436         return allocation
437
438     def schedule_tasks(self, cr, uid, ids, context=None):
439         context = context or {}
440         if type(ids) in (long, int,):
441             ids = [ids]
442         projects = self.browse(cr, uid, ids, context=context)
443         result = self._schedule_header(cr, uid, ids, False, context=context)
444         for project in projects:
445             result += self._schedule_project(cr, uid, project, context=context)
446             result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
447
448         local_dict = {}
449         exec result in local_dict
450         projects_gantt = Task.BalancedProject(local_dict['Project'])
451
452         for project in projects:
453             project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
454             for task in project.tasks:
455                 if task.state in ('done','cancelled'):
456                     continue
457
458                 p = getattr(project_gantt, 'Task_%d' % (task.id,))
459
460                 self.pool.get('project.task').write(cr, uid, [task.id], {
461                     'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
462                     'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
463                 }, context=context)
464                 if (not task.user_id) and (p.booked_resource):
465                     self.pool.get('project.task').write(cr, uid, [task.id], {
466                         'user_id': int(p.booked_resource[0].name[5:]),
467                     }, context=context)
468         return True
469 project()
470
471 class users(osv.osv):
472     _inherit = 'res.users'
473     _columns = {
474         'context_project_id': fields.many2one('project.project', 'Project')
475     }
476 users()
477
478 class task(osv.osv):
479     _name = "project.task"
480     _description = "Task"
481     _log_create = True
482     _date_name = "date_start"
483
484
485     def _resolve_project_id_from_context(self, cr, uid, context=None):
486         """Return ID of project based on the value of 'project_id'
487            context key, or None if it cannot be resolved to a single project.
488         """
489         if context is None: context = {}
490         if type(context.get('project_id')) in (int, long):
491             project_id = context['project_id']
492             return project_id
493         if isinstance(context.get('project_id'), basestring):
494             project_name = context['project_id']
495             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
496             if len(project_ids) == 1:
497                 return project_ids[0][0]
498
499     def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
500         stage_obj = self.pool.get('project.task.type')
501         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
502         order = stage_obj._order
503         access_rights_uid = access_rights_uid or uid
504         if read_group_order == 'type_id desc':
505             # lame way to allow reverting search, should just work in the trivial case
506             order = '%s desc' % order
507         if project_id:
508             domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
509         else:
510             domain = ['|', ('id','in',ids), ('project_default','=',1)]
511         stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
512         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
513         # restore order of the search
514         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
515         return result
516
517     def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
518         res_users = self.pool.get('res.users')
519         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
520         access_rights_uid = access_rights_uid or uid
521         if project_id:
522             ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
523             order = res_users._order
524             # lame way to allow reverting search, should just work in the trivial case
525             if read_group_order == 'user_id desc':
526                 order = '%s desc' % order
527             # de-duplicate and apply search order
528             ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
529         result = res_users.name_get(cr, access_rights_uid, ids, context=context)
530         # restore order of the search
531         result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
532         return result
533
534     _group_by_full = {
535         'type_id': _read_group_type_id,
536         'user_id': _read_group_user_id
537     }
538
539
540     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
541         obj_project = self.pool.get('project.project')
542         for domain in args:
543             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
544                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
545                 if id and isinstance(id, (long, int)):
546                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
547                         args.append(('active', '=', False))
548         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
549
550     def _str_get(self, task, level=0, border='***', context=None):
551         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'+ \
552             border[0]+' '+(task.name or '')+'\n'+ \
553             (task.description or '')+'\n\n'
554
555     # Compute: effective_hours, total_hours, progress
556     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
557         res = {}
558         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
559         hours = dict(cr.fetchall())
560         for task in self.browse(cr, uid, ids, context=context):
561             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)}
562             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
563             res[task.id]['progress'] = 0.0
564             if (task.remaining_hours + hours.get(task.id, 0.0)):
565                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
566             if task.state in ('done','cancelled'):
567                 res[task.id]['progress'] = 100.0
568         return res
569
570
571     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
572         if remaining and not planned:
573             return {'value':{'planned_hours': remaining}}
574         return {}
575
576     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
577         return {'value':{'remaining_hours': planned - effective}}
578
579     def onchange_project(self, cr, uid, id, project_id):
580         if not project_id:
581             return {}
582         data = self.pool.get('project.project').browse(cr, uid, [project_id])
583         partner_id=data and data[0].parent_id.partner_id
584         if partner_id:
585             return {'value':{'partner_id':partner_id.id}}
586         return {}
587
588     def _default_project(self, cr, uid, context=None):
589         if context is None:
590             context = {}
591         if 'project_id' in context and context['project_id']:
592             return int(context['project_id'])
593         return False
594
595     def duplicate_task(self, cr, uid, map_ids, context=None):
596         for new in map_ids.values():
597             task = self.browse(cr, uid, new, context)
598             child_ids = [ ch.id for ch in task.child_ids]
599             if task.child_ids:
600                 for child in task.child_ids:
601                     if child.id in map_ids.keys():
602                         child_ids.remove(child.id)
603                         child_ids.append(map_ids[child.id])
604
605             parent_ids = [ ch.id for ch in task.parent_ids]
606             if task.parent_ids:
607                 for parent in task.parent_ids:
608                     if parent.id in map_ids.keys():
609                         parent_ids.remove(parent.id)
610                         parent_ids.append(map_ids[parent.id])
611             #FIXME why there is already the copy and the old one
612             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
613
614     def copy_data(self, cr, uid, id, default={}, context=None):
615         default = default or {}
616         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
617         if not default.get('remaining_hours', False):
618             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
619         default['active'] = True
620         default['type_id'] = False
621         if not default.get('name', False):
622             default['name'] = self.browse(cr, uid, id, context=context).name or ''
623             if not context.get('copy',False):
624                 new_name = _("%s (copy)")%default.get('name','')
625                 default.update({'name':new_name})
626         return super(task, self).copy_data(cr, uid, id, default, context)
627
628
629     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
630         res = {}
631         for task in self.browse(cr, uid, ids, context=context):
632             res[task.id] = True
633             if task.project_id:
634                 if task.project_id.active == False or task.project_id.state == 'template':
635                     res[task.id] = False
636         return res
637
638     def _get_task(self, cr, uid, ids, context=None):
639         result = {}
640         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
641             if work.task_id: result[work.task_id.id] = True
642         return result.keys()
643
644     _columns = {
645         'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
646         'name': fields.char('Task Summary', size=128, required=True),
647         'description': fields.text('Description'),
648         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
649         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
650         'type_id': fields.many2one('project.task.type', 'Stage'),
651         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
652                                   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.\
653                                   \n If the task is over, the states is set to \'Done\'.'),
654         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
655                                          help="A task's kanban state indicates special situations affecting it:\n"
656                                               " * Normal is the default situation\n"
657                                               " * Blocked indicates something is preventing the progress of this task\n"
658                                               " * Ready To Pull indicates the task is ready to be pulled to the next stage",
659                                          readonly=True, required=False),
660         'create_date': fields.datetime('Create Date', readonly=True,select=True),
661         'date_start': fields.datetime('Starting Date',select=True),
662         'date_end': fields.datetime('Ending Date',select=True),
663         'date_deadline': fields.date('Deadline',select=True),
664         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
665         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
666         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
667         'notes': fields.text('Notes'),
668         '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.'),
669         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
670             store = {
671                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
672                 'project.task.work': (_get_task, ['hours'], 10),
673             }),
674         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
675         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
676             store = {
677                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
678                 'project.task.work': (_get_task, ['hours'], 10),
679             }),
680         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
681             store = {
682                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
683                 'project.task.work': (_get_task, ['hours'], 10),
684             }),
685         'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
686             store = {
687                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
688                 'project.task.work': (_get_task, ['hours'], 10),
689             }),
690         'user_id': fields.many2one('res.users', 'Assigned to'),
691         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
692         'partner_id': fields.many2one('res.partner', 'Partner'),
693         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
694         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
695         'company_id': fields.many2one('res.company', 'Company'),
696         'id': fields.integer('ID', readonly=True),
697         'color': fields.integer('Color Index'),
698         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
699     }
700
701     _defaults = {
702         'state': 'draft',
703         'kanban_state': 'normal',
704         'priority': '2',
705         'progress': 0,
706         'sequence': 10,
707         'active': True,
708         'project_id': _default_project,
709         'user_id': lambda obj, cr, uid, context: uid,
710         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
711     }
712
713     _order = "priority, sequence, date_start, name, id"
714
715     def set_priority(self, cr, uid, ids, priority):
716         """Set task priority
717         """
718         return self.write(cr, uid, ids, {'priority' : priority})
719
720     def set_high_priority(self, cr, uid, ids, *args):
721         """Set task priority to high
722         """
723         return self.set_priority(cr, uid, ids, '1')
724
725     def set_normal_priority(self, cr, uid, ids, *args):
726         """Set task priority to normal
727         """
728         return self.set_priority(cr, uid, ids, '3')
729
730     def _check_recursion(self, cr, uid, ids, context=None):
731         for id in ids:
732             visited_branch = set()
733             visited_node = set()
734             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
735             if not res:
736                 return False
737
738         return True
739
740     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
741         if id in visited_branch: #Cycle
742             return False
743
744         if id in visited_node: #Already tested don't work one more time for nothing
745             return True
746
747         visited_branch.add(id)
748         visited_node.add(id)
749
750         #visit child using DFS
751         task = self.browse(cr, uid, id, context=context)
752         for child in task.child_ids:
753             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
754             if not res:
755                 return False
756
757         visited_branch.remove(id)
758         return True
759
760     def _check_dates(self, cr, uid, ids, context=None):
761         if context == None:
762             context = {}
763         obj_task = self.browse(cr, uid, ids[0], context=context)
764         start = obj_task.date_start or False
765         end = obj_task.date_end or False
766         if start and end :
767             if start > end:
768                 return False
769         return True
770
771     _constraints = [
772         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
773         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
774     ]
775     #
776     # Override view according to the company definition
777     #
778     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
779         users_obj = self.pool.get('res.users')
780
781         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
782         # this should be safe (no context passed to avoid side-effects)
783         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
784         tm = obj_tm and obj_tm.name or 'Hours'
785
786         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
787
788         if tm in ['Hours','Hour']:
789             return res
790
791         eview = etree.fromstring(res['arch'])
792
793         def _check_rec(eview):
794             if eview.attrib.get('widget','') == 'float_time':
795                 eview.set('widget','float')
796             for child in eview:
797                 _check_rec(child)
798             return True
799
800         _check_rec(eview)
801
802         res['arch'] = etree.tostring(eview)
803
804         for f in res['fields']:
805             if 'Hours' in res['fields'][f]['string']:
806                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
807         return res
808
809     def _check_child_task(self, cr, uid, ids, context=None):
810         if context == None:
811             context = {}
812         tasks = self.browse(cr, uid, ids, context=context)
813         for task in tasks:
814             if task.child_ids:
815                 for child in task.child_ids:
816                     if child.state in ['draft', 'open', 'pending']:
817                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
818         return True
819
820     def action_close(self, cr, uid, ids, context=None):
821         # This action open wizard to send email to partner or project manager after close task.
822         if context == None:
823             context = {}
824         task_id = len(ids) and ids[0] or False
825         self._check_child_task(cr, uid, ids, context=context)
826         if not task_id: return False
827         task = self.browse(cr, uid, task_id, context=context)
828         project = task.project_id
829         res = self.do_close(cr, uid, [task_id], context=context)
830         if project.warn_manager or project.warn_customer:
831             return {
832                 'name': _('Send Email after close task'),
833                 'view_type': 'form',
834                 'view_mode': 'form',
835                 'res_model': 'mail.compose.message',
836                 'type': 'ir.actions.act_window',
837                 'target': 'new',
838                 'nodestroy': True,
839                 'context': {'active_id': task.id,
840                             'active_model': 'project.task'}
841            }
842         return res
843
844     def do_close(self, cr, uid, ids, context={}):
845         """
846         Close Task
847         """
848         request = self.pool.get('res.request')
849         if not isinstance(ids,list): ids = [ids]
850         for task in self.browse(cr, uid, ids, context=context):
851             vals = {}
852             project = task.project_id
853             if project:
854                 # Send request to project manager
855                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
856                     request.create(cr, uid, {
857                         'name': _("Task '%s' closed") % task.name,
858                         'state': 'waiting',
859                         'act_from': uid,
860                         'act_to': project.user_id.id,
861                         'ref_partner_id': task.partner_id.id,
862                         'ref_doc1': 'project.task,%d'% (task.id,),
863                         'ref_doc2': 'project.project,%d'% (project.id,),
864                     }, context=context)
865
866             for parent_id in task.parent_ids:
867                 if parent_id.state in ('pending','draft'):
868                     reopen = True
869                     for child in parent_id.child_ids:
870                         if child.id != task.id and child.state not in ('done','cancelled'):
871                             reopen = False
872                     if reopen:
873                         self.do_reopen(cr, uid, [parent_id.id], context=context)
874             vals.update({'state': 'done'})
875             vals.update({'remaining_hours': 0.0})
876             if not task.date_end:
877                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
878             self.write(cr, uid, [task.id],vals, context=context)
879             message = _("The task '%s' is done") % (task.name,)
880             self.log(cr, uid, task.id, message)
881         return True
882
883     def do_reopen(self, cr, uid, ids, context=None):
884         request = self.pool.get('res.request')
885
886         for task in self.browse(cr, uid, ids, context=context):
887             project = task.project_id
888             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
889                 request.create(cr, uid, {
890                     'name': _("Task '%s' set in progress") % task.name,
891                     'state': 'waiting',
892                     'act_from': uid,
893                     'act_to': project.user_id.id,
894                     'ref_partner_id': task.partner_id.id,
895                     'ref_doc1': 'project.task,%d' % task.id,
896                     'ref_doc2': 'project.project,%d' % project.id,
897                 }, context=context)
898
899             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
900         return True
901
902     def do_cancel(self, cr, uid, ids, context={}):
903         request = self.pool.get('res.request')
904         tasks = self.browse(cr, uid, ids, context=context)
905         self._check_child_task(cr, uid, ids, context=context)
906         for task in tasks:
907             project = task.project_id
908             if project.warn_manager and project.user_id and (project.user_id.id != uid):
909                 request.create(cr, uid, {
910                     'name': _("Task '%s' cancelled") % task.name,
911                     'state': 'waiting',
912                     'act_from': uid,
913                     'act_to': project.user_id.id,
914                     'ref_partner_id': task.partner_id.id,
915                     'ref_doc1': 'project.task,%d' % task.id,
916                     'ref_doc2': 'project.project,%d' % project.id,
917                 }, context=context)
918             message = _("The task '%s' is cancelled.") % (task.name,)
919             self.log(cr, uid, task.id, message)
920             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
921         return True
922
923     def do_open(self, cr, uid, ids, context={}):
924         if not isinstance(ids,list): ids = [ids]
925         tasks= self.browse(cr, uid, ids, context=context)
926         for t in tasks:
927             data = {'state': 'open'}
928             if not t.date_start:
929                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
930             self.write(cr, uid, [t.id], data, context=context)
931             message = _("The task '%s' is opened.") % (t.name,)
932             self.log(cr, uid, t.id, message)
933         return True
934
935     def do_draft(self, cr, uid, ids, context={}):
936         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
937         return True
938
939     def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
940         """
941         Delegate Task to another users.
942         """
943         assert delegate_data['user_id'], _("Delegated User should be specified")
944         delegrated_tasks = {}
945         for task in self.browse(cr, uid, ids, context=context):
946             delegrated_task_id = self.copy(cr, uid, task.id, {
947                 'name': delegate_data['name'],
948                 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
949                 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
950                 'planned_hours': delegate_data['planned_hours'] or 0.0,
951                 'parent_ids': [(6, 0, [task.id])],
952                 'state': 'draft',
953                 'description': delegate_data['new_task_description'] or '',
954                 'child_ids': [],
955                 'work_ids': []
956             }, context=context)
957             newname = delegate_data['prefix'] or ''
958             task.write({
959                 'remaining_hours': delegate_data['planned_hours_me'],
960                 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
961                 'name': newname,
962             }, context=context)
963             if delegate_data['state'] == 'pending':
964                 self.do_pending(cr, uid, task.id, context=context)
965             elif delegate_data['state'] == 'done':
966                 self.do_close(cr, uid, task.id, context=context)
967             
968             message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
969             self.log(cr, uid, task.id, message)
970             delegrated_tasks[task.id] = delegrated_task_id
971         return delegrated_tasks
972
973     def do_pending(self, cr, uid, ids, context={}):
974         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
975         for (id, name) in self.name_get(cr, uid, ids):
976             message = _("The task '%s' is pending.") % name
977             self.log(cr, uid, id, message)
978         return True
979
980     def set_remaining_time_1(self, cr, uid, ids, context=None):
981         self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
982         return True
983
984     def set_remaining_time_2(self, cr, uid, ids, context=None):
985         self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
986         return True
987
988     def set_remaining_time_5(self, cr, uid, ids, context=None):
989         self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
990         return True
991
992     def set_remaining_time_10(self, cr, uid, ids, context=None):
993         self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
994         return True
995
996     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
997         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
998
999     def set_kanban_state_normal(self, cr, uid, ids, context=None):
1000         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1001
1002     def set_kanban_state_done(self, cr, uid, ids, context=None):
1003         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1004
1005     def _change_type(self, cr, uid, ids, next, *args):
1006         """
1007             go to the next stage
1008             if next is False, go to previous stage
1009         """
1010         for task in self.browse(cr, uid, ids):
1011             if  task.project_id.type_ids:
1012                 typeid = task.type_id.id
1013                 types_seq={}
1014                 for type in task.project_id.type_ids :
1015                     types_seq[type.id] = type.sequence
1016                 if next:
1017                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1018                 else:
1019                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1020                 sorted_types = [x[0] for x in types]
1021                 if not typeid:
1022                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1023                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1024                     index = sorted_types.index(typeid)
1025                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1026         return True
1027
1028     def next_type(self, cr, uid, ids, *args):
1029         return self._change_type(cr, uid, ids, True, *args)
1030
1031     def prev_type(self, cr, uid, ids, *args):
1032         return self._change_type(cr, uid, ids, False, *args)
1033
1034     # Overridden to reset the kanban_state to normal whenever
1035     # the stage (type_id) of the task changes.
1036     def write(self, cr, uid, ids, vals, context=None):
1037         if isinstance(ids, (int, long)):
1038             ids = [ids]
1039         if vals and not 'kanban_state' in vals and 'type_id' in vals:
1040             new_stage = vals.get('type_id')
1041             vals_reset_kstate = dict(vals, kanban_state='normal')
1042             for t in self.browse(cr, uid, ids, context=context):
1043                 write_vals = vals_reset_kstate if t.type_id != new_stage else vals 
1044                 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1045             return True
1046         return super(task,self).write(cr, uid, ids, vals, context=context)
1047
1048     def unlink(self, cr, uid, ids, context=None):
1049         if context == None:
1050             context = {}
1051         self._check_child_task(cr, uid, ids, context=context)
1052         res = super(task, self).unlink(cr, uid, ids, context)
1053         return res
1054
1055     def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1056         context = context or {}
1057         result = ""
1058         ident = ' '*ident
1059         for task in tasks:
1060             if task.state in ('done','cancelled'):
1061                 continue
1062             result += '''
1063 %sdef Task_%s():
1064 %s  todo = \"%.2fH\"
1065 %s  effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1066             start = []
1067             for t2 in task.parent_ids:
1068                 start.append("up.Task_%s.end" % (t2.id,))
1069             if start:
1070                 result += '''
1071 %s  start = max(%s)
1072 ''' % (ident,','.join(start))
1073
1074             if task.user_id:
1075                 result += '''
1076 %s  resource = %s
1077 ''' % (ident, 'User_'+str(task.user_id.id))
1078
1079         result += "\n"
1080         return result
1081
1082 task()
1083
1084 class project_work(osv.osv):
1085     _name = "project.task.work"
1086     _description = "Project Task Work"
1087     _columns = {
1088         'name': fields.char('Work summary', size=128),
1089         'date': fields.datetime('Date', select="1"),
1090         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1091         'hours': fields.float('Time Spent'),
1092         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1093         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1094     }
1095
1096     _defaults = {
1097         'user_id': lambda obj, cr, uid, context: uid,
1098         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1099     }
1100
1101     _order = "date desc"
1102     def create(self, cr, uid, vals, *args, **kwargs):
1103         if 'hours' in vals and (not vals['hours']):
1104             vals['hours'] = 0.00
1105         if 'task_id' in vals:
1106             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1107         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1108
1109     def write(self, cr, uid, ids, vals, context=None):
1110         if 'hours' in vals and (not vals['hours']):
1111             vals['hours'] = 0.00
1112         if 'hours' in vals:
1113             for work in self.browse(cr, uid, ids, context=context):
1114                 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))
1115         return super(project_work,self).write(cr, uid, ids, vals, context)
1116
1117     def unlink(self, cr, uid, ids, *args, **kwargs):
1118         for work in self.browse(cr, uid, ids):
1119             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1120         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1121 project_work()
1122
1123 class account_analytic_account(osv.osv):
1124
1125     _inherit = 'account.analytic.account'
1126     _description = 'Analytic Account'
1127
1128     def create(self, cr, uid, vals, context=None):
1129         if context is None:
1130             context = {}
1131         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1132             vals['child_ids'] = []
1133         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1134     
1135     def unlink(self, cr, uid, ids, *args, **kwargs):
1136         project_obj = self.pool.get('project.project')
1137         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1138         if analytic_ids:
1139             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1140         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1141
1142 account_analytic_account()
1143
1144 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: