[IMP] removed method=True params from all fields.function
[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                 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.related('project_id', 'user_id', type='many2one', relation='res.users', store=True, select=1, string='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', domain="[('project_ids', '=', project_id)]"),
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='compute_day', type="float", store=True),
209         'day_close': fields.function(_compute_day, string='Days to Close', \
210                                 method=True, multi='compute_day', type="float", store=True),
211         'assigned_to': fields.many2one('res.users', 'Assigned to', required=False, select=1),
212         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
213                                 method=True, multi='compute_day', type="float", store=True),
214         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
215                                 method=True, multi='compute_day', 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, 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     def on_change_project(self, cr, uid, ids, project_id, context=None):
234         result = {}
235
236         if project_id:
237             project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
238             if project.user_id:
239                 result['value'] = {'user_id' : project.user_id.id}
240
241         return result
242
243
244     _defaults = {
245         'active': 1,
246         #'user_id': crm.crm_case._get_default_user,
247         'partner_id': crm.crm_case._get_default_partner,
248         'partner_address_id': crm.crm_case._get_default_partner_address,
249         'email_from': crm.crm_case._get_default_email,
250         'state': 'draft',
251         'section_id': crm.crm_case._get_section,
252         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
253         'priority': crm.AVAILABLE_PRIORITIES[2][0],
254         'project_id':_get_project,
255         'categ_id' : lambda *a: False,
256         #'assigned_to' : lambda obj, cr, uid, context: uid,
257     }
258
259     def convert_issue_task(self, cr, uid, ids, context=None):
260         case_obj = self.pool.get('project.issue')
261         data_obj = self.pool.get('ir.model.data')
262         task_obj = self.pool.get('project.task')
263
264
265         if context is None:
266             context = {}
267
268         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
269         res = data_obj.read(cr, uid, result, ['res_id'])
270         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
271         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
272         if id2:
273             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
274         if id3:
275             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
276
277         for bug in case_obj.browse(cr, uid, ids, context=context):
278             new_task_id = task_obj.create(cr, uid, {
279                 'name': bug.name,
280                 'partner_id': bug.partner_id.id,
281                 'description':bug.description,
282                 'date': bug.date,
283                 'project_id': bug.project_id.id,
284                 'priority': bug.priority,
285                 'user_id': bug.assigned_to.id,
286                 'planned_hours': 0.0,
287             })
288
289             vals = {
290                 'task_id': new_task_id,
291                 'state':'pending'
292             }
293             case_obj.write(cr, uid, [bug.id], vals)
294
295         return  {
296             'name': _('Tasks'),
297             'view_type': 'form',
298             'view_mode': 'form,tree',
299             'res_model': 'project.task',
300             'res_id': int(new_task_id),
301             'view_id': False,
302             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
303             'type': 'ir.actions.act_window',
304             'search_view_id': res['res_id'],
305             'nodestroy': True
306         }
307
308
309     def _convert(self, cr, uid, ids, xml_id, context=None):
310         data_obj = self.pool.get('ir.model.data')
311         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
312         categ_id = False
313         if id2:
314             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
315         if categ_id:
316             self.write(cr, uid, ids, {'categ_id': categ_id})
317         return True
318
319     def convert_to_feature(self, cr, uid, ids, context=None):
320         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
321
322     def convert_to_bug(self, cr, uid, ids, context=None):
323         return self._convert(cr, uid, ids, 'bug_categ', context=context)
324
325     def next_type(self, cr, uid, ids, *args):
326         for task in self.browse(cr, uid, ids):
327             typeid = task.type_id.id
328             types = map(lambda x:x.id, task.project_id.type_ids or [])
329             if types:
330                 if not typeid:
331                     self.write(cr, uid, task.id, {'type_id': types[0]})
332                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
333                     index = types.index(typeid)
334                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
335         return True
336
337     def prev_type(self, cr, uid, ids, *args):
338         for task in self.browse(cr, uid, ids):
339             typeid = task.type_id.id
340             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
341             if types:
342                 if typeid and typeid in types:
343                     index = types.index(typeid)
344                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
345         return True
346
347
348     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
349         result = {}
350         if not task_id:
351             return {'value':{}}
352         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
353         return {'value':{'assigned_to': task.user_id.id,}}
354
355     def case_escalate(self, cr, uid, ids, *args):
356         """Escalates case to top level
357         @param self: The object pointer
358         @param cr: the current row, from the database cursor,
359         @param uid: the current user’s ID for security checks,
360         @param ids: List of case Ids
361         @param *args: Tuple Value for additional Params
362         """
363         cases = self.browse(cr, uid, ids)
364         for case in cases:
365             data = {}
366             if case.project_id.project_escalation_id:
367                 data['project_id'] = case.project_id.project_escalation_id.id
368                 if case.project_id.project_escalation_id.user_id:
369                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
370                 if case.task_id:
371                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
372             else:
373                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
374             self.write(cr, uid, [case.id], data)
375         self._history(cr, uid, cases, _('Escalate'))
376         return True
377
378     def message_new(self, cr, uid, msg, context=None):
379         """
380         Automatically calls when new email message arrives
381
382         @param self: The object pointer
383         @param cr: the current row, from the database cursor,
384         @param uid: the current user’s ID for security checks
385         """
386         if context is None: 
387             context = {}
388         mailgate_pool = self.pool.get('email.server.tools')
389
390         subject = msg.get('subject') or _('No Title')
391         body = msg.get('body')
392         msg_from = msg.get('from')
393         priority = msg.get('priority')
394
395         vals = {
396             'name': subject,
397             'email_from': msg_from,
398             'email_cc': msg.get('cc'),
399             'description': body,
400             'user_id': False,
401         }
402         if msg.get('priority', False):
403             vals['priority'] = priority
404
405         res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
406         if res:
407             vals.update(res)
408         context.update({'state_to' : 'draft'})
409         res = self.create(cr, uid, vals, context=context)
410         self.convert_to_bug(cr, uid, [res], context=context)
411
412         attachents = msg.get('attachments', [])
413         for attactment in attachents or []:
414             data_attach = {
415                 'name': attactment,
416                 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
417                 'datas_fname': attactment,
418                 'description': 'Mail attachment',
419                 'res_model': self._name,
420                 'res_id': res,
421             }
422             self.pool.get('ir.attachment').create(cr, uid, data_attach)
423
424         return res
425
426     def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
427         """
428         @param self: The object pointer
429         @param cr: the current row, from the database cursor,
430         @param uid: the current user’s ID for security checks,
431         @param ids: List of update mail’s IDs
432         """
433
434         if vals is None:
435             vals = {}
436
437         if isinstance(ids, (str, int, long)):
438             ids = [ids]
439
440         vals.update({
441             'description': msg['body']
442         })
443         if msg.get('priority', False):
444             vals['priority'] = msg.get('priority')
445
446         maps = {
447             'cost': 'planned_cost',
448             'revenue': 'planned_revenue',
449             'probability': 'probability'
450         }
451
452         # Reassign the 'open' state to the case if this one is in pending or done
453         for record in self.browse(cr, uid, ids, context=context):
454             if record.state in ('pending', 'done'):
455                 record.write({'state' : 'open'})
456
457         vls = { }
458         for line in msg['body'].split('\n'):
459             line = line.strip()
460             res = tools.misc.command_re.match(line)
461             if res and maps.get(res.group(1).lower(), False):
462                 key = maps.get(res.group(1).lower())
463                 vls[key] = res.group(2).lower()
464
465         vals.update(vls)
466         res = self.write(cr, uid, ids, vals)
467         return res
468
469     def msg_send(self, cr, uid, id, *args, **argv):
470
471         """ Send The Message
472             @param self: The object pointer
473             @param cr: the current row, from the database cursor,
474             @param uid: the current user’s ID for security checks,
475             @param ids: List of email’s IDs
476             @param *args: Return Tuple Value
477             @param **args: Return Dictionary of Keyword Value
478         """
479         return True
480
481     def copy(self, cr, uid, id, default=None, context=None):
482         issue = self.read(cr, uid, id, ['name'], context=context)
483         if not default:
484             default = {}
485         default = default.copy()
486         default['name'] = issue['name'] + _(' (copy)')
487         return super(project_issue, self).copy(cr, uid, id, default=default,
488                 context=context)
489
490 project_issue()
491
492 class project(osv.osv):
493     _inherit = "project.project"
494     _columns = {
495         '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)]}),
496         '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)]}),
497         'reply_to' : fields.char('Reply-To Email Address', size=256)
498     }
499
500     def _check_escalation(self, cr, uid, ids, context=None):
501          project_obj = self.browse(cr, uid, ids[0], context=context)
502          if project_obj.project_escalation_id:
503              if project_obj.project_escalation_id.id == project_obj.id:
504                  return False
505          return True
506
507     _constraints = [
508         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
509     ]
510 project()
511
512 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: