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):
366 for obj in self.browse(cr, uid, ids, context=context):
367 message = _("has been <b>opened</b>.")
368 obj.message_append_note('' ,message)
371 def case_close_send_note(self, cr, uid, ids, context=None):
372 for obj in self.browse(cr, uid, ids, context=context):
373 message = _("has been <b>closed</b>.")
374 obj.message_append_note('' ,message)
378 def convert_to_task_send_note(self, cr, uid, ids, context=None):
379 for obj in self.browse(cr, uid, ids, context=context):
380 self.message_append_note(cr, uid, ids, _('System notification'),
381 _("has been <b>converted</b> in to task."), type='notification', context=context)
384 def get_needaction_user_id(self, cr, uid, ids, name, arg, context=None):
386 for obj in self.browse(cr, uid, ids, context=context):
387 result[obj.id] = False
388 if (obj.state == 'draft' and obj.user_id):
389 result[obj.id] = obj.user_id.id
390 if obj.project_id.project_escalation_id.user_id:
391 result[obj.id] = obj.project_id.project_escalation_id.user_id.id
394 def case_escalate_send_note(self, cr, uid, ids, context=None):
395 for obj in self.browse(cr, uid, ids, context=context):
396 if obj.project_id.project_escalation_id.user_id.id:
397 message = _("has been<b>escalated</b> from project <em>'%s'</em> to project <em>'%s'</em>.") % (obj.project_id.name, obj.project_id.project_escalation_id.name)
398 obj.message_append_note('' ,message, type='notification', context=context)
400 message = _("has been <b>escalated</b>.")
401 obj.message_append_note('' ,message, type='notification', context=context)
404 def case_create_send_note(self, cr, uid, ids, context=None):
405 for obj in self.browse(cr, uid, ids, context=context):
406 # self.message_subscribe(cr, uid, ids, [obj.user_id.id], context=context)
407 message = _("has been <b>created</b>.")
408 self.message_append_note(cr, uid, ids, _('System notification'),
409 message, type='notification', context=context)
412 def case_pending_send_note(self, cr, uid, ids, context=None):
413 for obj in self.browse(cr, uid, ids, context=context):
414 message = _("has been <b>pending<b>.")
415 obj.message_append_note('' ,message)
418 def case_reset_send_note(self, cr, uid, ids, context=None):
419 for obj in self.browse(cr, uid, ids, context=context):
420 message =_("has been set as <b>new<b>.")
421 obj.message_append_note('' ,message)
424 def case_cancel_send_note(self, cr, uid, ids, context=None):
425 for obj in self.browse(cr, uid, ids, context=context):
426 message = _("has been <b>cancelled<b>.")
427 obj.message_append_note('' ,message)
430 def case_reset(self, cr, uid, ids, context=None):
431 """Resets case as draft
433 res = super(project_issue, self).case_reset(cr, uid, ids, context)
434 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
435 self.case_reset_send_note(cr, uid, ids, context)
438 def case_pending(self, cr, uid, ids, context=None):
439 """Marks case as pending"""
440 res = super(project_issue, self).case_pending(cr, uid, ids, context)
441 self.write(cr, uid, ids, context)
444 def case_cancel(self, cr, uid, ids, context=None):
445 print "\n :: case cancel ::::"
446 """Overrides cancel for crm_case for setting probability
448 res = super(project_issue, self).case_cancel(cr, uid, ids, context)
449 self.write(cr, uid, ids, context)
452 def create(self, cr, uid, vals, context=None):
453 obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
454 self.case_create_send_note(cr, uid, [obj_id], context=context)
457 def case_open(self, cr, uid, ids, context=None):
459 @param self: The object pointer
460 @param cr: the current row, from the database cursor,
461 @param uid: the current user’s ID for security checks,
462 @param ids: List of case's Ids
465 res = super(project_issue, self).case_open(cr, uid, ids, context)
466 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
469 def case_close(self, cr, uid, ids, context=None):
471 @param self: The object pointer
472 @param cr: the current row, from the database cursor,
473 @param uid: the current user’s ID for security checks,
474 @param ids: List of case's Ids
477 res = super(project_issue, self).case_close(cr, uid, ids, context)
480 def case_escalate(self, cr, uid, ids, context=None):
481 """Escalates case to top level
482 @param self: The object pointer
483 @param cr: the current row, from the database cursor,
484 @param uid: the current user’s ID for security checks,
485 @param ids: List of case Ids
487 cases = self.browse(cr, uid, ids)
489 data = {'state' : 'draft'}
490 if case.project_id.project_escalation_id:
491 data['project_id'] = case.project_id.project_escalation_id.id
492 if case.project_id.project_escalation_id.user_id:
493 data['user_id'] = case.project_id.project_escalation_id.user_id.id
495 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
497 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
498 self.write(cr, uid, [case.id], data)
499 self.case_escalate_send_note(cr, uid, ids, context=context)
502 def message_new(self, cr, uid, msg, custom_values=None, context=None):
503 """Automatically called when new email message arrives"""
506 subject = msg.get('subject') or _('No Title')
507 body = msg.get('body_text')
508 msg_from = msg.get('from')
509 priority = msg.get('priority')
512 'email_from': msg_from,
513 'email_cc': msg.get('cc'),
518 vals['priority'] = priority
519 vals.update(self.message_partner_by_email(cr, uid, msg_from))
520 context.update({'state_to' : 'draft'})
522 if custom_values and isinstance(custom_values, dict):
523 vals.update(custom_values)
525 res_id = self.create(cr, uid, vals, context)
526 self.message_append_dict(cr, uid, [res_id], msg, context=context)
527 self.convert_to_bug(cr, uid, [res_id], context=context)
530 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
534 if isinstance(ids, (str, int, long)):
538 'description': msg['body_text']
540 if msg.get('priority', False):
541 vals['priority'] = msg.get('priority')
544 'cost': 'planned_cost',
545 'revenue': 'planned_revenue',
546 'probability': 'probability'
549 # Reassign the 'open' state to the case if this one is in pending or done
550 for record in self.browse(cr, uid, ids, context=context):
551 if record.state in ('pending', 'done'):
552 record.write({'state' : 'open'})
555 for line in msg['body_text'].split('\n'):
557 res = tools.misc.command_re.match(line)
558 if res and maps.get(res.group(1).lower(), False):
559 key = maps.get(res.group(1).lower())
560 vls[key] = res.group(2).lower()
563 res = self.write(cr, uid, ids, vals)
564 self.message_append_dict(cr, uid, ids, msg, context=context)
567 def copy(self, cr, uid, id, default=None, context=None):
568 issue = self.read(cr, uid, id, ['name'], context=context)
571 default = default.copy()
572 default['name'] = issue['name'] + _(' (copy)')
573 return super(project_issue, self).copy(cr, uid, id, default=default,
578 class project(osv.osv):
579 _inherit = "project.project"
581 '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)]}),
582 'reply_to' : fields.char('Reply-To Email Address', size=256)
585 def _check_escalation(self, cr, uid, ids, context=None):
586 project_obj = self.browse(cr, uid, ids[0], context=context)
587 if project_obj.project_escalation_id:
588 if project_obj.project_escalation_id.id == project_obj.id:
593 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
597 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: