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