[FIX] mail_gateway : Received emails are not creating dulicate attachments anymore
[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.many2one('res.users', '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'),
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.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'),
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     }
245
246     def convert_issue_task(self, cr, uid, ids, context=None):
247         case_obj = self.pool.get('project.issue')
248         data_obj = self.pool.get('ir.model.data')
249         task_obj = self.pool.get('project.task')
250
251
252         if context is None:
253             context = {}
254
255         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
256         res = data_obj.read(cr, uid, result, ['res_id'])
257         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
258         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
259         if id2:
260             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
261         if id3:
262             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
263
264         for bug in case_obj.browse(cr, uid, ids, context=context):
265             new_task_id = task_obj.create(cr, uid, {
266                 'name': bug.name,
267                 'partner_id': bug.partner_id.id,
268                 'description':bug.description,
269                 'date': bug.date,
270                 'project_id': bug.project_id.id,
271                 'priority': bug.priority,
272                 'user_id': bug.assigned_to.id,
273                 'planned_hours': 0.0,
274             })
275
276             vals = {
277                 'task_id': new_task_id,
278                 'state':'pending'
279             }
280             case_obj.write(cr, uid, [bug.id], vals)
281
282         return  {
283             'name': _('Tasks'),
284             'view_type': 'form',
285             'view_mode': 'form,tree',
286             'res_model': 'project.task',
287             'res_id': int(new_task_id),
288             'view_id': False,
289             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
290             'type': 'ir.actions.act_window',
291             'search_view_id': res['res_id'],
292             'nodestroy': True
293         }
294
295
296     def _convert(self, cr, uid, ids, xml_id, context=None):
297         data_obj = self.pool.get('ir.model.data')
298         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
299         categ_id = False
300         if id2:
301             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
302         if categ_id:
303             self.write(cr, uid, ids, {'categ_id': categ_id})
304         return True
305
306     def convert_to_feature(self, cr, uid, ids, context=None):
307         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
308
309     def convert_to_bug(self, cr, uid, ids, context=None):
310         return self._convert(cr, uid, ids, 'bug_categ', context=context)
311
312     def next_type(self, cr, uid, ids, *args):
313         for task in self.browse(cr, uid, ids):
314             typeid = task.type_id.id
315             types = map(lambda x:x.id, task.project_id.type_ids or [])
316             if types:
317                 if not typeid:
318                     self.write(cr, uid, task.id, {'type_id': types[0]})
319                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
320                     index = types.index(typeid)
321                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
322         return True
323
324     def prev_type(self, cr, uid, ids, *args):
325         for task in self.browse(cr, uid, ids):
326             typeid = task.type_id.id
327             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
328             if types:
329                 if typeid and typeid in types:
330                     index = types.index(typeid)
331                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
332         return True
333
334
335     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
336         result = {}
337         if not task_id:
338             return {'value':{}}
339         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
340         return {'value':{'assigned_to': task.user_id.id,}}
341
342     def case_escalate(self, cr, uid, ids, *args):
343         """Escalates case to top level
344         @param self: The object pointer
345         @param cr: the current row, from the database cursor,
346         @param uid: the current user’s ID for security checks,
347         @param ids: List of case Ids
348         @param *args: Tuple Value for additional Params
349         """
350         cases = self.browse(cr, uid, ids)
351         for case in cases:
352             data = {}
353             if case.project_id.project_escalation_id:
354                 data['project_id'] = case.project_id.project_escalation_id.id
355                 if case.project_id.project_escalation_id.user_id:
356                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
357                 if case.task_id:
358                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
359             else:
360                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
361             self.write(cr, uid, [case.id], data)
362         self._history(cr, uid, cases, _('Escalate'))
363         return True
364
365     def message_new(self, cr, uid, msg, context=None):
366         """
367         Automatically calls when new email message arrives
368
369         @param self: The object pointer
370         @param cr: the current row, from the database cursor,
371         @param uid: the current user’s ID for security checks
372         """
373         if context is None: 
374             context = {}
375         mailgate_pool = self.pool.get('email.server.tools')
376
377         subject = msg.get('subject') or _('No Title')
378         body = msg.get('body')
379         msg_from = msg.get('from')
380         priority = msg.get('priority')
381
382         vals = {
383             'name': subject,
384             'email_from': msg_from,
385             'email_cc': msg.get('cc'),
386             'description': body,
387             'user_id': False,
388         }
389         if msg.get('priority', False):
390             vals['priority'] = priority
391
392         res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
393         if res:
394             vals.update(res)
395         context.update({'state_to' : 'draft'})
396         res = self.create(cr, uid, vals, context=context)
397         self.convert_to_bug(cr, uid, [res], context=context)
398
399         attachents = msg.get('attachments', [])
400         att_ids = []
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             att_ids.append(self.pool.get('ir.attachment').create(cr, uid, data_attach))
411
412         return res, att_ids
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: