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