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