[fix] problem in function field not returning correctly all fields
[odoo/odoo.git] / addons / project_issue / project_issue.py
1  #-*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from crm import crm
23 from datetime import datetime
24 from osv import fields,osv
25 from tools.translate import _
26 import binascii
27 import time
28 import tools
29 from crm import wizard
30
31 wizard.mail_compose_message.SUPPORTED_MODELS.append('project.issue')
32
33 class project_issue_version(osv.osv):
34     _name = "project.issue.version"
35     _order = "name desc"
36     _columns = {
37         'name': fields.char('Version Number', size=32, required=True),
38         'active': fields.boolean('Active', required=False),
39     }
40     _defaults = {
41         'active': 1,
42     }
43 project_issue_version()
44
45 class project_issue(crm.crm_case, osv.osv):
46     _name = "project.issue"
47     _description = "Project Issue"
48     _order = "priority, create_date desc"
49     _inherit = ['mail.thread']
50
51     def case_open(self, cr, uid, ids, *args):
52         """
53         @param self: The object pointer
54         @param cr: the current row, from the database cursor,
55         @param uid: the current user’s ID for security checks,
56         @param ids: List of case's Ids
57         @param *args: Give Tuple Value
58         """
59
60         res = super(project_issue, self).case_open(cr, uid, ids, *args)
61         self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
62         for (id, name) in self.name_get(cr, uid, ids):
63             message = _("Issue '%s' has been opened.") % name
64             self.log(cr, uid, id, message)
65         return res
66
67     def case_close(self, cr, uid, ids, *args):
68         """
69         @param self: The object pointer
70         @param cr: the current row, from the database cursor,
71         @param uid: the current user’s ID for security checks,
72         @param ids: List of case's Ids
73         @param *args: Give Tuple Value
74         """
75
76         res = super(project_issue, self).case_close(cr, uid, ids, *args)
77         for (id, name) in self.name_get(cr, uid, ids):
78             message = _("Issue '%s' has been closed.") % name
79             self.log(cr, uid, id, message)
80         return res
81
82     def _compute_day(self, cr, uid, ids, fields, args, context=None):
83         """
84         @param cr: the current row, from the database cursor,
85         @param uid: the current user’s ID for security checks,
86         @param ids: List of Openday’s IDs
87         @return: difference between current date and log date
88         @param context: A standard dictionary for contextual values
89         """
90         cal_obj = self.pool.get('resource.calendar')
91         res_obj = self.pool.get('resource.resource')
92
93         res = {}
94         for issue in self.browse(cr, uid, ids, context=context):
95             res[issue.id] = {}
96             for field in fields:
97                 duration = 0
98                 ans = False
99                 hours = 0
100
101                 if field in ['working_hours_open','day_open']:
102                     if issue.date_open:
103                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
104                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
105                         ans = date_open - date_create
106                         date_until = issue.date_open
107                         #Calculating no. of working hours to open the issue
108                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
109                                  datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
110                                  datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
111                 elif field in ['working_hours_close','day_close']:
112                     if issue.date_closed:
113                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
114                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
115                         date_until = issue.date_closed
116                         ans = date_close - date_create
117                         #Calculating no. of working hours to close the issue
118                         hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
119                                 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
120                                 datetime.strptime(issue.date_closed, '%Y-%m-%d %H:%M:%S'))
121                 elif field in ['days_since_creation']:
122                     if issue.create_date:
123                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
124                         res[issue.id][field] = days_since_creation.days
125                     continue
126
127                 elif field in ['inactivity_days']:
128                     res[issue.id][field] = 0
129                     if issue.date_action_last:
130                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
131                         res[issue.id][field] = inactive_days.days
132                     continue
133                 if ans:
134                     resource_id = False
135                     if issue.user_id:
136                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
137                         if resource_ids and len(resource_ids):
138                             resource_id = resource_ids[0]
139                     duration = float(ans.days)
140                     if issue.project_id and issue.project_id.resource_calendar_id:
141                         duration = float(ans.days) * 24
142                         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)
143                         no_days = []
144                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
145                         for in_time, out_time in new_dates:
146                             if in_time.date not in no_days:
147                                 no_days.append(in_time.date)
148                             if out_time > date_until:
149                                 break
150                         duration = len(no_days)
151                 if field in ['working_hours_open','working_hours_close']:
152                     res[issue.id][field] = hours
153                 else:
154                     res[issue.id][field] = abs(float(duration))
155         return res
156
157     def _get_issue_task(self, cr, uid, ids, context=None):
158         issues = []
159         issue_pool = self.pool.get('project.issue')
160         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
161             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
162         return issues
163
164     def _get_issue_work(self, cr, uid, ids, context=None):
165         issues = []
166         issue_pool = self.pool.get('project.issue')
167         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
168             if work.task_id:
169                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
170         return issues
171
172     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
173         task_pool = self.pool.get('project.task')
174         res = {}
175         for issue in self.browse(cr, uid, ids, context=context):
176             progress = 0.0
177             if issue.task_id:
178                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
179             res[issue.id] = {'progress' : progress}
180         return res
181
182     _columns = {
183         'id': fields.integer('ID', readonly=True),
184         'name': fields.char('Issue', size=128, required=True),
185         'active': fields.boolean('Active', required=False),
186         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
187         'write_date': fields.datetime('Update Date', readonly=True),
188         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
189                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
190         'date_deadline': fields.date('Deadline'),
191         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
192                         select=True, help='Sales team to which Case belongs to.\
193                              Define Responsible user and Email account for mail gateway.'),
194         'partner_id': fields.many2one('res.partner', 'Partner', select=1),
195         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
196                                  domain="[('partner_id','=',partner_id)]"),
197         'company_id': fields.many2one('res.company', 'Company'),
198         'description': fields.text('Description'),
199         'state': fields.selection([('draft', 'New'), ('open', 'To Solve'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
200                                   help='The state is set to \'Draft\', when a case is created.\
201                                   \nIf the case is in progress the state is set to \'Open\'.\
202                                   \nWhen the case is over, the state is set to \'Done\'.\
203                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
204         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
205         '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"),
206         'date_open': fields.datetime('Opened', readonly=True,select=True),
207         # Project Issue fields
208         'date_closed': fields.datetime('Closed', readonly=True,select=True),
209         'date': fields.datetime('Date'),
210         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
211         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
212         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
213         'version_id': fields.many2one('project.issue.version', 'Version'),
214         'type_id': fields.many2one ('project.task.type', 'Resolution', domain="[('project_ids', '=', project_id)]"),
215         'project_id':fields.many2one('project.project', 'Project'),
216         'duration': fields.float('Duration'),
217         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
218         'day_open': fields.function(_compute_day, string='Days to Open', \
219                                 multi='compute_day', type="float", store=True),
220         'day_close': fields.function(_compute_day, string='Days to Close', \
221                                 multi='compute_day', type="float", store=True),
222         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
223         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
224                                 multi='compute_day', type="float", store=True),
225         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
226                                 multi='compute_day', type="float", store=True),
227         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
228                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
229         'color': fields.integer('Color Index'),
230         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
231         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
232         'date_action_last': fields.datetime('Last Action', readonly=1),
233         'date_action_next': fields.datetime('Next Action', readonly=1),
234         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
235             store = {
236                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
237                 'project.task': (_get_issue_task, ['progress'], 10),
238                 'project.task.work': (_get_issue_work, ['hours'], 10),
239             }),
240     }
241
242     def _get_project(self, cr, uid, context=None):
243         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
244         if user.context_project_id:
245             return user.context_project_id.id
246         return False
247
248     def on_change_project(self, cr, uid, ids, project_id, context=None):
249         return {}
250
251
252     _defaults = {
253         'active': 1,
254         'partner_id': crm.crm_case._get_default_partner,
255         'partner_address_id': crm.crm_case._get_default_partner_address,
256         'email_from': crm.crm_case._get_default_email,
257         'state': 'draft',
258         'section_id': crm.crm_case._get_section,
259         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
260         'priority': crm.AVAILABLE_PRIORITIES[2][0],
261         'project_id':_get_project,
262         'categ_id' : lambda *a: False,
263          }
264
265     def set_priority(self, cr, uid, ids, priority):
266         """Set lead priority
267         """
268         return self.write(cr, uid, ids, {'priority' : priority})
269
270     def set_high_priority(self, cr, uid, ids, *args):
271         """Set lead priority to high
272         """
273         return self.set_priority(cr, uid, ids, '1')
274
275     def set_normal_priority(self, cr, uid, ids, *args):
276         """Set lead priority to normal
277         """
278         return self.set_priority(cr, uid, ids, '3')
279
280     def convert_issue_task(self, cr, uid, ids, context=None):
281         case_obj = self.pool.get('project.issue')
282         data_obj = self.pool.get('ir.model.data')
283         task_obj = self.pool.get('project.task')
284
285
286         if context is None:
287             context = {}
288
289         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
290         res = data_obj.read(cr, uid, result, ['res_id'])
291         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
292         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
293         if id2:
294             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
295         if id3:
296             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
297
298         for bug in case_obj.browse(cr, uid, ids, context=context):
299             new_task_id = task_obj.create(cr, uid, {
300                 'name': bug.name,
301                 'partner_id': bug.partner_id.id,
302                 'description':bug.description,
303                 'date': bug.date,
304                 'project_id': bug.project_id.id,
305                 'priority': bug.priority,
306                 'user_id': bug.user_id.id,
307                 'planned_hours': 0.0,
308             })
309
310             vals = {
311                 'task_id': new_task_id,
312                 'state':'pending'
313             }
314             case_obj.write(cr, uid, [bug.id], vals)
315
316         return  {
317             'name': _('Tasks'),
318             'view_type': 'form',
319             'view_mode': 'form,tree',
320             'res_model': 'project.task',
321             'res_id': int(new_task_id),
322             'view_id': False,
323             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
324             'type': 'ir.actions.act_window',
325             'search_view_id': res['res_id'],
326             'nodestroy': True
327         }
328
329
330     def _convert(self, cr, uid, ids, xml_id, context=None):
331         data_obj = self.pool.get('ir.model.data')
332         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
333         categ_id = False
334         if id2:
335             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
336         if categ_id:
337             self.write(cr, uid, ids, {'categ_id': categ_id})
338         return True
339
340     def convert_to_feature(self, cr, uid, ids, context=None):
341         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
342
343     def convert_to_bug(self, cr, uid, ids, context=None):
344         return self._convert(cr, uid, ids, 'bug_categ', context=context)
345
346     def next_type(self, cr, uid, ids, *args):
347         for task in self.browse(cr, uid, ids):
348             typeid = task.type_id.id
349             types = map(lambda x:x.id, task.project_id.type_ids or [])
350             if types:
351                 if not typeid:
352                     self.write(cr, uid, task.id, {'type_id': types[0]})
353                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
354                     index = types.index(typeid)
355                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
356         return True
357
358     def prev_type(self, cr, uid, ids, *args):
359         for task in self.browse(cr, uid, ids):
360             typeid = task.type_id.id
361             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
362             if types:
363                 if typeid and typeid in types:
364                     index = types.index(typeid)
365                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
366         return True
367
368
369     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
370         result = {}
371         if not task_id:
372             return {'value':{}}
373         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
374         return {'value':{'user_id': task.user_id.id,}}
375
376     def case_escalate(self, cr, uid, ids, *args):
377         """Escalates case to top level
378         @param self: The object pointer
379         @param cr: the current row, from the database cursor,
380         @param uid: the current user’s ID for security checks,
381         @param ids: List of case Ids
382         @param *args: Tuple Value for additional Params
383         """
384         cases = self.browse(cr, uid, ids)
385         for case in cases:
386             data = {}
387             if case.project_id.project_escalation_id:
388                 data['project_id'] = case.project_id.project_escalation_id.id
389                 if case.project_id.project_escalation_id.user_id:
390                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
391                 if case.task_id:
392                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
393             else:
394                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
395             self.write(cr, uid, [case.id], data)
396         self.message_append(cr, uid, cases, _('Escalate'))
397         return True
398
399     def message_new(self, cr, uid, msg, custom_values=None, context=None):
400         """Automatically called when new email message arrives"""
401         if context is None:
402             context = {}
403         subject = msg.get('subject') or _('No Title')
404         body = msg.get('body_text')
405         msg_from = msg.get('from')
406         priority = msg.get('priority')
407         vals = {
408             'name': subject,
409             'email_from': msg_from,
410             'email_cc': msg.get('cc'),
411             'description': body,
412             'user_id': False,
413         }
414         if priority:
415             vals['priority'] = priority
416         vals.update(self.message_partner_by_email(cr, uid, msg_from))
417         context.update({'state_to' : 'draft'})
418
419         if custom_values and isinstance(custom_values, dict):
420             vals.update(custom_values)
421
422         res_id = self.create(cr, uid, vals, context)
423         self.message_append_dict(cr, uid, [res_id], msg, context=context)
424         self.convert_to_bug(cr, uid, [res_id], context=context)
425         return res_id
426
427     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
428
429         if vals is None:
430             vals = {}
431
432         if isinstance(ids, (str, int, long)):
433             ids = [ids]
434
435         vals.update({
436             'description': msg['body_text']
437         })
438         if msg.get('priority', False):
439             vals['priority'] = msg.get('priority')
440
441         maps = {
442             'cost': 'planned_cost',
443             'revenue': 'planned_revenue',
444             'probability': 'probability'
445         }
446
447         # Reassign the 'open' state to the case if this one is in pending or done
448         for record in self.browse(cr, uid, ids, context=context):
449             if record.state in ('pending', 'done'):
450                 record.write({'state' : 'open'})
451
452         vls = { }
453         for line in msg['body_text'].split('\n'):
454             line = line.strip()
455             res = tools.misc.command_re.match(line)
456             if res and maps.get(res.group(1).lower(), False):
457                 key = maps.get(res.group(1).lower())
458                 vls[key] = res.group(2).lower()
459
460         vals.update(vls)
461         res = self.write(cr, uid, ids, vals)
462         self.message_append_dict(cr, uid, ids, msg, context=context)
463         return res
464
465     def copy(self, cr, uid, id, default=None, context=None):
466         issue = self.read(cr, uid, id, ['name'], context=context)
467         if not default:
468             default = {}
469         default = default.copy()
470         default['name'] = issue['name'] + _(' (copy)')
471         return super(project_issue, self).copy(cr, uid, id, default=default,
472                 context=context)
473
474 project_issue()
475
476 class project(osv.osv):
477     _inherit = "project.project"
478     _columns = {
479         '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)]}),
480         '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)]}),
481         'reply_to' : fields.char('Reply-To Email Address', size=256)
482     }
483
484     def _check_escalation(self, cr, uid, ids, context=None):
485          project_obj = self.browse(cr, uid, ids[0], context=context)
486          if project_obj.project_escalation_id:
487              if project_obj.project_escalation_id.id == project_obj.id:
488                  return False
489          return True
490
491     _constraints = [
492         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
493     ]
494 project()
495
496 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: