[IMP]: Added context=None in function argument for rest modules.
[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         if context is None:
82             context = {}
83         """
84         @param cr: the current row, from the database cursor,
85         @param uid: the current user’s ID for security checks,
86         @param ids: List of Openday’s IDs
87         @return: difference between current date and log date
88         @param context: A standard dictionary for contextual values
89         """
90         cal_obj = self.pool.get('resource.calendar')
91         res_obj = self.pool.get('resource.resource')
92
93         res = {}
94         for issue in self.browse(cr, uid, ids, context=context):
95             for field in fields:
96                 res[issue.id] = {}
97                 duration = 0
98                 ans = False
99                 hours = 0
100
101                 if field in ['working_hours_open','day_open']:
102                     if issue.date_open:
103                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
104                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
105                         ans = date_open - date_create
106                         date_until = issue.date_open
107                         #Calculating no. of working hours to open the issue
108                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
109                                  datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
110                                  datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
111                 elif field in ['working_hours_close','day_close']:
112                     if issue.date_closed:
113                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
114                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
115                         date_until = issue.date_closed
116                         ans = date_close - date_create
117                         #Calculating no. of working hours to close the issue
118                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
119                                 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
120                                 datetime.strptime(issue.date_closed, '%Y-%m-%d %H:%M:%S'))
121                 if ans:
122                     resource_id = False
123                     if issue.user_id:
124                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
125                         if resource_ids and len(resource_ids):
126                             resource_id = resource_ids[0]
127                     duration = float(ans.days)
128                     if issue.project_id and issue.project_id.resource_calendar_id:
129                         duration = float(ans.days) * 24
130                         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)
131                         no_days = []
132                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
133                         for in_time, out_time in new_dates:
134                             if in_time.date not in no_days:
135                                 no_days.append(in_time.date)
136                             if out_time > date_until:
137                                 break
138                         duration = len(no_days)
139                 if field in ['working_hours_open','working_hours_close']:
140                     res[issue.id][field] = hours
141                 else:
142                     res[issue.id][field] = abs(float(duration))
143         return res
144
145     def _get_issue_task(self, cr, uid, ids, context=None):
146         if context is None:
147             context = {}
148         issues = []
149         issue_pool = self.pool.get('project.issue')
150         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
151             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])            
152         return issues
153
154     def _get_issue_work(self, cr, uid, ids, context=None):
155         if context is None:
156             context = {}
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),
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.many2one('res.users', '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),
198         # Project Issue fields
199         'date_closed': fields.datetime('Closed', readonly=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'),
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                                 method=True, multi='day_open', type="float", store=True),
215         'day_close': fields.function(_compute_day, string='Days to Close', \
216                                 method=True, multi='day_close', type="float", store=True),
217         'assigned_to': fields.related('task_id', 'user_id', string = 'Assigned to', type="many2one", relation="res.users", store=True, help='This is the current user to whom the related task have been assigned'),
218         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
219                                 method=True, multi='working_days_open', type="float", store=True),
220         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
221                                 method=True, multi='working_days_close', type="float", store=True),
222         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
223         'date_action_last': fields.datetime('Last Action', readonly=1),
224         'date_action_next': fields.datetime('Next Action', readonly=1),
225         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
226             store = {
227                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
228                 'project.task': (_get_issue_task, ['progress'], 10),
229                 'project.task.work': (_get_issue_work, ['hours'], 10),
230             }),
231     }
232
233     def _get_project(self, cr, uid, context=None):
234         if not context:
235             context = {}
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     _defaults = {
242         'active': 1,
243         'user_id': crm.crm_case._get_default_user,
244         'partner_id': crm.crm_case._get_default_partner,
245         'partner_address_id': crm.crm_case._get_default_partner_address,
246         'email_from': crm.crm_case. _get_default_email,
247         'state': 'draft',
248         'section_id': crm.crm_case. _get_section,
249         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
250         'priority': crm.AVAILABLE_PRIORITIES[2][0],
251         'project_id':_get_project,
252     }
253
254     def convert_issue_task(self, cr, uid, ids, context=None):
255         case_obj = self.pool.get('project.issue')
256         data_obj = self.pool.get('ir.model.data')
257         task_obj = self.pool.get('project.task')
258
259
260         if context is None:
261             context = {}
262
263         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
264         res = data_obj.read(cr, uid, result, ['res_id'])
265         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
266         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
267         if id2:
268             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
269         if id3:
270             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
271
272         for bug in case_obj.browse(cr, uid, ids, context=context):
273             new_task_id = task_obj.create(cr, uid, {
274                 'name': bug.name,
275                 'partner_id': bug.partner_id.id,
276                 'description':bug.description,
277                 'date': bug.date,
278                 'project_id': bug.project_id.id,
279                 'priority': bug.priority,
280                 'user_id': bug.assigned_to.id,
281                 'planned_hours': 0.0,
282             })
283
284             vals = {
285                 'task_id': new_task_id,
286                 'state':'pending'
287             }
288             case_obj.write(cr, uid, [bug.id], vals)
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         if not context:
306             context = {}
307         data_obj = self.pool.get('ir.model.data')
308         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
309         categ_id = False
310         if id2:
311             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
312         if categ_id:
313             self.write(cr, uid, ids, {'categ_id': categ_id})
314         return True
315
316     def convert_to_feature(self, cr, uid, ids, context=None):
317         if not context:
318             context = {}
319         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
320
321     def convert_to_bug(self, cr, uid, ids, context=None):
322         if not context:
323             context = {}
324         return self._convert(cr, uid, ids, 'bug_categ', context=context)
325
326     def next_type(self, cr, uid, ids, *args):
327         for task in self.browse(cr, uid, ids):
328             typeid = task.type_id.id
329             types = map(lambda x:x.id, task.project_id.type_ids or [])
330             if types:
331                 if not typeid:
332                     self.write(cr, uid, task.id, {'type_id': types[0]})
333                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
334                     index = types.index(typeid)
335                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
336         return True
337
338     def prev_type(self, cr, uid, ids, *args):
339         for task in self.browse(cr, uid, ids):
340             typeid = task.type_id.id
341             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
342             if types:
343                 if typeid and typeid in types:
344                     index = types.index(typeid)
345                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
346         return True
347
348
349     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
350         if context is None:
351             context = {}
352         result = {}
353         if not task_id:
354             return {'value':{}}
355         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
356         return {'value':{'assigned_to': task.user_id.id,}}
357
358     def case_escalate(self, cr, uid, ids, *args):
359         """Escalates case to top level
360         @param self: The object pointer
361         @param cr: the current row, from the database cursor,
362         @param uid: the current user’s ID for security checks,
363         @param ids: List of case Ids
364         @param *args: Tuple Value for additional Params
365         """
366         cases = self.browse(cr, uid, ids)
367         for case in cases:
368             data = {}
369             if case.project_id.project_escalation_id:
370                 data['project_id'] = case.project_id.project_escalation_id.id
371                 if case.project_id.project_escalation_id.user_id:
372                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
373                 if case.task_id:
374                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
375             else:
376                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
377             self.write(cr, uid, [case.id], data)
378         self._history(cr, uid, cases, _('Escalate'))
379         return True
380
381     def message_new(self, cr, uid, msg, context=None):
382         """
383         Automatically calls when new email message arrives
384
385         @param self: The object pointer
386         @param cr: the current row, from the database cursor,
387         @param uid: the current user’s ID for security checks
388         """
389         if not context:
390             context = {}
391         mailgate_pool = self.pool.get('email.server.tools')
392
393         subject = msg.get('subject') or _('No Title')
394         body = msg.get('body')
395         msg_from = msg.get('from')
396         priority = msg.get('priority')
397
398         vals = {
399             'name': subject,
400             'email_from': msg_from,
401             'email_cc': msg.get('cc'),
402             'description': body,
403             'user_id': False,
404         }
405         if msg.get('priority', False):
406             vals['priority'] = priority
407
408         res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
409         if res:
410             vals.update(res)
411         context.update({'state_to' : 'draft'})
412         res = self.create(cr, uid, vals, context=context)
413         self.convert_to_bug(cr, uid, [res], context=context)
414
415         attachents = msg.get('attachments', [])
416         for attactment in attachents or []:
417             data_attach = {
418                 'name': attactment,
419                 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
420                 'datas_fname': attactment,
421                 'description': 'Mail attachment',
422                 'res_model': self._name,
423                 'res_id': res,
424             }
425             self.pool.get('ir.attachment').create(cr, uid, data_attach)
426
427         return res
428
429     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
430         if context is None:
431             context = {}
432         """
433         @param self: The object pointer
434         @param cr: the current row, from the database cursor,
435         @param uid: the current user’s ID for security checks,
436         @param ids: List of update mail’s IDs
437         """
438
439         if isinstance(ids, (str, int, long)):
440             ids = [ids]
441
442         vals.update({
443             'description': msg['body']
444         })
445         if msg.get('priority', False):
446             vals['priority'] = msg.get('priority')
447
448         maps = {
449             'cost': 'planned_cost',
450             'revenue': 'planned_revenue',
451             'probability': 'probability'
452         }
453         vls = { }
454         for line in msg['body'].split('\n'):
455             line = line.strip()
456             res = tools.misc.command_re.match(line)
457             if res and maps.get(res.group(1).lower(), False):
458                 key = maps.get(res.group(1).lower())
459                 vls[key] = res.group(2).lower()
460
461         vals.update(vls)
462         res = self.write(cr, uid, ids, vals)
463         return res
464
465     def msg_send(self, cr, uid, id, *args, **argv):
466
467         """ Send The Message
468             @param self: The object pointer
469             @param cr: the current row, from the database cursor,
470             @param uid: the current user’s ID for security checks,
471             @param ids: List of email’s IDs
472             @param *args: Return Tuple Value
473             @param **args: Return Dictionary of Keyword Value
474         """
475         return True
476
477     def copy(self, cr, uid, id, default=None, context=None):
478         if not context:
479             context={}
480         issue = self.read(cr, uid, id, ['name'], context=context)
481         if not default:
482             default = {}
483         default = default.copy()
484         default['name'] = issue['name'] + _(' (copy)')
485         return super(project_issue, self).copy(cr, uid, id, default=default,
486                 context=context)
487
488 project_issue()
489
490 class project(osv.osv):
491     _inherit = "project.project"
492     _columns = {
493         '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)]}),
494         '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)]}),
495         'reply_to' : fields.char('Reply-To Email Address', size=256)
496     }
497
498     def _check_escalation(self, cr, uid, ids, context=None):
499          if not context:
500             context = {}
501          project_obj = self.browse(cr, uid, ids[0], context=context)
502          if project_obj.project_escalation_id:
503              if project_obj.project_escalation_id.id == project_obj.id:
504                  return False
505          return True
506
507     _constraints = [
508         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
509     ]
510 project()
511
512 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: