[MERGE] project_issue: automatic update of date_action_last field as soon as the...
[odoo/odoo.git] / addons / project_issue / project_issue.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 crm import crm
23 from datetime import datetime
24 from osv import fields,osv
25 from tools.translate import _
26 import binascii
27 import time
28 import tools
29 from crm import wizard
30
31 wizard.mail_compose_message.SUPPORTED_MODELS.append('project.issue')
32
33 class project_issue_version(osv.osv):
34     _name = "project.issue.version"
35     _order = "name desc"
36     _columns = {
37         'name': fields.char('Version Number', size=32, required=True),
38         'active': fields.boolean('Active', required=False),
39     }
40     _defaults = {
41         'active': 1,
42     }
43 project_issue_version()
44
45 class project_issue(crm.crm_case, osv.osv):
46     _name = "project.issue"
47     _description = "Project Issue"
48     _order = "priority, create_date desc"
49     _inherit = ['mail.thread']
50
51     def write(self, cr, uid, ids, vals, context=None):
52         #Update last action date everytime the user change the stage, the state or send a new email
53         logged_fields = ['type_id', 'state', 'message_ids']
54         if any([field in vals for field in logged_fields]):
55             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
56         return super(project_issue, self).write(cr, uid, ids, vals, context)
57
58     def case_open(self, cr, uid, ids, *args):
59         """
60         @param self: The object pointer
61         @param cr: the current row, from the database cursor,
62         @param uid: the current user’s ID for security checks,
63         @param ids: List of case's Ids
64         @param *args: Give Tuple Value
65         """
66
67         res = super(project_issue, self).case_open(cr, uid, ids, *args)
68         self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
69         for (id, name) in self.name_get(cr, uid, ids):
70             message = _("Issue '%s' has been opened.") % name
71             self.log(cr, uid, id, message)
72         return res
73
74     def case_close(self, cr, uid, ids, *args):
75         """
76         @param self: The object pointer
77         @param cr: the current row, from the database cursor,
78         @param uid: the current user’s ID for security checks,
79         @param ids: List of case's Ids
80         @param *args: Give Tuple Value
81         """
82
83         res = super(project_issue, self).case_close(cr, uid, ids, *args)
84         for (id, name) in self.name_get(cr, uid, ids):
85             message = _("Issue '%s' has been closed.") % name
86             self.log(cr, uid, id, message)
87         return res
88
89     def _compute_day(self, cr, uid, ids, fields, args, context=None):
90         """
91         @param cr: the current row, from the database cursor,
92         @param uid: the current user’s ID for security checks,
93         @param ids: List of Openday’s IDs
94         @return: difference between current date and log date
95         @param context: A standard dictionary for contextual values
96         """
97         cal_obj = self.pool.get('resource.calendar')
98         res_obj = self.pool.get('resource.resource')
99
100         res = {}
101         for issue in self.browse(cr, uid, ids, context=context):
102             res[issue.id] = {}
103             for field in fields:
104                 duration = 0
105                 ans = False
106                 hours = 0
107
108                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
109                 if field in ['working_hours_open','day_open']:
110                     if issue.date_open:
111                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
112                         ans = date_open - date_create
113                         date_until = issue.date_open
114                         #Calculating no. of working hours to open the issue
115                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
116                                                            date_create,
117                                                            date_open)
118                 elif field in ['working_hours_close','day_close']:
119                     if issue.date_closed:
120                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
121                         date_until = issue.date_closed
122                         ans = date_close - date_create
123                         #Calculating no. of working hours to close the issue
124                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
125                                date_create,
126                                date_close)
127                 elif field in ['days_since_creation']:
128                     if issue.create_date:
129                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
130                         res[issue.id][field] = days_since_creation.days
131                     continue
132
133                 elif field in ['inactivity_days']:
134                     res[issue.id][field] = 0
135                     if issue.date_action_last:
136                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
137                         res[issue.id][field] = inactive_days.days
138                     continue
139                 if ans:
140                     resource_id = False
141                     if issue.user_id:
142                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
143                         if resource_ids and len(resource_ids):
144                             resource_id = resource_ids[0]
145                     duration = float(ans.days)
146                     if issue.project_id and issue.project_id.resource_calendar_id:
147                         duration = float(ans.days) * 24
148
149                         new_dates = cal_obj.interval_min_get(cr, uid,
150                                                              issue.project_id.resource_calendar_id.id,
151                                                              date_create,
152                                                              duration, resource=resource_id)
153                         no_days = []
154                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
155                         for in_time, out_time in new_dates:
156                             if in_time.date not in no_days:
157                                 no_days.append(in_time.date)
158                             if out_time > date_until:
159                                 break
160                         duration = len(no_days)
161
162                 if field in ['working_hours_open','working_hours_close']:
163                     res[issue.id][field] = hours
164                 else:
165                     res[issue.id][field] = abs(float(duration))
166
167         return res
168
169     def _get_issue_task(self, cr, uid, ids, context=None):
170         issues = []
171         issue_pool = self.pool.get('project.issue')
172         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
173             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
174         return issues
175
176     def _get_issue_work(self, cr, uid, ids, context=None):
177         issues = []
178         issue_pool = self.pool.get('project.issue')
179         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
180             if work.task_id:
181                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
182         return issues
183
184     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
185         task_pool = self.pool.get('project.task')
186         res = {}
187         for issue in self.browse(cr, uid, ids, context=context):
188             progress = 0.0
189             if issue.task_id:
190                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
191             res[issue.id] = {'progress' : progress}
192         return res
193
194     _columns = {
195         'id': fields.integer('ID', readonly=True),
196         'name': fields.char('Issue', size=128, required=True),
197         'active': fields.boolean('Active', required=False),
198         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
199         'write_date': fields.datetime('Update Date', readonly=True),
200         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
201                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
202         'date_deadline': fields.date('Deadline'),
203         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
204                         select=True, help='Sales team to which Case belongs to.\
205                              Define Responsible user and Email account for mail gateway.'),
206         'partner_id': fields.many2one('res.partner', 'Partner', select=1),
207         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
208                                  domain="[('partner_id','=',partner_id)]"),
209         'company_id': fields.many2one('res.company', 'Company'),
210         'description': fields.text('Description'),
211         'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
212                                   help='The state is set to \'Draft\', when a case is created.\
213                                   \nIf the case is in progress the state is set to \'Open\'.\
214                                   \nWhen the case is over, the state is set to \'Done\'.\
215                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
216         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
217         'email_cc': fields.char('Watchers Emails', size=256, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
218         'date_open': fields.datetime('Opened', readonly=True,select=True),
219         # Project Issue fields
220         'date_closed': fields.datetime('Closed', readonly=True,select=True),
221         'date': fields.datetime('Date'),
222         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
223         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
224         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
225         'version_id': fields.many2one('project.issue.version', 'Version'),
226         'type_id': fields.many2one ('project.task.type', 'Stages', domain="[('project_ids', '=', project_id)]"),
227         'project_id':fields.many2one('project.project', 'Project'),
228         'duration': fields.float('Duration'),
229         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
230         'day_open': fields.function(_compute_day, string='Days to Open', \
231                                 multi='compute_day', type="float", store=True),
232         'day_close': fields.function(_compute_day, string='Days to Close', \
233                                 multi='compute_day', type="float", store=True),
234         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
235         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
236                                 multi='compute_day', type="float", store=True),
237         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
238                                 multi='compute_day', type="float", store=True),
239         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
240                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
241         'color': fields.integer('Color Index'),
242         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
243         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
244         'date_action_last': fields.datetime('Last Action', readonly=1),
245         'date_action_next': fields.datetime('Next Action', readonly=1),
246         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
247             store = {
248                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
249                 'project.task': (_get_issue_task, ['progress'], 10),
250                 'project.task.work': (_get_issue_work, ['hours'], 10),
251             }),
252     }
253
254     def _get_project(self, cr, uid, context=None):
255         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
256         if user.context_project_id:
257             return user.context_project_id.id
258         return False
259
260     def on_change_project(self, cr, uid, ids, project_id, context=None):
261         return {}
262
263
264     _defaults = {
265         'active': 1,
266         'partner_id': crm.crm_case._get_default_partner,
267         'partner_address_id': crm.crm_case._get_default_partner_address,
268         'email_from': crm.crm_case._get_default_email,
269         'state': 'draft',
270         'section_id': crm.crm_case._get_section,
271         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
272         'priority': crm.AVAILABLE_PRIORITIES[2][0],
273         'project_id':_get_project,
274         'categ_id' : lambda *a: False,
275          }
276
277     def set_priority(self, cr, uid, ids, priority):
278         """Set lead priority
279         """
280         return self.write(cr, uid, ids, {'priority' : priority})
281
282     def set_high_priority(self, cr, uid, ids, *args):
283         """Set lead priority to high
284         """
285         return self.set_priority(cr, uid, ids, '1')
286
287     def set_normal_priority(self, cr, uid, ids, *args):
288         """Set lead priority to normal
289         """
290         return self.set_priority(cr, uid, ids, '3')
291
292     def convert_issue_task(self, cr, uid, ids, context=None):
293         case_obj = self.pool.get('project.issue')
294         data_obj = self.pool.get('ir.model.data')
295         task_obj = self.pool.get('project.task')
296
297
298         if context is None:
299             context = {}
300
301         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
302         res = data_obj.read(cr, uid, result, ['res_id'])
303         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
304         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
305         if id2:
306             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
307         if id3:
308             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
309
310         for bug in case_obj.browse(cr, uid, ids, context=context):
311             new_task_id = task_obj.create(cr, uid, {
312                 'name': bug.name,
313                 'partner_id': bug.partner_id.id,
314                 'description':bug.description,
315                 'date': bug.date,
316                 'project_id': bug.project_id.id,
317                 'priority': bug.priority,
318                 'user_id': bug.user_id.id,
319                 'planned_hours': 0.0,
320             })
321
322             vals = {
323                 'task_id': new_task_id,
324                 'state':'pending'
325             }
326             case_obj.write(cr, uid, [bug.id], vals)
327
328         return  {
329             'name': _('Tasks'),
330             'view_type': 'form',
331             'view_mode': 'form,tree',
332             'res_model': 'project.task',
333             'res_id': int(new_task_id),
334             'view_id': False,
335             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
336             'type': 'ir.actions.act_window',
337             'search_view_id': res['res_id'],
338             'nodestroy': True
339         }
340
341
342     def _convert(self, cr, uid, ids, xml_id, context=None):
343         data_obj = self.pool.get('ir.model.data')
344         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
345         categ_id = False
346         if id2:
347             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
348         if categ_id:
349             self.write(cr, uid, ids, {'categ_id': categ_id})
350         return True
351
352     def convert_to_feature(self, cr, uid, ids, context=None):
353         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
354
355     def convert_to_bug(self, cr, uid, ids, context=None):
356         return self._convert(cr, uid, ids, 'bug_categ', context=context)
357
358     def next_type(self, cr, uid, ids, *args):
359         for task in self.browse(cr, uid, ids):
360             typeid = task.type_id.id
361             types = map(lambda x:x.id, task.project_id.type_ids or [])
362             if types:
363                 if not typeid:
364                     self.write(cr, uid, task.id, {'type_id': types[0]})
365                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
366                     index = types.index(typeid)
367                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
368         return True
369
370     def prev_type(self, cr, uid, ids, *args):
371         for task in self.browse(cr, uid, ids):
372             typeid = task.type_id.id
373             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
374             if types:
375                 if typeid and typeid in types:
376                     index = types.index(typeid)
377                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
378         return True
379
380     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
381         result = {}
382         if not task_id:
383             return {'value':{}}
384         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
385         return {'value':{'user_id': task.user_id.id,}}
386
387     def case_escalate(self, cr, uid, ids, *args):
388         """Escalates case to top level
389         @param self: The object pointer
390         @param cr: the current row, from the database cursor,
391         @param uid: the current user’s ID for security checks,
392         @param ids: List of case Ids
393         @param *args: Tuple Value for additional Params
394         """
395         cases = self.browse(cr, uid, ids)
396         for case in cases:
397             data = {'state' : 'draft'}
398             if case.project_id.project_escalation_id:
399                 data['project_id'] = case.project_id.project_escalation_id.id
400                 if case.project_id.project_escalation_id.user_id:
401                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
402                 if case.task_id:
403                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
404             else:
405                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
406             self.write(cr, uid, [case.id], data)
407         self.message_append(cr, uid, cases, _('Escalate'))
408         return True
409
410     def message_new(self, cr, uid, msg, custom_values=None, context=None):
411         """Automatically called when new email message arrives"""
412         if context is None:
413             context = {}
414         subject = msg.get('subject') or _('No Title')
415         body = msg.get('body_text')
416         msg_from = msg.get('from')
417         priority = msg.get('priority')
418         vals = {
419             'name': subject,
420             'email_from': msg_from,
421             'email_cc': msg.get('cc'),
422             'description': body,
423             'user_id': False,
424         }
425         if priority:
426             vals['priority'] = priority
427         vals.update(self.message_partner_by_email(cr, uid, msg_from))
428         context.update({'state_to' : 'draft'})
429
430         if custom_values and isinstance(custom_values, dict):
431             vals.update(custom_values)
432
433         res_id = self.create(cr, uid, vals, context)
434         self.message_append_dict(cr, uid, [res_id], msg, context=context)
435         self.convert_to_bug(cr, uid, [res_id], context=context)
436         return res_id
437
438     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
439
440         if vals is None:
441             vals = {}
442
443         if isinstance(ids, (str, int, long)):
444             ids = [ids]
445
446         vals.update({
447             'description': msg['body_text']
448         })
449         if msg.get('priority', False):
450             vals['priority'] = msg.get('priority')
451
452         maps = {
453             'cost': 'planned_cost',
454             'revenue': 'planned_revenue',
455             'probability': 'probability'
456         }
457
458         # Reassign the 'open' state to the case if this one is in pending or done
459         for record in self.browse(cr, uid, ids, context=context):
460             if record.state in ('pending', 'done'):
461                 record.write({'state' : 'open'})
462
463         vls = { }
464         for line in msg['body_text'].split('\n'):
465             line = line.strip()
466             res = tools.misc.command_re.match(line)
467             if res and maps.get(res.group(1).lower(), False):
468                 key = maps.get(res.group(1).lower())
469                 vls[key] = res.group(2).lower()
470
471         vals.update(vls)
472         res = self.write(cr, uid, ids, vals)
473         self.message_append_dict(cr, uid, ids, msg, context=context)
474         return res
475
476     def copy(self, cr, uid, id, default=None, context=None):
477         issue = self.read(cr, uid, id, ['name'], context=context)
478         if not default:
479             default = {}
480         default = default.copy()
481         default['name'] = issue['name'] + _(' (copy)')
482         return super(project_issue, self).copy(cr, uid, id, default=default,
483                 context=context)
484
485 project_issue()
486
487 class project(osv.osv):
488     _inherit = "project.project"
489     _columns = {
490         'resource_calendar_id' : fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
491         '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.', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
492         'reply_to' : fields.char('Reply-To Email Address', size=256)
493     }
494
495     def _check_escalation(self, cr, uid, ids, context=None):
496          project_obj = self.browse(cr, uid, ids[0], context=context)
497          if project_obj.project_escalation_id:
498              if project_obj.project_escalation_id.id == project_obj.id:
499                  return False
500          return True
501
502     _constraints = [
503         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
504     ]
505 project()
506
507 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: