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