2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
23 from datetime import datetime
24 from osv import fields,osv
25 from tools.translate import _
31 class project_issue_version(osv.osv):
32 _name = "project.issue.version"
35 'name': fields.char('Version Number', size=32, required=True),
36 'active': fields.boolean('Active', required=False),
41 project_issue_version()
43 class project_issue(crm.crm_case, osv.osv):
44 _name = "project.issue"
45 _description = "Project Issue"
46 _order = "priority, id desc"
47 _inherit = ['mailgate.thread']
49 def case_open(self, cr, uid, ids, *args):
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
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')})
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)
65 def case_close(self, cr, uid, ids, *args):
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
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)
80 def _compute_day(self, cr, uid, ids, fields, args, context=None):
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
88 cal_obj = self.pool.get('resource.calendar')
89 res_obj = self.pool.get('resource.resource')
92 for issue in self.browse(cr, uid, ids, context=context):
99 if field in ['working_hours_open','day_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'))
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)
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:
136 duration = len(no_days)
137 if field in ['working_hours_open','working_hours_close']:
138 res[issue.id][field] = hours
140 res[issue.id][field] = abs(float(duration))
143 def _get_issue_task(self, cr, uid, ids, context=None):
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)])
150 def _get_issue_work(self, cr, uid, ids, context=None):
152 issue_pool = self.pool.get('project.issue')
153 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
155 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
158 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
159 task_pool = self.pool.get('project.task')
161 for issue in self.browse(cr, uid, ids, context=context):
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}
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='day_open', type="float", store=True),
209 'day_close': fields.function(_compute_day, string='Days to Close', \
210 method=True, multi='day_close', type="float", store=True),
211 'assigned_to': fields.many2one('res.users', 'Assigned to', required=True, select=1),
212 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
213 method=True, multi='working_days_open', type="float", store=True),
214 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
215 method=True, multi='working_days_close', 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, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
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),
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
235 'user_id': crm.crm_case._get_default_user,
236 'partner_id': crm.crm_case._get_default_partner,
237 'partner_address_id': crm.crm_case._get_default_partner_address,
238 'email_from': crm.crm_case. _get_default_email,
240 'section_id': crm.crm_case. _get_section,
241 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
242 'priority': crm.AVAILABLE_PRIORITIES[2][0],
243 'project_id':_get_project,
244 'assigned_to' : lambda obj, cr, uid, context: uid,
247 def convert_issue_task(self, cr, uid, ids, context=None):
248 case_obj = self.pool.get('project.issue')
249 data_obj = self.pool.get('ir.model.data')
250 task_obj = self.pool.get('project.task')
256 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
257 res = data_obj.read(cr, uid, result, ['res_id'])
258 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
259 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
261 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
263 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
265 for bug in case_obj.browse(cr, uid, ids, context=context):
266 new_task_id = task_obj.create(cr, uid, {
268 'partner_id': bug.partner_id.id,
269 'description':bug.description,
271 'project_id': bug.project_id.id,
272 'priority': bug.priority,
273 'user_id': bug.assigned_to.id,
274 'planned_hours': 0.0,
278 'task_id': new_task_id,
281 case_obj.write(cr, uid, [bug.id], vals)
286 'view_mode': 'form,tree',
287 'res_model': 'project.task',
288 'res_id': int(new_task_id),
290 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
291 'type': 'ir.actions.act_window',
292 'search_view_id': res['res_id'],
297 def _convert(self, cr, uid, ids, xml_id, context=None):
298 data_obj = self.pool.get('ir.model.data')
299 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
302 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
304 self.write(cr, uid, ids, {'categ_id': categ_id})
307 def convert_to_feature(self, cr, uid, ids, context=None):
308 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
310 def convert_to_bug(self, cr, uid, ids, context=None):
311 return self._convert(cr, uid, ids, 'bug_categ', context=context)
313 def next_type(self, cr, uid, ids, *args):
314 for task in self.browse(cr, uid, ids):
315 typeid = task.type_id.id
316 types = map(lambda x:x.id, task.project_id.type_ids or [])
319 self.write(cr, uid, task.id, {'type_id': types[0]})
320 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
321 index = types.index(typeid)
322 self.write(cr, uid, task.id, {'type_id': types[index+1]})
325 def prev_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 and task.project_id.type_ids or [])
330 if typeid and typeid in types:
331 index = types.index(typeid)
332 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
336 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
340 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
341 return {'value':{'assigned_to': task.user_id.id,}}
343 def case_escalate(self, cr, uid, ids, *args):
344 """Escalates case to top level
345 @param self: The object pointer
346 @param cr: the current row, from the database cursor,
347 @param uid: the current user’s ID for security checks,
348 @param ids: List of case Ids
349 @param *args: Tuple Value for additional Params
351 cases = self.browse(cr, uid, ids)
354 if case.project_id.project_escalation_id:
355 data['project_id'] = case.project_id.project_escalation_id.id
356 if case.project_id.project_escalation_id.user_id:
357 data['user_id'] = case.project_id.project_escalation_id.user_id.id
359 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
361 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
362 self.write(cr, uid, [case.id], data)
363 self._history(cr, uid, cases, _('Escalate'))
366 def message_new(self, cr, uid, msg, context=None):
368 Automatically calls when new email message arrives
370 @param self: The object pointer
371 @param cr: the current row, from the database cursor,
372 @param uid: the current user’s ID for security checks
376 mailgate_pool = self.pool.get('email.server.tools')
378 subject = msg.get('subject') or _('No Title')
379 body = msg.get('body')
380 msg_from = msg.get('from')
381 priority = msg.get('priority')
385 'email_from': msg_from,
386 'email_cc': msg.get('cc'),
390 if msg.get('priority', False):
391 vals['priority'] = priority
393 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
396 context.update({'state_to' : 'draft'})
397 res = self.create(cr, uid, vals, context=context)
398 self.convert_to_bug(cr, uid, [res], context=context)
400 attachents = msg.get('attachments', [])
401 for attactment in attachents or []:
404 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
405 'datas_fname': attactment,
406 'description': 'Mail attachment',
407 'res_model': self._name,
410 self.pool.get('ir.attachment').create(cr, uid, data_attach)
414 def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
416 @param self: The object pointer
417 @param cr: the current row, from the database cursor,
418 @param uid: the current user’s ID for security checks,
419 @param ids: List of update mail’s IDs
425 if isinstance(ids, (str, int, long)):
429 'description': msg['body']
431 if msg.get('priority', False):
432 vals['priority'] = msg.get('priority')
435 'cost': 'planned_cost',
436 'revenue': 'planned_revenue',
437 'probability': 'probability'
440 # Reassign the 'open' state to the case if this one is in pending or done
441 for record in self.browse(cr, uid, ids, context=context):
442 if record.state in ('pending', 'done'):
443 record.write({'state' : 'open'})
446 for line in msg['body'].split('\n'):
448 res = tools.misc.command_re.match(line)
449 if res and maps.get(res.group(1).lower(), False):
450 key = maps.get(res.group(1).lower())
451 vls[key] = res.group(2).lower()
454 res = self.write(cr, uid, ids, vals)
457 def msg_send(self, cr, uid, id, *args, **argv):
460 @param self: The object pointer
461 @param cr: the current row, from the database cursor,
462 @param uid: the current user’s ID for security checks,
463 @param ids: List of email’s IDs
464 @param *args: Return Tuple Value
465 @param **args: Return Dictionary of Keyword Value
469 def copy(self, cr, uid, id, default=None, context=None):
470 issue = self.read(cr, uid, id, ['name'], context=context)
473 default = default.copy()
474 default['name'] = issue['name'] + _(' (copy)')
475 return super(project_issue, self).copy(cr, uid, id, default=default,
480 class project(osv.osv):
481 _inherit = "project.project"
483 '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)]}),
484 '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)]}),
485 'reply_to' : fields.char('Reply-To Email Address', size=256)
488 def _check_escalation(self, cr, uid, ids, context=None):
489 project_obj = self.browse(cr, uid, ids[0], context=context)
490 if project_obj.project_escalation_id:
491 if project_obj.project_escalation_id.id == project_obj.id:
496 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
500 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: