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