[IMP] The responsible of the project issue is the manager of the associated project.
[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
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, id desc"
47     _inherit = ['mailgate.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')})
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                 if ans:
120                     resource_id = False
121                     if issue.user_id:
122                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
123                         if resource_ids and len(resource_ids):
124                             resource_id = resource_ids[0]
125                     duration = float(ans.days)
126                     if issue.project_id and issue.project_id.resource_calendar_id:
127                         duration = float(ans.days) * 24
128                         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)
129                         no_days = []
130                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
131                         for in_time, out_time in new_dates:
132                             if in_time.date not in no_days:
133                                 no_days.append(in_time.date)
134                             if out_time > date_until:
135                                 break
136                         duration = len(no_days)
137                 if field in ['working_hours_open','working_hours_close']:
138                     res[issue.id][field] = hours
139                 else:
140                     res[issue.id][field] = abs(float(duration))
141         return res
142
143     def _get_issue_task(self, cr, uid, ids, context=None):
144         issues = []
145         issue_pool = self.pool.get('project.issue')
146         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
147             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])            
148         return issues
149
150     def _get_issue_work(self, cr, uid, ids, context=None):
151         issues = []
152         issue_pool = self.pool.get('project.issue')
153         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
154             if work.task_id:
155                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
156         return issues
157
158     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
159         task_pool = self.pool.get('project.task')
160         res = {}
161         for issue in self.browse(cr, uid, ids, context=context):
162             progress = 0.0
163             if issue.task_id:
164                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
165             res[issue.id] = {'progress' : progress}     
166         return res        
167
168     _columns = {
169         'id': fields.integer('ID'),
170         'name': fields.char('Issue', size=128, required=True),
171         'active': fields.boolean('Active', required=False),
172         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
173         'write_date': fields.datetime('Update Date', readonly=True),
174         'date_deadline': fields.date('Deadline'),
175         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
176                         select=True, help='Sales team to which Case belongs to.\
177                              Define Responsible user and Email account for mail gateway.'),
178         'user_id': fields.related('project_id', 'user_id', type='many2one', relation='res.users', store=True, select=1, string='Responsible'),
179         'partner_id': fields.many2one('res.partner', 'Partner'),
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', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('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."),
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         'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
196                                                                         " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
197         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
198         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
199         'version_id': fields.many2one('project.issue.version', 'Version'),
200         'partner_name': fields.char("Employee's Name", size=64),
201         'partner_mobile': fields.char('Mobile', size=32),
202         'partner_phone': fields.char('Phone', size=32),
203         'type_id': fields.many2one ('project.task.type', 'Resolution', domain="[('project_ids', '=', project_id)]"),
204         'project_id':fields.many2one('project.project', 'Project'),
205         'duration': fields.float('Duration'),
206         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
207         'day_open': fields.function(_compute_day, string='Days to Open', \
208                                 method=True, multi='day_open', type="float", store=True),
209         'day_close': fields.function(_compute_day, string='Days to Close', \
210                                 method=True, multi='day_close', type="float", store=True),
211         'assigned_to': fields.many2one('res.users', 'Assigned to', required=True, select=1),
212         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
213                                 method=True, multi='working_days_open', type="float", store=True),
214         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
215                                 method=True, multi='working_days_close', type="float", store=True),
216         'message_ids': fields.one2many('mailgate.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, method=True, 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     def _get_project(self, cr, uid, context=None):
228         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
229         if user.context_project_id:
230             return user.context_project_id.id
231         return False
232
233     _defaults = {
234         'active': 1,
235         'user_id': crm.crm_case._get_default_user,
236         'partner_id': crm.crm_case._get_default_partner,
237         'partner_address_id': crm.crm_case._get_default_partner_address,
238         'email_from': crm.crm_case. _get_default_email,
239         'state': 'draft',
240         'section_id': crm.crm_case. _get_section,
241         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
242         'priority': crm.AVAILABLE_PRIORITIES[2][0],
243         'project_id':_get_project,
244         'assigned_to' : lambda obj, cr, uid, context: uid,
245     }
246
247     def convert_issue_task(self, cr, uid, ids, context=None):
248         case_obj = self.pool.get('project.issue')
249         data_obj = self.pool.get('ir.model.data')
250         task_obj = self.pool.get('project.task')
251
252
253         if context is None:
254             context = {}
255
256         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
257         res = data_obj.read(cr, uid, result, ['res_id'])
258         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
259         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
260         if id2:
261             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
262         if id3:
263             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
264
265         for bug in case_obj.browse(cr, uid, ids, context=context):
266             new_task_id = task_obj.create(cr, uid, {
267                 'name': bug.name,
268                 'partner_id': bug.partner_id.id,
269                 'description':bug.description,
270                 'date': bug.date,
271                 'project_id': bug.project_id.id,
272                 'priority': bug.priority,
273                 'user_id': bug.assigned_to.id,
274                 'planned_hours': 0.0,
275             })
276
277             vals = {
278                 'task_id': new_task_id,
279                 'state':'pending'
280             }
281             case_obj.write(cr, uid, [bug.id], vals)
282
283         return  {
284             'name': _('Tasks'),
285             'view_type': 'form',
286             'view_mode': 'form,tree',
287             'res_model': 'project.task',
288             'res_id': int(new_task_id),
289             'view_id': False,
290             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
291             'type': 'ir.actions.act_window',
292             'search_view_id': res['res_id'],
293             'nodestroy': True
294         }
295
296
297     def _convert(self, cr, uid, ids, xml_id, context=None):
298         data_obj = self.pool.get('ir.model.data')
299         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
300         categ_id = False
301         if id2:
302             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
303         if categ_id:
304             self.write(cr, uid, ids, {'categ_id': categ_id})
305         return True
306
307     def convert_to_feature(self, cr, uid, ids, context=None):
308         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
309
310     def convert_to_bug(self, cr, uid, ids, context=None):
311         return self._convert(cr, uid, ids, 'bug_categ', context=context)
312
313     def next_type(self, cr, uid, ids, *args):
314         for task in self.browse(cr, uid, ids):
315             typeid = task.type_id.id
316             types = map(lambda x:x.id, task.project_id.type_ids or [])
317             if types:
318                 if not typeid:
319                     self.write(cr, uid, task.id, {'type_id': types[0]})
320                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
321                     index = types.index(typeid)
322                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
323         return True
324
325     def prev_type(self, cr, uid, ids, *args):
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
336     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
337         result = {}
338         if not task_id:
339             return {'value':{}}
340         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
341         return {'value':{'assigned_to': task.user_id.id,}}
342
343     def case_escalate(self, cr, uid, ids, *args):
344         """Escalates case to top level
345         @param self: The object pointer
346         @param cr: the current row, from the database cursor,
347         @param uid: the current user’s ID for security checks,
348         @param ids: List of case Ids
349         @param *args: Tuple Value for additional Params
350         """
351         cases = self.browse(cr, uid, ids)
352         for case in cases:
353             data = {}
354             if case.project_id.project_escalation_id:
355                 data['project_id'] = case.project_id.project_escalation_id.id
356                 if case.project_id.project_escalation_id.user_id:
357                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
358                 if case.task_id:
359                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
360             else:
361                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
362             self.write(cr, uid, [case.id], data)
363         self._history(cr, uid, cases, _('Escalate'))
364         return True
365
366     def message_new(self, cr, uid, msg, context=None):
367         """
368         Automatically calls when new email message arrives
369
370         @param self: The object pointer
371         @param cr: the current row, from the database cursor,
372         @param uid: the current user’s ID for security checks
373         """
374         if context is None: 
375             context = {}
376         mailgate_pool = self.pool.get('email.server.tools')
377
378         subject = msg.get('subject') or _('No Title')
379         body = msg.get('body')
380         msg_from = msg.get('from')
381         priority = msg.get('priority')
382
383         vals = {
384             'name': subject,
385             'email_from': msg_from,
386             'email_cc': msg.get('cc'),
387             'description': body,
388             'user_id': False,
389         }
390         if msg.get('priority', False):
391             vals['priority'] = priority
392
393         res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
394         if res:
395             vals.update(res)
396         context.update({'state_to' : 'draft'})
397         res = self.create(cr, uid, vals, context=context)
398         self.convert_to_bug(cr, uid, [res], context=context)
399
400         attachents = msg.get('attachments', [])
401         for attactment in attachents or []:
402             data_attach = {
403                 'name': attactment,
404                 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
405                 'datas_fname': attactment,
406                 'description': 'Mail attachment',
407                 'res_model': self._name,
408                 'res_id': res,
409             }
410             self.pool.get('ir.attachment').create(cr, uid, data_attach)
411
412         return res
413
414     def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
415         """
416         @param self: The object pointer
417         @param cr: the current row, from the database cursor,
418         @param uid: the current user’s ID for security checks,
419         @param ids: List of update mail’s IDs
420         """
421
422         if vals is None:
423             vals = {}
424
425         if isinstance(ids, (str, int, long)):
426             ids = [ids]
427
428         vals.update({
429             'description': msg['body']
430         })
431         if msg.get('priority', False):
432             vals['priority'] = msg.get('priority')
433
434         maps = {
435             'cost': 'planned_cost',
436             'revenue': 'planned_revenue',
437             'probability': 'probability'
438         }
439
440         # Reassign the 'open' state to the case if this one is in pending or done
441         for record in self.browse(cr, uid, ids, context=context):
442             if record.state in ('pending', 'done'):
443                 record.write({'state' : 'open'})
444
445         vls = { }
446         for line in msg['body'].split('\n'):
447             line = line.strip()
448             res = tools.misc.command_re.match(line)
449             if res and maps.get(res.group(1).lower(), False):
450                 key = maps.get(res.group(1).lower())
451                 vls[key] = res.group(2).lower()
452
453         vals.update(vls)
454         res = self.write(cr, uid, ids, vals)
455         return res
456
457     def msg_send(self, cr, uid, id, *args, **argv):
458
459         """ Send The Message
460             @param self: The object pointer
461             @param cr: the current row, from the database cursor,
462             @param uid: the current user’s ID for security checks,
463             @param ids: List of email’s IDs
464             @param *args: Return Tuple Value
465             @param **args: Return Dictionary of Keyword Value
466         """
467         return True
468
469     def copy(self, cr, uid, id, default=None, context=None):
470         issue = self.read(cr, uid, id, ['name'], context=context)
471         if not default:
472             default = {}
473         default = default.copy()
474         default['name'] = issue['name'] + _(' (copy)')
475         return super(project_issue, self).copy(cr, uid, id, default=default,
476                 context=context)
477
478 project_issue()
479
480 class project(osv.osv):
481     _inherit = "project.project"
482     _columns = {
483         '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)]}),
484         '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)]}),
485         'reply_to' : fields.char('Reply-To Email Address', size=256)
486     }
487
488     def _check_escalation(self, cr, uid, ids, context=None):
489          project_obj = self.browse(cr, uid, ids[0], context=context)
490          if project_obj.project_escalation_id:
491              if project_obj.project_escalation_id.id == project_obj.id:
492                  return False
493          return True
494
495     _constraints = [
496         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
497     ]
498 project()
499
500 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: