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 _
29 from crm import wizard
31 wizard.mail_compose_message.SUPPORTED_MODELS.append('project.issue')
33 class project_issue_version(osv.osv):
34 _name = "project.issue.version"
37 'name': fields.char('Version Number', size=32, required=True),
38 'active': fields.boolean('Active', required=False),
43 project_issue_version()
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 = ['ir.needaction', 'mail.thread']
51 def _compute_day(self, cr, uid, ids, fields, args, context=None):
53 @param cr: the current row, from the database cursor,
54 @param uid: the current user’s ID for security checks,
55 @param ids: List of Openday’s IDs
56 @return: difference between current date and log date
57 @param context: A standard dictionary for contextual values
59 cal_obj = self.pool.get('resource.calendar')
60 res_obj = self.pool.get('resource.resource')
63 for issue in self.browse(cr, uid, ids, context=context):
70 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
71 if field in ['working_hours_open','day_open']:
73 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
74 ans = date_open - date_create
75 date_until = issue.date_open
76 #Calculating no. of working hours to open the issue
77 if issue.project_id.resource_calendar_id:
78 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
81 elif field in ['working_hours_close','day_close']:
83 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
84 date_until = issue.date_closed
85 ans = date_close - date_create
86 #Calculating no. of working hours to close the issue
87 if issue.project_id.resource_calendar_id:
88 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
91 elif field in ['days_since_creation']:
93 days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
94 res[issue.id][field] = days_since_creation.days
97 elif field in ['inactivity_days']:
98 res[issue.id][field] = 0
99 if issue.date_action_last:
100 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
101 res[issue.id][field] = inactive_days.days
106 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
107 if resource_ids and len(resource_ids):
108 resource_id = resource_ids[0]
109 duration = float(ans.days)
110 if issue.project_id and issue.project_id.resource_calendar_id:
111 duration = float(ans.days) * 24
113 new_dates = cal_obj.interval_min_get(cr, uid,
114 issue.project_id.resource_calendar_id.id,
116 duration, resource=resource_id)
118 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
119 for in_time, out_time in new_dates:
120 if in_time.date not in no_days:
121 no_days.append(in_time.date)
122 if out_time > date_until:
124 duration = len(no_days)
126 if field in ['working_hours_open','working_hours_close']:
127 res[issue.id][field] = hours
129 res[issue.id][field] = abs(float(duration))
133 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
134 task_pool = self.pool.get('project.task')
136 for issue in self.browse(cr, uid, ids, context=context):
139 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
140 res[issue.id] = {'progress' : progress}
143 def _get_project(self, cr, uid, context=None):
144 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
145 if user.context_project_id:
146 return user.context_project_id.id
149 def on_change_project(self, cr, uid, ids, project_id, context=None):
152 def _get_issue_task(self, cr, uid, ids, context=None):
154 issue_pool = self.pool.get('project.issue')
155 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
156 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
159 def _get_issue_work(self, cr, uid, ids, context=None):
161 issue_pool = self.pool.get('project.issue')
162 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
164 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
168 'id': fields.integer('ID', readonly=True),
169 'name': fields.char('Issue', size=128, required=True),
170 'active': fields.boolean('Active', required=False),
171 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
172 'write_date': fields.datetime('Update Date', readonly=True),
173 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
174 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
175 'date_deadline': fields.date('Deadline'),
176 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
177 select=True, help='Sales team to which Case belongs to.\
178 Define Responsible user and Email account for mail gateway.'),
179 'partner_id': fields.many2one('res.partner', 'Partner', select=1),
180 'company_id': fields.many2one('res.company', 'Company'),
181 'description': fields.text('Description'),
182 'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
183 help='The state is set to \'Draft\', when a case is created.\
184 \nIf the case is in progress the state is set to \'Open\'.\
185 \nWhen the case is over, the state is set to \'Done\'.\
186 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
187 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
188 '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"),
189 'date_open': fields.datetime('Opened', readonly=True,select=True),
190 # Project Issue fields
191 'date_closed': fields.datetime('Closed', readonly=True,select=True),
192 'date': fields.datetime('Date'),
193 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
194 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
195 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
196 'version_id': fields.many2one('project.issue.version', 'Version'),
197 'type_id': fields.many2one ('project.task.type', 'Stages', domain="[('project_ids', '=', project_id)]"),
198 'project_id':fields.many2one('project.project', 'Project'),
199 'duration': fields.float('Duration'),
200 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
201 'day_open': fields.function(_compute_day, string='Days to Open', \
202 multi='compute_day', type="float", store=True),
203 'day_close': fields.function(_compute_day, string='Days to Close', \
204 multi='compute_day', type="float", store=True),
205 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
206 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
207 multi='compute_day', type="float", store=True),
208 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
209 multi='compute_day', type="float", store=True),
210 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
211 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
212 'color': fields.integer('Color Index'),
213 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
214 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
215 'date_action_last': fields.datetime('Last Action', readonly=1),
216 'date_action_next': fields.datetime('Next Action', readonly=1),
217 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
219 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
220 'project.task': (_get_issue_task, ['progress'], 10),
221 'project.task.work': (_get_issue_work, ['hours'], 10),
228 'partner_id': crm.crm_case._get_default_partner,
229 'email_from': crm.crm_case._get_default_email,
231 'section_id': crm.crm_case._get_section,
232 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
233 'priority': crm.AVAILABLE_PRIORITIES[2][0],
234 'project_id':_get_project,
235 'categ_id' : lambda *a: False,
238 def set_priority(self, cr, uid, ids, priority):
241 return self.write(cr, uid, ids, {'priority' : priority})
243 def set_high_priority(self, cr, uid, ids, *args):
244 """Set lead priority to high
246 return self.set_priority(cr, uid, ids, '1')
248 def set_normal_priority(self, cr, uid, ids, *args):
249 """Set lead priority to normal
251 return self.set_priority(cr, uid, ids, '3')
253 def convert_issue_task(self, cr, uid, ids, context=None):
257 case_obj = self.pool.get('project.issue')
258 data_obj = self.pool.get('ir.model.data')
259 task_obj = self.pool.get('project.task')
261 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
262 res = data_obj.read(cr, uid, result, ['res_id'])
263 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
264 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
266 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
268 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
270 for bug in case_obj.browse(cr, uid, ids, context=context):
271 new_task_id = task_obj.create(cr, uid, {
273 'partner_id': bug.partner_id.id,
274 'description':bug.description,
275 'date_deadline': bug.date,
276 'project_id': bug.project_id.id,
277 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
278 'priority': str(int(bug.priority) - 1),
279 'user_id': bug.user_id.id,
280 'planned_hours': 0.0,
283 'task_id': new_task_id,
286 self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
287 case_obj.write(cr, uid, [bug.id], vals, context=context)
288 self.case_pending_send_note(cr, uid, [bug.id], context=context)
293 'view_mode': 'form,tree',
294 'res_model': 'project.task',
295 'res_id': int(new_task_id),
297 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
298 'type': 'ir.actions.act_window',
299 'search_view_id': res['res_id'],
304 def _convert(self, cr, uid, ids, xml_id, context=None):
305 data_obj = self.pool.get('ir.model.data')
306 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
309 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
311 self.write(cr, uid, ids, {'categ_id': categ_id})
314 def convert_to_feature(self, cr, uid, ids, context=None):
315 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
317 def convert_to_bug(self, cr, uid, ids, context=None):
318 return self._convert(cr, uid, ids, 'bug_categ', context=context)
320 def next_type(self, cr, uid, ids, context=None):
321 for task in self.browse(cr, uid, ids):
322 typeid = task.type_id.id
323 types = map(lambda x:x.id, task.project_id.type_ids or [])
326 self.write(cr, uid, [task.id], {'type_id': types[0]})
327 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
328 index = types.index(typeid)
329 self.write(cr, uid, [task.id], {'type_id': types[index+1]})
332 def prev_type(self, cr, uid, ids, context=None):
333 for task in self.browse(cr, uid, ids):
334 typeid = task.type_id.id
335 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
337 if typeid and typeid in types:
338 index = types.index(typeid)
339 self.write(cr, uid, [task.id], {'type_id': index and types[index-1] or False})
342 def write(self, cr, uid, ids, vals, context=None):
343 #Update last action date every time the user change the stage, the state or send a new email
344 logged_fields = ['type_id', 'state', 'message_ids']
345 if any([field in vals for field in logged_fields]):
346 vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
347 if vals.get('type_id', False):
348 stage = self.pool.get('project.task.type').browse(cr, uid, vals['type_id'], context=context)
349 self.message_append_note(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % stage.name, context=context)
350 return super(project_issue, self).write(cr, uid, ids, vals, context)
352 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
356 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
357 return {'value':{'user_id': task.user_id.id,}}
359 def case_reset(self, cr, uid, ids, context=None):
360 """Resets case as draft
362 res = super(project_issue, self).case_reset(cr, uid, ids, context)
363 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
366 def create(self, cr, uid, vals, context=None):
367 obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
368 self.create_send_note(cr, uid, [obj_id], context=context)
371 def case_open(self, cr, uid, ids, context=None):
372 res = super(project_issue, self).case_open(cr, uid, ids, context)
373 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
376 def case_escalate(self, cr, uid, ids, context=None):
377 cases = self.browse(cr, uid, ids)
379 data = {'state' : 'draft'}
380 if case.project_id.project_escalation_id:
381 data['project_id'] = case.project_id.project_escalation_id.id
382 if case.project_id.project_escalation_id.user_id:
383 data['user_id'] = case.project_id.project_escalation_id.user_id.id
385 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
387 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
388 self.write(cr, uid, [case.id], data)
389 self.case_escalate_send_note(cr, uid, [case.id], context)
392 def message_new(self, cr, uid, msg, custom_values=None, context=None):
393 """Automatically called when new email message arrives"""
396 subject = msg.get('subject') or _('No Title')
397 body = msg.get('body_text')
398 msg_from = msg.get('from')
399 priority = msg.get('priority')
402 'email_from': msg_from,
403 'email_cc': msg.get('cc'),
408 vals['priority'] = priority
409 vals.update(self.message_partner_by_email(cr, uid, msg_from))
410 context.update({'state_to' : 'draft'})
412 if custom_values and isinstance(custom_values, dict):
413 vals.update(custom_values)
415 res_id = self.create(cr, uid, vals, context)
416 self.message_append_dict(cr, uid, [res_id], msg, context=context)
417 self.convert_to_bug(cr, uid, [res_id], context=context)
420 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
424 if isinstance(ids, (str, int, long)):
428 'description': msg['body_text']
430 if msg.get('priority', False):
431 vals['priority'] = msg.get('priority')
434 'cost': 'planned_cost',
435 'revenue': 'planned_revenue',
436 'probability': 'probability'
439 # Reassign the 'open' state to the case if this one is in pending or done
440 for record in self.browse(cr, uid, ids, context=context):
441 if record.state in ('pending', 'done'):
442 record.write({'state' : 'open'})
445 for line in msg['body_text'].split('\n'):
447 res = tools.misc.command_re.match(line)
448 if res and maps.get(res.group(1).lower(), False):
449 key = maps.get(res.group(1).lower())
450 vls[key] = res.group(2).lower()
453 res = self.write(cr, uid, ids, vals)
454 self.message_append_dict(cr, uid, ids, msg, context=context)
457 def copy(self, cr, uid, id, default=None, context=None):
458 issue = self.read(cr, uid, id, ['name'], context=context)
461 default = default.copy()
462 default['name'] = issue['name'] + _(' (copy)')
463 return super(project_issue, self).copy(cr, uid, id, default=default,
466 # -------------------------------------------------------
467 # OpenChatter methods and notifications
468 # -------------------------------------------------------
470 def get_needaction_user_ids(self, cr, uid, ids, context=None):
471 result = dict.fromkeys(ids, [])
472 for obj in self.browse(cr, uid, ids, context=context):
473 if obj.state == 'draft' and obj.user_id:
474 result[obj.id] = [obj.user_id.id]
477 def message_get_subscribers(self, cr, uid, ids, context=None):
478 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
479 for obj in self.browse(cr, uid, ids, context=context):
481 sub_ids.append(obj.user_id.id)
482 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
484 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
485 return 'Project issue '
487 def convert_to_task_send_note(self, cr, uid, ids, context=None):
488 message = _("Project issue has been <b>converted</b> in to task.")
489 return self.message_append_note(cr, uid, ids, body=message, context=context)
491 def create_send_note(self, cr, uid, ids, context=None):
492 message = _("Project issue has been <b>created</b>.")
493 return self.message_append_note(cr, uid, ids, body=message, context=context)
495 def case_escalate_send_note(self, cr, uid, ids, context=None):
496 for obj in self.browse(cr, uid, ids, context=context):
498 message = _("has been <b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
499 obj.message_append_note(body=message, context=context)
501 message = _("has been <b>escalated</b>.")
502 obj.message_append_note(body=message, context=context)
507 class project(osv.osv):
508 _inherit = "project.project"
510 '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)]}),
511 'reply_to' : fields.char('Reply-To Email Address', size=256)
514 def _check_escalation(self, cr, uid, ids, context=None):
515 project_obj = self.browse(cr, uid, ids[0], context=context)
516 if project_obj.project_escalation_id:
517 if project_obj.project_escalation_id.id == project_obj.id:
522 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
526 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: