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