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