19db547983b401450f259e2be26242b82c19c80a
[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(crm.crm_case, osv.osv):
32     _name = "project.issue"
33     _description = "Project Issue"
34     _order = "priority, id desc"
35     _inherit = ['mailgate.thread']
36
37     def case_open(self, cr, uid, ids, *args):
38         """
39         @param self: The object pointer
40         @param cr: the current row, from the database cursor,
41         @param uid: the current user’s ID for security checks,
42         @param ids: List of case's Ids
43         @param *args: Give Tuple Value
44         """
45
46         res = super(project_issue, self).case_open(cr, uid, ids, *args)
47         self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
48         for (id, name) in self.name_get(cr, uid, ids):
49             message = _('Issue ') + " '" + name + "' "+ _("is Open.")
50             self.log(cr, uid, id, message)
51         return res
52
53     def case_close(self, cr, uid, ids, *args):
54         """
55         @param self: The object pointer
56         @param cr: the current row, from the database cursor,
57         @param uid: the current user’s ID for security checks,
58         @param ids: List of case's Ids
59         @param *args: Give Tuple Value
60         """
61
62         res = super(project_issue, self).case_close(cr, uid, ids, *args)
63         for (id, name) in self.name_get(cr, uid, ids):
64             message = _('Issue ') + " '" + name + "' "+ _("is Closed.")
65             self.log(cr, uid, id, message)
66         return res
67
68     def _compute_day(self, cr, uid, ids, fields, args, context=None):
69         if context is None:
70             context = {}
71         """
72         @param cr: the current row, from the database cursor,
73         @param uid: the current user’s ID for security checks,
74         @param ids: List of Openday’s IDs
75         @return: difference between current date and log date
76         @param context: A standard dictionary for contextual values
77         """
78         cal_obj = self.pool.get('resource.calendar')
79         res_obj = self.pool.get('resource.resource')
80
81         res = {}
82         for issue in self.browse(cr, uid, ids, context=context):
83             for field in fields:
84                 res[issue.id] = {}
85                 duration = 0
86                 ans = False
87                 hours = 0
88
89                 if field in ['working_hours_open','day_open']:
90                     if issue.date_open:
91                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
92                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
93                         ans = date_open - date_create
94                         date_until = issue.date_open
95                         #Calculating no. of working hours to open the issue
96                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
97                                  datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
98                                  datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
99                 elif field in ['working_hours_close','day_close']:
100                     if issue.date_closed:
101                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
102                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
103                         date_until = issue.date_closed
104                         ans = date_close - date_create
105                         #Calculating no. of working hours to close 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_closed, '%Y-%m-%d %H:%M:%S'))
109                 if ans:
110                     resource_id = False
111                     if issue.user_id:
112                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
113                         if resource_ids and len(resource_ids):
114                             resource_id = resource_ids[0]
115                     duration = float(ans.days)
116                     if issue.project_id and issue.project_id.resource_calendar_id:
117                         duration = float(ans.days) * 24
118                         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)
119                         no_days = []
120                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
121                         for in_time, out_time in new_dates:
122                             if in_time.date not in no_days:
123                                 no_days.append(in_time.date)
124                             if out_time > date_until:
125                                 break
126                         duration = len(no_days)
127                 if field in ['working_hours_open','working_hours_close']:
128                     res[issue.id][field] = hours
129                 else:
130                     res[issue.id][field] = abs(float(duration))
131         return res
132
133     _columns = {
134         'id': fields.integer('ID'),
135         'name': fields.char('Name', size=128, required=True),
136         'active': fields.boolean('Active', required=False),
137         'create_date': fields.datetime('Creation Date', readonly=True),
138         'write_date': fields.datetime('Update Date', readonly=True),
139         'date_deadline': fields.date('Deadline'),
140         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
141                         select=True, help='Sales team to which Case belongs to.\
142                              Define Responsible user and Email account for mail gateway.'),
143         'user_id': fields.many2one('res.users', 'Responsible'),
144         'partner_id': fields.many2one('res.partner', 'Partner'),
145         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
146                                  domain="[('partner_id','=',partner_id)]"),
147         'company_id': fields.many2one('res.company', 'Company'),
148         'description': fields.text('Description'),
149         'state': fields.selection([('draft', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
150                                   help='The state is set to \'Draft\', when a case is created.\
151                                   \nIf the case is in progress the state is set to \'Open\'.\
152                                   \nWhen the case is over, the state is set to \'Done\'.\
153                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
154         'email_from': fields.char('Email', size=128, help="These people will receive email."),
155         '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"),
156         'date_open': fields.datetime('Opened', readonly=True),
157         # Project Issue fields
158         'date_closed': fields.datetime('Closed', readonly=True),
159         'date': fields.datetime('Date'),
160         'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
161                                                                         " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
162         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
163         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Severity'),
164         'type_id': fields.many2one('crm.case.resource.type', 'Version', domain="[('object_id.model', '=', 'project.issue')]"),
165         'partner_name': fields.char("Employee's Name", size=64),
166         'partner_mobile': fields.char('Mobile', size=32),
167         'partner_phone': fields.char('Phone', size=32),
168         'stage_id': fields.many2one ('crm.case.stage', 'Stage', domain="[('object_id.model', '=', 'project.issue')]"),
169         'project_id':fields.many2one('project.project', 'Project'),
170         'duration': fields.float('Duration'),
171         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
172         'day_open': fields.function(_compute_day, string='Days to Open', \
173                                 method=True, multi='day_open', type="float", store=True),
174         'day_close': fields.function(_compute_day, string='Days to Close', \
175                                 method=True, multi='day_close', type="float", store=True),
176         'assigned_to': fields.many2one('res.users', 'Assigned to', help='This is the current user to whom the related task have been assigned'),
177         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
178                                 method=True, multi='working_days_open', type="float", store=True),
179         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
180                                 method=True, multi='working_days_close', type="float", store=True),
181         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
182         'date_action_last': fields.datetime('Last Action', readonly=1),
183         'date_action_next': fields.datetime('Next Action', readonly=1),
184     }
185
186     def _get_project(self, cr, uid, context):
187         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
188         if user.context_project_id:
189             return user.context_project_id.id
190         return False
191
192     _defaults = {
193         'active': 1,
194         'user_id': crm.crm_case._get_default_user,
195         'partner_id': crm.crm_case._get_default_partner,
196         'partner_address_id': crm.crm_case._get_default_partner_address,
197         'email_from': crm.crm_case. _get_default_email,
198         'state': 'draft',
199         'section_id': crm.crm_case. _get_section,
200         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
201         'priority': crm.AVAILABLE_PRIORITIES[2][0],
202         'project_id':_get_project,
203     }
204
205     def convert_issue_task(self, cr, uid, ids, context=None):
206         case_obj = self.pool.get('project.issue')
207         data_obj = self.pool.get('ir.model.data')
208         task_obj = self.pool.get('project.task')
209
210         if context is None:
211             context = {}
212
213         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
214         res = data_obj.read(cr, uid, result, ['res_id'])
215         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
216         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
217         if id2:
218             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
219         if id3:
220             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
221
222         for bug in case_obj.browse(cr, uid, ids, context=context):
223             new_task_id = task_obj.create(cr, uid, {
224                 'name': bug.name,
225                 'partner_id': bug.partner_id.id,
226                 'description':bug.description,
227                 'date': bug.date,
228                 'project_id': bug.project_id.id,
229                 'priority': bug.priority,
230                 'user_id': bug.assigned_to.id,
231                 'planned_hours': 0.0,
232             })
233
234             vals = {
235                 'task_id': new_task_id,
236             }
237             case_obj.write(cr, uid, [bug.id], vals)
238
239         return  {
240             'name': _('Tasks'),
241             'view_type': 'form',
242             'view_mode': 'form,tree',
243             'res_model': 'project.task',
244             'res_id': int(new_task_id),
245             'view_id': False,
246             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
247             'type': 'ir.actions.act_window',
248             'search_view_id': res['res_id'],
249             'nodestroy': True
250         }
251
252     def _convert(self, cr, uid, ids, xml_id, context=None):
253         data_obj = self.pool.get('ir.model.data')
254         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
255         categ_id = False
256         if id2:
257             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
258         if categ_id:
259             self.write(cr, uid, ids, {'categ_id': categ_id})
260         return True
261
262     def convert_to_feature(self, cr, uid, ids, context=None):
263         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
264
265     def convert_to_bug(self, cr, uid, ids, context=None):
266         return self._convert(cr, uid, ids, 'bug_categ', context=context)
267
268     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
269         if context is None:
270             context = {}
271         if not stage_id:
272             return {'value':{}}
273         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
274         if not stage.on_change:
275             return {'value':{}}
276         return {'value':{}}
277
278     def case_escalate(self, cr, uid, ids, *args):
279         """Escalates case to top level
280         @param self: The object pointer
281         @param cr: the current row, from the database cursor,
282         @param uid: the current user’s ID for security checks,
283         @param ids: List of case Ids
284         @param *args: Tuple Value for additional Params
285         """
286         cases = self.browse(cr, uid, ids)
287         for case in cases:
288             data = {}
289             if case.project_id.project_escalation_id:
290                 data['project_id'] = case.project_id.project_escalation_id.id
291                 if case.project_id.project_escalation_id.user_id:
292                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
293                 if case.task_id:
294                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
295             else:
296                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
297             self.write(cr, uid, [case.id], data)
298         return True
299
300     def message_new(self, cr, uid, msg, context):
301         """
302         Automatically calls when new email message arrives
303
304         @param self: The object pointer
305         @param cr: the current row, from the database cursor,
306         @param uid: the current user’s ID for security checks
307         """
308
309         mailgate_pool = self.pool.get('email.server.tools')
310
311         subject = msg.get('subject') or _('No Title')
312         body = msg.get('body')
313         msg_from = msg.get('from')
314         priority = msg.get('priority')
315
316         vals = {
317             'name': subject,
318             'email_from': msg_from,
319             'email_cc': msg.get('cc'),
320             'description': body,
321             'user_id': False,
322         }
323         if msg.get('priority', False):
324             vals['priority'] = priority
325
326         res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
327         if res:
328             vals.update(res)
329         context.update({'state_to' : 'draft'})
330         res = self.create(cr, uid, vals, context)
331         message = _('An Issue created') + " '" + subject + "' " + _("from Mailgate.")
332         self.log(cr, uid, res, message)
333         self.convert_to_bug(cr, uid, [res], context=context)
334
335         attachents = msg.get('attachments', [])
336         for attactment in attachents or []:
337             data_attach = {
338                 'name': attactment,
339                 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
340                 'datas_fname': attactment,
341                 'description': 'Mail attachment',
342                 'res_model': self._name,
343                 'res_id': res,
344             }
345             self.pool.get('ir.attachment').create(cr, uid, data_attach)
346
347         return res
348
349     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
350         if context is None:
351             context = {}
352         """
353         @param self: The object pointer
354         @param cr: the current row, from the database cursor,
355         @param uid: the current user’s ID for security checks,
356         @param ids: List of update mail’s IDs
357         """
358
359         if isinstance(ids, (str, int, long)):
360             ids = [ids]
361
362         vals.update({
363             'description': msg['body']
364         })
365         if msg.get('priority', False):
366             vals['priority'] = msg.get('priority')
367
368         maps = {
369             'cost': 'planned_cost',
370             'revenue': 'planned_revenue',
371             'probability': 'probability'
372         }
373         vls = { }
374         for line in msg['body'].split('\n'):
375             line = line.strip()
376             res = tools.misc.command_re.match(line)
377             if res and maps.get(res.group(1).lower(), False):
378                 key = maps.get(res.group(1).lower())
379                 vls[key] = res.group(2).lower()
380
381         vals.update(vls)
382         res = self.write(cr, uid, ids, vals)
383         return res
384
385     def msg_send(self, cr, uid, id, *args, **argv):
386
387         """ Send The Message
388             @param self: The object pointer
389             @param cr: the current row, from the database cursor,
390             @param uid: the current user’s ID for security checks,
391             @param ids: List of email’s IDs
392             @param *args: Return Tuple Value
393             @param **args: Return Dictionary of Keyword Value
394         """
395         return True
396
397 project_issue()
398
399 class project(osv.osv):
400     _inherit = "project.project"
401     _columns = {
402         '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)]}),
403         '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)]}),
404         'reply_to' : fields.char('Reply-To Email Address', size=256)
405     }
406
407     def _check_escalation(self, cr, uid, ids):
408          project_obj = self.browse(cr, uid, ids[0])
409          if project_obj.project_escalation_id:
410              if project_obj.project_escalation_id.id == project_obj.id:
411                  return False
412          return True
413
414     _constraints = [
415         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
416     ]
417 project()
418
419 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: