ab1ee801cec4e477a5ccf27d74fe233695899963
[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 = ['ir.needaction', '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         'company_id': fields.many2one('res.company', 'Company'),
181         'description': fields.text('Description'),
182         'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
183                                   help='The state is set to \'Draft\', when a case is created.\
184                                   \nIf the case is in progress the state is set to \'Open\'.\
185                                   \nWhen the case is over, the state is set to \'Done\'.\
186                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
187         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
188         '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"),
189         'date_open': fields.datetime('Opened', readonly=True,select=True),
190         # Project Issue fields
191         'date_closed': fields.datetime('Closed', readonly=True,select=True),
192         'date': fields.datetime('Date'),
193         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
194         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
195         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
196         'version_id': fields.many2one('project.issue.version', 'Version'),
197         'type_id': fields.many2one ('project.task.type', 'Stages', domain="[('project_ids', '=', project_id)]"),
198         'project_id':fields.many2one('project.project', 'Project'),
199         'duration': fields.float('Duration'),
200         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
201         'day_open': fields.function(_compute_day, string='Days to Open', \
202                                 multi='compute_day', type="float", store=True),
203         'day_close': fields.function(_compute_day, string='Days to Close', \
204                                 multi='compute_day', type="float", store=True),
205         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
206         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
207                                 multi='compute_day', type="float", store=True),
208         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
209                                 multi='compute_day', type="float", store=True),
210         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
211                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
212         'color': fields.integer('Color Index'),
213         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
214         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
215         'date_action_last': fields.datetime('Last Action', readonly=1),
216         'date_action_next': fields.datetime('Next Action', readonly=1),
217         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
218             store = {
219                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
220                 'project.task': (_get_issue_task, ['progress'], 10),
221                 'project.task.work': (_get_issue_work, ['hours'], 10),
222             }),
223     }
224
225
226     _defaults = {
227         'active': 1,
228         'partner_id': crm.crm_case._get_default_partner,
229         'email_from': crm.crm_case._get_default_email,
230         'state': 'draft',
231         'section_id': crm.crm_case._get_section,
232         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
233         'priority': crm.AVAILABLE_PRIORITIES[2][0],
234         'project_id':_get_project,
235         'categ_id' : lambda *a: False,
236          }
237
238     def set_priority(self, cr, uid, ids, priority):
239         """Set lead priority
240         """
241         return self.write(cr, uid, ids, {'priority' : priority})
242
243     def set_high_priority(self, cr, uid, ids, *args):
244         """Set lead priority to high
245         """
246         return self.set_priority(cr, uid, ids, '1')
247
248     def set_normal_priority(self, cr, uid, ids, *args):
249         """Set lead priority to normal
250         """
251         return self.set_priority(cr, uid, ids, '3')
252
253     def convert_issue_task(self, cr, uid, ids, context=None):
254         if context is None:
255             context = {}
256         
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         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
262         res = data_obj.read(cr, uid, result, ['res_id'])
263         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
264         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
265         if id2:
266             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
267         if id3:
268             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
269
270         for bug in case_obj.browse(cr, uid, ids, context=context):
271             new_task_id = task_obj.create(cr, uid, {
272                 'name': bug.name,
273                 'partner_id': bug.partner_id.id,
274                 'description':bug.description,
275                 'date_deadline': bug.date,
276                 'project_id': bug.project_id.id,
277                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
278                 'priority': str(int(bug.priority) - 1),
279                 'user_id': bug.user_id.id,
280                 'planned_hours': 0.0,
281             })
282             vals = {
283                 'task_id': new_task_id,
284                 'state':'pending'
285             }
286             self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
287             case_obj.write(cr, uid, [bug.id], vals, context=context)
288             self.case_pending_send_note(cr, uid, [bug.id], context=context)
289
290         return  {
291             'name': _('Tasks'),
292             'view_type': 'form',
293             'view_mode': 'form,tree',
294             'res_model': 'project.task',
295             'res_id': int(new_task_id),
296             'view_id': False,
297             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
298             'type': 'ir.actions.act_window',
299             'search_view_id': res['res_id'],
300             'nodestroy': True
301         }
302
303
304     def _convert(self, cr, uid, ids, xml_id, context=None):
305         data_obj = self.pool.get('ir.model.data')
306         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
307         categ_id = False
308         if id2:
309             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
310         if categ_id:
311             self.write(cr, uid, ids, {'categ_id': categ_id})
312         return True
313
314     def convert_to_feature(self, cr, uid, ids, context=None):
315         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
316
317     def convert_to_bug(self, cr, uid, ids, context=None):
318         return self._convert(cr, uid, ids, 'bug_categ', context=context)
319
320     def next_type(self, cr, uid, ids, context=None):
321         for task in self.browse(cr, uid, ids):
322             typeid = task.type_id.id
323             types = map(lambda x:x.id, task.project_id.type_ids or [])
324             if types:
325                 if not typeid:
326                     self.write(cr, uid, [task.id], {'type_id': types[0]})
327                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
328                     index = types.index(typeid)
329                     self.write(cr, uid, [task.id], {'type_id': types[index+1]})
330         return True
331
332     def prev_type(self, cr, uid, ids, context=None):
333         for task in self.browse(cr, uid, ids):
334             typeid = task.type_id.id
335             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
336             if types:
337                 if typeid and typeid in types:
338                     index = types.index(typeid)
339                     self.write(cr, uid, [task.id], {'type_id': index and types[index-1] or False})
340         return True
341
342     def write(self, cr, uid, ids, vals, context=None):
343         #Update last action date every time the user change the stage, the state or send a new email
344         logged_fields = ['type_id', 'state', 'message_ids']
345         if any([field in vals for field in logged_fields]):
346             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
347         if vals.get('type_id', False):
348             stage = self.pool.get('project.task.type').browse(cr, uid, vals['type_id'], context=context)
349             self.message_append_note(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % stage.name, context=context)
350         return super(project_issue, self).write(cr, uid, ids, vals, context)
351
352     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
353         result = {}
354         if not task_id:
355             return {'value':{}}
356         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
357         return {'value':{'user_id': task.user_id.id,}}
358
359     def case_reset(self, cr, uid, ids, context=None):
360         """Resets case as draft
361         """
362         res = super(project_issue, self).case_reset(cr, uid, ids, context)
363         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
364         return res
365
366     def create(self, cr, uid, vals, context=None):
367         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
368         self.create_send_note(cr, uid, [obj_id], context=context)
369         return obj_id
370
371     def case_open(self, cr, uid, ids, context=None):
372         res = super(project_issue, self).case_open(cr, uid, ids, context)
373         self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
374         return res
375
376     def case_escalate(self, cr, uid, ids, context=None):
377         cases = self.browse(cr, uid, ids)
378         for case in cases:
379             data = {'state' : 'draft'}
380             if case.project_id.project_escalation_id:
381                 data['project_id'] = case.project_id.project_escalation_id.id
382                 if case.project_id.project_escalation_id.user_id:
383                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
384                 if case.task_id:
385                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
386             else:
387                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
388             self.write(cr, uid, [case.id], data)
389             self.case_escalate_send_note(cr, uid, [case.id], context)
390         return True
391
392     def message_new(self, cr, uid, msg, custom_values=None, context=None):
393         """Automatically called when new email message arrives"""
394         if context is None:
395             context = {}
396         subject = msg.get('subject') or _('No Title')
397         body = msg.get('body_text')
398         msg_from = msg.get('from')
399         priority = msg.get('priority')
400         vals = {
401             'name': subject,
402             'email_from': msg_from,
403             'email_cc': msg.get('cc'),
404             'description': body,
405             'user_id': False,
406         }
407         if priority:
408             vals['priority'] = priority
409         vals.update(self.message_partner_by_email(cr, uid, msg_from))
410         context.update({'state_to' : 'draft'})
411
412         if custom_values and isinstance(custom_values, dict):
413             vals.update(custom_values)
414
415         res_id = self.create(cr, uid, vals, context)
416         self.message_append_dict(cr, uid, [res_id], msg, context=context)
417         self.convert_to_bug(cr, uid, [res_id], context=context)
418         return res_id
419
420     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
421         if vals is None:
422             vals = {}
423
424         if isinstance(ids, (str, int, long)):
425             ids = [ids]
426
427         vals.update({
428             'description': msg['body_text']
429         })
430         if msg.get('priority', False):
431             vals['priority'] = msg.get('priority')
432
433         maps = {
434             'cost': 'planned_cost',
435             'revenue': 'planned_revenue',
436             'probability': 'probability'
437         }
438
439         # Reassign the 'open' state to the case if this one is in pending or done
440         for record in self.browse(cr, uid, ids, context=context):
441             if record.state in ('pending', 'done'):
442                 record.write({'state' : 'open'})
443
444         vls = { }
445         for line in msg['body_text'].split('\n'):
446             line = line.strip()
447             res = tools.misc.command_re.match(line)
448             if res and maps.get(res.group(1).lower(), False):
449                 key = maps.get(res.group(1).lower())
450                 vls[key] = res.group(2).lower()
451
452         vals.update(vls)
453         res = self.write(cr, uid, ids, vals)
454         self.message_append_dict(cr, uid, ids, msg, context=context)
455         return res
456
457     def copy(self, cr, uid, id, default=None, context=None):
458         issue = self.read(cr, uid, id, ['name'], context=context)
459         if not default:
460             default = {}
461         default = default.copy()
462         default['name'] = issue['name'] + _(' (copy)')
463         return super(project_issue, self).copy(cr, uid, id, default=default,
464                 context=context)
465     
466     # -------------------------------------------------------
467     # OpenChatter methods and notifications
468     # -------------------------------------------------------
469     
470     def get_needaction_user_ids(self, cr, uid, ids, context=None):
471         result = dict.fromkeys(ids, [])
472         for obj in self.browse(cr, uid, ids, context=context):
473             if obj.state == 'draft' and obj.user_id:
474                 result[obj.id] = [obj.user_id.id]
475         return result
476     
477     def message_get_subscribers(self, cr, uid, ids, context=None):
478         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
479         for obj in self.browse(cr, uid, ids, context=context):
480             if obj.user_id:
481                 sub_ids.append(obj.user_id.id)
482         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
483     
484     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
485         return 'Project issue '
486
487     def convert_to_task_send_note(self, cr, uid, ids, context=None):
488         message = _("Project issue has been <b>converted</b> in to task.")
489         return self.message_append_note(cr, uid, ids, body=message, context=context)
490
491     def create_send_note(self, cr, uid, ids, context=None):
492         message = _("Project issue has been <b>created</b>.")
493         return self.message_append_note(cr, uid, ids, body=message, context=context)
494
495     def case_escalate_send_note(self, cr, uid, ids, context=None):
496         for obj in self.browse(cr, uid, ids, context=context):
497             if obj.project_id:
498                 message = _("has been <b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
499                 obj.message_append_note(body=message, context=context)
500             else:
501                 message = _("has been <b>escalated</b>.")
502                 obj.message_append_note(body=message, context=context)
503         return True
504
505 project_issue()
506
507 class project(osv.osv):
508     _inherit = "project.project"
509     _columns = {
510         '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)]}),
511         'reply_to' : fields.char('Reply-To Email Address', size=256)
512     }
513
514     def _check_escalation(self, cr, uid, ids, context=None):
515         project_obj = self.browse(cr, uid, ids[0], context=context)
516         if project_obj.project_escalation_id:
517             if project_obj.project_escalation_id.id == project_obj.id:
518                 return False
519         return True
520
521     _constraints = [
522         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
523     ]
524 project()
525
526 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: