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