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 = ['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 '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', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('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.", select=1),
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 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
196 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
197 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
198 'version_id': fields.many2one('project.issue.version', 'Version'),
199 'type_id': fields.many2one ('project.task.type', 'Stages', domain="[('project_ids', '=', project_id)]"),
200 'project_id':fields.many2one('project.project', 'Project'),
201 'duration': fields.float('Duration'),
202 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
203 'day_open': fields.function(_compute_day, string='Days to Open', \
204 multi='compute_day', type="float", store=True),
205 'day_close': fields.function(_compute_day, string='Days to Close', \
206 multi='compute_day', type="float", store=True),
207 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
208 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
209 multi='compute_day', type="float", store=True),
210 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
211 multi='compute_day', type="float", store=True),
212 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
213 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
214 'color': fields.integer('Color Index'),
215 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
216 'message_ids': fields.one2many('mail.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.",
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),
230 'partner_id': crm.crm_case._get_default_partner,
231 'partner_address_id': crm.crm_case._get_default_partner_address,
232 'email_from': crm.crm_case._get_default_email,
234 'section_id': crm.crm_case._get_section,
235 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
236 'priority': crm.AVAILABLE_PRIORITIES[2][0],
237 'project_id':_get_project,
238 'categ_id' : lambda *a: False,
241 def set_priority(self, cr, uid, ids, priority):
244 return self.write(cr, uid, ids, {'priority' : priority})
246 def set_high_priority(self, cr, uid, ids, *args):
247 """Set lead priority to high
249 return self.set_priority(cr, uid, ids, '1')
251 def set_normal_priority(self, cr, uid, ids, *args):
252 """Set lead priority to normal
254 return self.set_priority(cr, uid, ids, '3')
256 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')
265 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
266 res = data_obj.read(cr, uid, result, ['res_id'])
267 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
268 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
270 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
272 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
274 for bug in case_obj.browse(cr, uid, ids, context=context):
275 new_task_id = task_obj.create(cr, uid, {
277 'partner_id': bug.partner_id.id,
278 'description':bug.description,
279 'date_deadline': bug.date,
280 'project_id': bug.project_id.id,
281 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
282 'priority': str(int(bug.priority) - 1),
283 'user_id': bug.user_id.id,
284 'planned_hours': 0.0,
287 'task_id': new_task_id,
290 self.convert_to_task_send_note(cr, uid, ids, context)
291 cases = self.browse(cr, uid, ids)
292 self.case_pending_send_note(cr, uid, ids, context)
293 case_obj.write(cr, uid, [bug.id], vals)
298 'view_mode': 'form,tree',
299 'res_model': 'project.task',
300 'res_id': int(new_task_id),
302 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
303 'type': 'ir.actions.act_window',
304 'search_view_id': res['res_id'],
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)
314 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
316 self.write(cr, uid, ids, {'categ_id': categ_id})
319 def convert_to_feature(self, cr, uid, ids, context=None):
320 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
322 def convert_to_bug(self, cr, uid, ids, context=None):
323 return self._convert(cr, uid, ids, 'bug_categ', context=context)
325 def prev_type(self, cr, uid, ids, context=None):
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})
335 def next_type(self, cr, uid, ids, context=None):
336 for task in self.browse(cr, uid, ids):
337 typeid = task.type_id.id
338 types = map(lambda x:x.id, task.project_id.type_ids or [])
341 self.write(cr, uid, [task.id], {'type_id': types[0]})
342 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
343 index = types.index(typeid)
344 self.write(cr, uid, [task.id], {'type_id': types[index+1]})
347 def write(self, cr, uid, ids, vals, context=None):
348 #Update last action date every time the user change the stage, the state or send a new email
349 logged_fields = ['type_id', 'state', 'message_ids']
350 if any([field in vals for field in logged_fields]):
351 vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
352 if 'type_id' in vals and vals['type_id']:
353 stage = self.pool.get('project.task.type').browse(cr, uid, vals['type_id'], context=context)
354 self.message_append_note(cr, uid, ids, _('System notification'),
355 _("changed stage to <b>%s</b>.") % stage.name, type='notification')
356 return super(project_issue, self).write(cr, uid, ids, vals, context)
358 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
362 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
363 return {'value':{'user_id': task.user_id.id,}}
365 def case_open_send_note(self, cr, uid, ids, context=None):
367 message = _("has been <b>opened</b>.")
368 self.message_append_note(cr, uid, [id],'System Notification', message, context=context)
371 def convert_to_task_send_note(self, cr, uid, ids, context=None):
373 message = _("has been <b>converted</b> in to task.")
374 self.message_append_note(cr, uid, [id], 'System notification', message, context=context)
377 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
380 def get_needaction_user_ids(self, cr, uid, ids, context=None):
381 result = dict.fromkeys(ids, [])
382 for obj in self.browse(cr, uid, ids, context=context):
383 if obj.state == 'draft' and obj.user_id:
384 result[obj.id] = [obj.user_id.id]
387 def message_get_subscribers(self, cr, uid, ids, context=None):
388 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
389 for obj in self.browse(cr, uid, ids, context=context):
391 sub_ids.append(obj.user_id.id)
392 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
394 def create_send_note(self, cr, uid, ids, context=None):
395 message = _("has been <b>created</b>.")
396 self.message_append_note(cr, uid, ids, _('System notification'),
397 message, type='notification', context=context)
400 def case_escalate_send_note(self, cr, uid, ids, context=None):
401 for obj in self.browse(cr, uid, ids, context=context):
403 message = _("has been <b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
404 obj.message_append_note('' ,message, type='notification', context=context)
406 message = _("has been <b>escalated</b>.")
407 obj.message_append_note('' ,message, type='notification', context=context)
410 def case_reset(self, cr, uid, ids, context=None):
411 """Resets case as draft
413 res = super(project_issue, self).case_reset(cr, uid, ids, context)
414 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
417 def create(self, cr, uid, vals, context=None):
418 obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
419 self.create_send_note(cr, uid, [obj_id], context=context)
422 def case_open(self, cr, uid, ids, context=None):
424 @param self: The object pointer
425 @param cr: the current row, from the database cursor,
426 @param uid: the current user’s ID for security checks,
427 @param ids: List of case's Ids
429 res = super(project_issue, self).case_open(cr, uid, ids, context)
430 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
433 def case_escalate(self, cr, uid, ids, context=None):
434 """Escalates case to top level
435 @param self: The object pointer
436 @param cr: the current row, from the database cursor,
437 @param uid: the current user’s ID for security checks,
438 @param ids: List of case Ids
440 cases = self.browse(cr, uid, ids)
442 data = {'state' : 'draft'}
443 if case.project_id.project_escalation_id:
444 data['project_id'] = case.project_id.project_escalation_id.id
445 if case.project_id.project_escalation_id.user_id:
446 data['user_id'] = case.project_id.project_escalation_id.user_id.id
448 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
450 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
451 self.write(cr, uid, [case.id], data)
452 self.case_escalate_send_note(cr, uid, [case.id], context)
455 def message_new(self, cr, uid, msg, custom_values=None, context=None):
456 """Automatically called when new email message arrives"""
459 subject = msg.get('subject') or _('No Title')
460 body = msg.get('body_text')
461 msg_from = msg.get('from')
462 priority = msg.get('priority')
465 'email_from': msg_from,
466 'email_cc': msg.get('cc'),
471 vals['priority'] = priority
472 vals.update(self.message_partner_by_email(cr, uid, msg_from))
473 context.update({'state_to' : 'draft'})
475 if custom_values and isinstance(custom_values, dict):
476 vals.update(custom_values)
478 res_id = self.create(cr, uid, vals, context)
479 self.message_append_dict(cr, uid, [res_id], msg, context=context)
480 self.convert_to_bug(cr, uid, [res_id], context=context)
483 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
487 if isinstance(ids, (str, int, long)):
491 'description': msg['body_text']
493 if msg.get('priority', False):
494 vals['priority'] = msg.get('priority')
497 'cost': 'planned_cost',
498 'revenue': 'planned_revenue',
499 'probability': 'probability'
502 # Reassign the 'open' state to the case if this one is in pending or done
503 for record in self.browse(cr, uid, ids, context=context):
504 if record.state in ('pending', 'done'):
505 record.write({'state' : 'open'})
508 for line in msg['body_text'].split('\n'):
510 res = tools.misc.command_re.match(line)
511 if res and maps.get(res.group(1).lower(), False):
512 key = maps.get(res.group(1).lower())
513 vls[key] = res.group(2).lower()
516 res = self.write(cr, uid, ids, vals)
517 self.message_append_dict(cr, uid, ids, msg, context=context)
520 def copy(self, cr, uid, id, default=None, context=None):
521 issue = self.read(cr, uid, id, ['name'], context=context)
524 default = default.copy()
525 default['name'] = issue['name'] + _(' (copy)')
526 return super(project_issue, self).copy(cr, uid, id, default=default,
531 class project(osv.osv):
532 _inherit = "project.project"
534 '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)]}),
535 'reply_to' : fields.char('Reply-To Email Address', size=256)
538 def _check_escalation(self, cr, uid, ids, context=None):
539 project_obj = self.browse(cr, uid, ids[0], context=context)
540 if project_obj.project_escalation_id:
541 if project_obj.project_escalation_id.id == project_obj.id:
546 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
550 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: