[IMP]:set start_date to current date when starting the project.task and timebox field...
[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 date, datetime
25 from tools.translate import _
26 from osv import fields, osv
27
28 class project_task_type(osv.osv):
29     _name = 'project.task.type'
30     _description = 'Task Stage'
31     _columns = {
32         'name': fields.char('Stage Name', required=True, size=64, translate=True),
33         'description': fields.text('Description'),
34         'sequence': fields.integer('Sequence'),
35     }
36     _order = 'sequence'
37
38     _defaults = {
39         'sequence': 1
40     }
41     
42 project_task_type()
43
44 class project(osv.osv):
45     _name = "project.project"
46     _description = "Project"
47     _inherits = {'account.analytic.account':"category_id"}
48
49     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
50         if user == 1:
51             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
52         if context and context.has_key('user_prefence') and context['user_prefence']:
53                 cr.execute("""SELECT project.id FROM project_project project
54                            LEFT JOIN account_analytic_account account ON account.id = project.category_id
55                            LEFT JOIN project_user_rel rel ON rel.project_id = project.category_id
56                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
57                 res = cr.fetchall()
58                 return [(r[0]) for r in res]
59         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
60             context=context, count=count)
61
62     def _complete_name(self, cr, uid, ids, name, args, context=None):
63         res = {}
64         for m in self.browse(cr, uid, ids, context=context):
65             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
66         return res
67
68     def check_recursion(self, cursor, user, ids, parent=None):
69         return super(project, self).check_recursion(cursor, user, ids,
70                 parent=parent)
71
72     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
73         partner_obj = self.pool.get('res.partner')
74         if not part:
75             return {'value':{'contact_id': False, 'pricelist_id': False}}
76         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
77         pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
78         pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
79         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
80
81     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
82         res = {}.fromkeys(ids, 0.0)
83         progress = {}
84         if not ids:
85             return res
86         ids2 = self.search(cr, uid, [('parent_id','child_of',ids)], context=context)
87         if ids2:
88             cr.execute('''SELECT
89                     project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
90                 FROM
91                     project_task
92                 WHERE
93                     project_id IN %s AND
94                     state<>'cancelled'
95                 GROUP BY
96                     project_id''',(tuple(ids2),))
97             progress = dict(map(lambda x: (x[0], (x[1], x[2], x[3])), cr.fetchall()))
98         for project in self.browse(cr, uid, ids, context=context):
99             s = [0.0, 0.0, 0.0]
100             tocompute = [project]
101             while tocompute:
102                 p = tocompute.pop()
103                 tocompute += p.child_ids
104                 for i in range(3):
105                     s[i] += progress.get(p.id, (0.0, 0.0, 0.0))[i]
106             res[project.id] = {
107                 'planned_hours': s[0],
108                 'effective_hours': s[2],
109                 'total_hours': s[1],
110                 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
111             }
112         return res
113
114     def unlink(self, cr, uid, ids, *args, **kwargs):
115         for proj in self.browse(cr, uid, ids):
116             if proj.tasks:
117                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
118         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
119
120     _columns = {
121         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
122         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
123         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
124         'category_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."),
125         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
126         '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."),
127         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
128         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
129         '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."),
130         '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."),
131         '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."),
132         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
133         '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."),
134         '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."),
135         '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."),
136         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages'),
137         'project_escalation_id': fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.'),
138      }
139     
140     _order = "sequence"
141     
142     _defaults = {
143         'active': True,
144         'priority': 1,
145         'sequence': 10,
146         'warn_manager': True,
147     }
148     
149     def _check_dates(self, cr, uid, ids):
150          leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
151          if leave['date_start'] and leave['date']:
152              if leave['date_start'] > leave['date']:
153                  return False
154          return True
155
156     def _check_escalation(self, cr, uid, ids):
157          project_obj = self.browse(cr, uid, ids[0])
158          if project_obj.project_escalation_id:
159              if project_obj.project_escalation_id.id == project_obj.id:
160                  return False
161          return True
162
163     _constraints = [
164         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date']),
165         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
166     ]
167     
168     def set_template(self, cr, uid, ids, context=None):
169         res = self.setActive(cr, uid, ids, value=False, context=context)
170         return res
171
172     def set_done(self, cr, uid, ids, context=None):
173         task_obj = self.pool.get('project.task')
174         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
175         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
176         self.write(cr, uid, ids, {'state':'close'}, context=context)
177         for (id, name) in self.name_get(cr, uid, ids):
178             message = _('Project ') + " '" + name + "' "+ _("is Closed.")
179             self.log(cr, uid, id, message)
180         return True
181
182     def set_cancel(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', '!=', 'done')])
185         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
187         return True
188
189     def set_pending(self, cr, uid, ids, context=None):
190         self.write(cr, uid, ids, {'state':'pending'}, context=context)
191         return True
192
193     def set_open(self, cr, uid, ids, context=None):
194         self.write(cr, uid, ids, {'state':'open'}, context=context)
195         return True
196
197     def reset_project(self, cr, uid, ids, context=None):
198         res = self.setActive(cr, uid, ids, value=True, context=context)
199         for (id, name) in self.name_get(cr, uid, ids):
200             message = _('Project ') + " '" + name + "' "+ _("is Open.")
201             self.log(cr, uid, id, message)
202         return res
203
204     def copy(self, cr, uid, id, default={}, context=None):
205         if context is None:
206             context = {}
207             
208         task_obj = self.pool.get('project.task')
209         proj = self.browse(cr, uid, id, context=context)
210         default = default or {}
211         context['active_test'] = False
212         default['state'] = 'open'
213         if not default.get('name', False):
214             default['name'] = proj.name+_(' (copy)')        
215         res = super(project, self).copy(cr, uid, id, default, context)
216         
217         task_ids = task_obj.search(cr, uid, [('project_id','=', res), ('active','=',False)])
218         tasks = task_obj.browse(cr, uid, task_ids)
219         for task in tasks:
220             date_deadline = None
221             date_end = None
222             if task.date_start:
223                 ds = date(*time.strptime(task.date_start,'%Y-%m-%d %H:%M:%S')[:3])
224                 if task.date_deadline:
225                     dd = date(*time.strptime(task.date_deadline,'%Y-%m-%d')[:3])
226                     diff = dd-ds
227                     date_deadline = (datetime.now()+diff).strftime('%Y-%m-%d %H:%M:%S')
228             task_obj.write(cr, uid, task.id, {'active':True, 
229                                               'date_start':time.strftime('%Y-%m-%d %H:%M:%S'),
230                                               'date_deadline':date_deadline,
231                                               'date_end':date_end})
232         
233         ids = self.search(cr, uid, [('parent_id','child_of', [res])])
234         return res
235
236     def duplicate_template(self, cr, uid, ids, context=None):
237         if context is None:
238             context = {}
239         project_obj = self.pool.get('project.project')
240         data_obj = self.pool.get('ir.model.data')
241         task_obj = self.pool.get('project.task')
242         result = []
243         for proj in self.browse(cr, uid, ids, context=context):
244             parent_id = context.get('parent_id', False) # check me where to pass context for parent id ??
245             context.update({'analytic_project_copy': True})
246             new_id = project_obj.copy(cr, uid, proj.id, default = {
247                                     'name': proj.name +_(' (copy)'),
248                                     'state':'open',
249                                     'parent_id':parent_id}, context=context)
250             result.append(new_id)
251             cr.execute('select id from project_task where project_id=%s', (proj.id,))
252             res = cr.fetchall()
253             child_ids = self.search(cr, uid, [('parent_id','=', proj.category_id.id)], context=context)
254             parent_id = self.read(cr, uid, new_id, ['category_id'])['category_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_list')
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.id)])
289             if child_ids:
290                 self.setActive(cr, uid, child_ids, value, context=None)
291         return True
292     
293 project()
294
295 class task(osv.osv):
296     _name = "project.task"
297     _description = "Task"
298     _log_create = True
299     _date_name = "date_start"
300
301     def _str_get(self, task, level=0, border='***', context=None):
302         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'+ \
303             border[0]+' '+(task.name or '')+'\n'+ \
304             (task.description or '')+'\n\n'
305
306     # Compute: effective_hours, total_hours, progress
307     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
308         res = {}
309         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
310         hours = dict(cr.fetchall())
311         for task in self.browse(cr, uid, ids, context=context):
312             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
313             res[task.id]['progress'] = 0.0
314             if (task.remaining_hours + hours.get(task.id, 0.0)):
315                 if task.state != 'done':
316                     res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99), 2)
317
318             if task.state in ('done', 'cancel'):
319                 res[task.id]['progress'] = 100.0
320             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
321         return res
322
323     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
324         return {'value':{'remaining_hours': planned - effective}}
325
326     def _default_project(self, cr, uid, context=None):
327         if context is None:
328             context = {}
329         if 'project_id' in context and context['project_id']:
330             return int(context['project_id'])
331         return False
332
333     #_sql_constraints = [
334     #    ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
335     #]
336
337     def copy_data(self, cr, uid, id, default={}, context=None):
338         default = default or {}
339         default['work_ids'] = []
340         return super(task, self).copy_data(cr, uid, id, default, context)
341
342     def _check_dates(self, cr, uid, ids, context=None):
343         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
344         if task['date_start'] and task['date_end']:
345              if task['date_start'] > task['date_end']:
346                  return False
347         return True
348
349     _columns = {
350         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it. This is basically used for the management of templates of projects and tasks."),
351         'name': fields.char('Task Summary', size=128, required=True),
352         'description': fields.text('Description'),
353         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
354         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
355         'type': fields.many2one('project.task.type', 'Stage',),
356         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
357                                   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.\
358                                   \n If the task is over, the states is set to \'Done\'.'),
359         'create_date': fields.datetime('Create Date', readonly=True),
360         'date_start': fields.datetime('Starting Date'),
361         'date_end': fields.datetime('Ending Date'),
362         'date_deadline': fields.date('Deadline'),
363         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
364             help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
365         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
366         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
367         'notes': fields.text('Notes'),
368         'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
369         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
370         'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
371         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
372         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
373         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as difference of the time estimated by the project manager and the real time to close the task."),
374
375         'user_id': fields.many2one('res.users', 'Assigned to'),
376         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
377         'partner_id': fields.many2one('res.partner', 'Partner'),
378         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
379         'manager_id': fields.related('project_id', 'category_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
380         'company_id': fields.many2one('res.company', 'Company'),
381     }
382     
383     _defaults = {
384         'state': 'draft',
385         'priority': '2',
386         'progress': 0,
387         'sequence': 10,
388         'active': True,
389         'project_id': _default_project,
390         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
391     }
392     
393     _order = "sequence, priority, date_start, id"
394
395     _constraints = [
396         (_check_dates, 'Error! task start-date must be lower then task end-date.', ['date_start', 'date_end'])
397     ]
398     #
399     # Override view according to the company definition
400     #
401     
402     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
403         users_obj = self.pool.get('res.users')
404         obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
405         tm = obj_tm and obj_tm.name or 'Hours'
406
407         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
408
409         if tm in ['Hours','Hour']:
410             return res
411
412         eview = etree.fromstring(res['arch'])
413
414         def _check_rec(eview):
415             if eview.attrib.get('widget','') == 'float_time':
416                 eview.set('widget','float')
417             for child in eview:
418                 _check_rec(child)
419             return True
420
421         _check_rec(eview)
422
423         res['arch'] = etree.tostring(eview)
424
425         for f in res['fields']:
426             if 'Hours' in res['fields'][f]['string']:
427                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
428
429         return res
430
431     def do_close(self, cr, uid, ids, *args):
432         mail_send = False
433         mod_obj = self.pool.get('ir.model.data')
434         request = self.pool.get('res.request')
435         tasks = self.browse(cr, uid, ids)
436         task_id = ids[0]
437         cntx = {}
438         if len(args):
439             cntx = args[0]
440         for task in tasks:
441             project = task.project_id
442             if project:
443                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
444                     request.create(cr, uid, {
445                         'name': _("Task '%s' closed") % task.name,
446                         'state': 'waiting',
447                         'act_from': uid,
448                         'act_to': project.user_id.id,
449                         'ref_partner_id': task.partner_id.id,
450                         'ref_doc1': 'project.task,%d'% (task.id,),
451                         'ref_doc2': 'project.project,%d'% (project.id,),
452                     })
453                 elif project.warn_manager and cntx.get('mail_send',True):
454                     mail_send = True
455             message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
456             self.log(cr, uid, task.id, message)
457
458             for parent_id in task.parent_ids:
459                 if parent_id.state in ('pending','draft'):
460                     reopen = True
461                     for child in parent_id.child_ids:
462                         if child.id != task.id and child.state not in ('done','cancelled'):
463                             reopen = False
464                     if reopen:
465                         self.do_reopen(cr, uid, [parent_id.id])
466         if mail_send:
467             model_data_ids = mod_obj.search(cr,uid,[('model','=','ir.ui.view'),('name','=','view_project_close_task')])
468             resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'])[0]['res_id']
469             cntx.update({'task_id': task_id})
470             return {
471                 'name': _('Email Send to Customer'),
472                 'view_type': 'form',
473                 'context': cntx, # improve me
474                 'view_mode': 'tree,form',
475                 'res_model': 'close.task',
476                 'views': [(resource_id,'form')],
477                 'type': 'ir.actions.act_window',
478                 'target': 'new',
479                 'nodestroy': True
480                     }
481         else:
482             self.write(cr, uid, [task_id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
483         return False
484
485     def do_reopen(self, cr, uid, ids, *args):
486         request = self.pool.get('res.request')
487         tasks = self.browse(cr, uid, ids)
488         for task in tasks:
489             project = task.project_id
490             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
491                 request.create(cr, uid, {
492                     'name': _("Task '%s' set in progress") % task.name,
493                     'state': 'waiting',
494                     'act_from': uid,
495                     'act_to': project.user_id.id,
496                     'ref_partner_id': task.partner_id.id,
497                     'ref_doc1': 'project.task,%d' % task.id,
498                     'ref_doc2': 'project.project,%d' % project.id,
499                 })
500
501             self.write(cr, uid, [task.id], {'state': 'open'})
502         return True
503
504     def do_cancel(self, cr, uid, ids, *args):
505         request = self.pool.get('res.request')
506         tasks = self.browse(cr, uid, ids)
507         for task in tasks:
508             project = task.project_id
509             if project.warn_manager and project.user_id and (project.user_id.id != uid):
510                 request.create(cr, uid, {
511                     'name': _("Task '%s' cancelled") % task.name,
512                     'state': 'waiting',
513                     'act_from': uid,
514                     'act_to': project.user_id.id,
515                     'ref_partner_id': task.partner_id.id,
516                     'ref_doc1': 'project.task,%d' % task.id,
517                     'ref_doc2': 'project.project,%d' % project.id,
518                 })
519             message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
520             self.log(cr, uid, task.id, message)
521             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
522         return True
523
524     def do_open(self, cr, uid, ids, *args):
525         tasks= self.browse(cr,uid,ids)
526         for t in tasks:
527             self.write(cr, uid, [t.id], {'state': 'open',  'date_start': time.strftime('%Y-%m-%d %H:%M:%S'),})
528             message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
529             self.log(cr, uid, t.id, message)
530         return True
531
532     def do_draft(self, cr, uid, ids, *args):
533         self.write(cr, uid, ids, {'state': 'draft'})
534         return True
535
536
537     def do_pending(self, cr, uid, ids, *args):
538         self.write(cr, uid, ids, {'state': 'pending'})
539         for (id, name) in self.name_get(cr, uid, ids):
540             message = _('Task ') + " '" + name + "' "+ _("is Pending.")
541             self.log(cr, uid, id, message)
542         return True
543
544     def next_type(self, cr, uid, ids, *args):
545         for typ in self.browse(cr, uid, ids):
546             typeid = typ.type.id
547             types = map(lambda x:x.id, typ.project_id.type_ids or [])
548             if types:
549                 if not typeid:
550                     self.write(cr, uid, typ.id, {'type': types[0]})
551                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
552                     index = types.index(typeid)
553                     self.write(cr, uid, typ.id, {'type': types[index+1]})
554         return True
555
556     def prev_type(self, cr, uid, ids, *args):
557         for typ in self.browse(cr, uid, ids):
558             typeid = typ.type.id
559             types = map(lambda x:x.id, typ.project_id.type_ids)
560             if types:
561                 if typeid and typeid in types:
562                     index = types.index(typeid)
563                     self.write(cr, uid, typ.id, {'type': index and types[index-1] or False})
564         return True
565
566 task()
567
568 class project_work(osv.osv):
569     _name = "project.task.work"
570     _description = "Task Work"
571     _columns = {
572         'name': fields.char('Work summary', size=128),
573         'date': fields.datetime('Date'),
574         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
575         'hours': fields.float('Time Spent'),
576         'user_id': fields.many2one('res.users', 'Done by', required=True),
577         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
578     }
579     
580     _defaults = {
581         'user_id': lambda obj, cr, uid, context: uid,
582         'date': time.strftime('%Y-%m-%d %H:%M:%S')
583     }
584     
585     _order = "date desc"
586     
587     def create(self, cr, uid, vals, *args, **kwargs):
588         if 'hours' in vals and (not vals['hours']):
589             vals['hours'] = 0.00
590         if 'task_id' in vals:
591             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
592         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
593
594     def write(self, cr, uid, ids, vals, context=None):
595         if context is None:
596             context = {}
597         if 'hours' in vals and (not vals['hours']):
598             vals['hours'] = 0.00
599         if 'hours' in vals:
600             for work in self.browse(cr, uid, ids, context):
601                 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))
602         return super(project_work,self).write(cr, uid, ids, vals, context)
603
604     def unlink(self, cr, uid, ids, *args, **kwargs):
605         for work in self.browse(cr, uid, ids):
606             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
607         return super(project_work,self).unlink(cr, uid, ids, *args, **kwargs)
608     
609 project_work()
610
611 class config_compute_remaining(osv.osv_memory):
612     _name='config.compute.remaining'
613     
614     def _get_remaining(self,cr, uid, ctx):
615         task_obj = self.pool.get('project.task')
616         if 'active_id' in ctx:
617             return task_obj.browse(cr, uid, ctx['active_id'], context=ctx).remaining_hours
618         return False
619
620     _columns = {
621         'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Put here the remaining hours required to close the task."),
622     }
623     
624     def _get_analytic_account(self, cr, uid, context={}):
625         if context.get('account_id', False):
626             return context.get('account_id')
627         return False
628     
629     _defaults = {
630               'account_id' : _get_analytic_account,
631               }
632
633     _defaults = {
634         'remaining_hours': _get_remaining
635     }
636
637     def compute_hours(self, cr, uid, ids, context=None):
638         if context is None:
639             context = {}
640         task_obj = self.pool.get('project.task')
641         request = self.pool.get('res.request')
642         if 'active_id' in context:
643             remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
644             task_obj.write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
645         if context.get('button_reactivate', False):
646             tasks = task_obj.browse(cr, uid, [context['active_id']], context=context)
647             for task in tasks:
648                 project = task.project_id
649                 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
650                     request.create(cr, uid, {
651                         'name': _("Task '%s' set in progress") % task.name,
652                         'state': 'waiting',
653                         'act_from': uid,
654                         'act_to': project.user_id.id,
655                         'ref_partner_id': task.partner_id.id,
656                         'ref_doc1': 'project.task,%d' % task.id,
657                         'ref_doc2': 'project.project,%d' % project.id,
658                     })
659                 task_obj.write(cr, uid, [task.id], {'state': 'open'})
660         return {
661                 'type': 'ir.actions.act_window_close',
662          }
663
664 config_compute_remaining()
665
666 class message(osv.osv):
667     _name = "project.message"
668     _description = "Message"
669     
670     _columns = {
671         'subject': fields.char('Subject', size=128, required="True"),
672         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
673         'date': fields.date('Date', required=1),
674         'user_id': fields.many2one('res.users', 'User', required="True"),
675         'description': fields.text('Description'),
676     }
677
678     def _default_project(self, cr, uid, context=None):
679         if context is None:
680             context = {}
681         if 'project_id' in context and context['project_id']:
682             return int(context['project_id'])
683         return False
684
685     _defaults = {
686         'user_id': lambda self,cr,uid,ctx: uid,
687         'date': time.strftime('%Y-%m-%d'),
688         'project_id': _default_project
689     }
690
691 message()
692
693 class users(osv.osv):
694     _inherit = 'res.users'
695     _description = "Users"
696     _columns = {
697         'context_project_id': fields.many2one('project.project', 'Project')
698      }
699     
700 users()
701
702 class account_analytic_account(osv.osv):
703
704     _inherit = 'account.analytic.account'
705     _description = 'Analytic Account'
706
707     def create(self, cr, uid, vals, context=None):
708         if context is None:
709             context = {}
710         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
711             vals['child_ids'] = []
712         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
713
714 account_analytic_account()
715
716 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: