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 write(self, cr, uid, ids, vals, context=None):
52 #Update last action date everytime the user change the stage, the state or send a new email
53 logged_fields = ['type_id', 'state', 'message_ids']
54 if any([field in vals for field in logged_fields]):
55 vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
56 return super(project_issue, self).write(cr, uid, ids, vals, context)
58 def case_open(self, cr, uid, ids, *args):
60 @param self: The object pointer
61 @param cr: the current row, from the database cursor,
62 @param uid: the current user’s ID for security checks,
63 @param ids: List of case's Ids
64 @param *args: Give Tuple Value
67 res = super(project_issue, self).case_open(cr, uid, ids, *args)
68 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
69 for (id, name) in self.name_get(cr, uid, ids):
70 message = _("Issue '%s' has been opened.") % name
71 self.log(cr, uid, id, message)
74 def case_close(self, cr, uid, ids, *args):
76 @param self: The object pointer
77 @param cr: the current row, from the database cursor,
78 @param uid: the current user’s ID for security checks,
79 @param ids: List of case's Ids
80 @param *args: Give Tuple Value
83 res = super(project_issue, self).case_close(cr, uid, ids, *args)
84 for (id, name) in self.name_get(cr, uid, ids):
85 message = _("Issue '%s' has been closed.") % name
86 self.log(cr, uid, id, message)
89 def _compute_day(self, cr, uid, ids, fields, args, context=None):
91 @param cr: the current row, from the database cursor,
92 @param uid: the current user’s ID for security checks,
93 @param ids: List of Openday’s IDs
94 @return: difference between current date and log date
95 @param context: A standard dictionary for contextual values
97 cal_obj = self.pool.get('resource.calendar')
98 res_obj = self.pool.get('resource.resource')
101 for issue in self.browse(cr, uid, ids, context=context):
108 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
109 if field in ['working_hours_open','day_open']:
111 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
112 ans = date_open - date_create
113 date_until = issue.date_open
114 #Calculating no. of working hours to open the issue
115 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
118 elif field in ['working_hours_close','day_close']:
119 if issue.date_closed:
120 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
121 date_until = issue.date_closed
122 ans = date_close - date_create
123 #Calculating no. of working hours to close the issue
124 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
127 elif field in ['days_since_creation']:
128 if issue.create_date:
129 days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
130 res[issue.id][field] = days_since_creation.days
133 elif field in ['inactivity_days']:
134 res[issue.id][field] = 0
135 if issue.date_action_last:
136 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
137 res[issue.id][field] = inactive_days.days
142 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
143 if resource_ids and len(resource_ids):
144 resource_id = resource_ids[0]
145 duration = float(ans.days)
146 if issue.project_id and issue.project_id.resource_calendar_id:
147 duration = float(ans.days) * 24
149 new_dates = cal_obj.interval_min_get(cr, uid,
150 issue.project_id.resource_calendar_id.id,
152 duration, resource=resource_id)
154 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
155 for in_time, out_time in new_dates:
156 if in_time.date not in no_days:
157 no_days.append(in_time.date)
158 if out_time > date_until:
160 duration = len(no_days)
162 if field in ['working_hours_open','working_hours_close']:
163 res[issue.id][field] = hours
165 res[issue.id][field] = abs(float(duration))
169 def _get_issue_task(self, cr, uid, ids, context=None):
171 issue_pool = self.pool.get('project.issue')
172 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
173 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
176 def _get_issue_work(self, cr, uid, ids, context=None):
178 issue_pool = self.pool.get('project.issue')
179 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
181 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
184 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
185 task_pool = self.pool.get('project.task')
187 for issue in self.browse(cr, uid, ids, context=context):
190 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
191 res[issue.id] = {'progress' : progress}
195 'id': fields.integer('ID', readonly=True),
196 'name': fields.char('Issue', size=128, required=True),
197 'active': fields.boolean('Active', required=False),
198 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
199 'write_date': fields.datetime('Update Date', readonly=True),
200 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
201 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
202 'date_deadline': fields.date('Deadline'),
203 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
204 select=True, help='Sales team to which Case belongs to.\
205 Define Responsible user and Email account for mail gateway.'),
206 'partner_id': fields.many2one('res.partner', 'Partner', select=1),
207 'company_id': fields.many2one('res.company', 'Company'),
208 'description': fields.text('Description'),
209 'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
210 help='The state is set to \'Draft\', when a case is created.\
211 \nIf the case is in progress the state is set to \'Open\'.\
212 \nWhen the case is over, the state is set to \'Done\'.\
213 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
214 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
215 '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"),
216 'date_open': fields.datetime('Opened', readonly=True,select=True),
217 # Project Issue fields
218 'date_closed': fields.datetime('Closed', readonly=True,select=True),
219 'date': fields.datetime('Date'),
220 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
221 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
222 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
223 'version_id': fields.many2one('project.issue.version', 'Version'),
224 'type_id': fields.many2one ('project.task.type', 'Stages', domain="[('project_ids', '=', project_id)]"),
225 'project_id':fields.many2one('project.project', 'Project'),
226 'duration': fields.float('Duration'),
227 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
228 'day_open': fields.function(_compute_day, string='Days to Open', \
229 multi='compute_day', type="float", store=True),
230 'day_close': fields.function(_compute_day, string='Days to Close', \
231 multi='compute_day', type="float", store=True),
232 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
233 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
234 multi='compute_day', type="float", store=True),
235 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
236 multi='compute_day', type="float", store=True),
237 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
238 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
239 'color': fields.integer('Color Index'),
240 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
241 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
242 'date_action_last': fields.datetime('Last Action', readonly=1),
243 'date_action_next': fields.datetime('Next Action', readonly=1),
244 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
246 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
247 'project.task': (_get_issue_task, ['progress'], 10),
248 'project.task.work': (_get_issue_work, ['hours'], 10),
252 def _get_project(self, cr, uid, context=None):
253 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
254 if user.context_project_id:
255 return user.context_project_id.id
257 project_id = context.get('active_id',False)
260 def on_change_project(self, cr, uid, ids, project_id, context=None):
266 'partner_id': crm.crm_case._get_default_partner,
267 'email_from': crm.crm_case._get_default_email,
269 'section_id': crm.crm_case._get_section,
270 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
271 'priority': crm.AVAILABLE_PRIORITIES[2][0],
272 'project_id':_get_project,
273 'categ_id' : lambda *a: False,
276 def set_priority(self, cr, uid, ids, priority):
279 return self.write(cr, uid, ids, {'priority' : priority})
281 def set_high_priority(self, cr, uid, ids, *args):
282 """Set lead priority to high
284 return self.set_priority(cr, uid, ids, '1')
286 def set_normal_priority(self, cr, uid, ids, *args):
287 """Set lead priority to normal
289 return self.set_priority(cr, uid, ids, '3')
291 def convert_issue_task(self, cr, uid, ids, context=None):
292 case_obj = self.pool.get('project.issue')
293 data_obj = self.pool.get('ir.model.data')
294 task_obj = self.pool.get('project.task')
300 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
301 res = data_obj.read(cr, uid, result, ['res_id'])
302 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
303 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
305 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
307 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
309 for bug in case_obj.browse(cr, uid, ids, context=context):
310 new_task_id = task_obj.create(cr, uid, {
312 'partner_id': bug.partner_id.id,
313 'description':bug.description,
314 'date_deadline': bug.date,
315 'project_id': bug.project_id.id,
316 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
317 'priority': str(int(bug.priority) - 1),
318 'user_id': bug.user_id.id,
319 'planned_hours': 0.0,
323 'task_id': new_task_id,
326 case_obj.write(cr, uid, [bug.id], vals)
331 'view_mode': 'form,tree',
332 'res_model': 'project.task',
333 'res_id': int(new_task_id),
335 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
336 'type': 'ir.actions.act_window',
337 'search_view_id': res['res_id'],
342 def _convert(self, cr, uid, ids, xml_id, context=None):
343 data_obj = self.pool.get('ir.model.data')
344 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
347 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
349 self.write(cr, uid, ids, {'categ_id': categ_id})
352 def convert_to_feature(self, cr, uid, ids, context=None):
353 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
355 def convert_to_bug(self, cr, uid, ids, context=None):
356 return self._convert(cr, uid, ids, 'bug_categ', context=context)
358 def next_type(self, cr, uid, ids, *args):
359 for task in self.browse(cr, uid, ids):
360 typeid = task.type_id.id
361 types = map(lambda x:x.id, task.project_id.type_ids or [])
364 self.write(cr, uid, task.id, {'type_id': types[0]})
365 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
366 index = types.index(typeid)
367 self.write(cr, uid, task.id, {'type_id': types[index+1]})
370 def prev_type(self, cr, uid, ids, *args):
371 for task in self.browse(cr, uid, ids):
372 typeid = task.type_id.id
373 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
375 if typeid and typeid in types:
376 index = types.index(typeid)
377 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
380 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
384 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
385 return {'value':{'user_id': task.user_id.id,}}
387 def case_escalate(self, cr, uid, ids, *args):
388 """Escalates case to top level
389 @param self: The object pointer
390 @param cr: the current row, from the database cursor,
391 @param uid: the current user’s ID for security checks,
392 @param ids: List of case Ids
393 @param *args: Tuple Value for additional Params
395 cases = self.browse(cr, uid, ids)
397 data = {'state' : 'draft'}
398 if case.project_id.project_escalation_id:
399 data['project_id'] = case.project_id.project_escalation_id.id
400 if case.project_id.project_escalation_id.user_id:
401 data['user_id'] = case.project_id.project_escalation_id.user_id.id
403 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
405 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
406 self.write(cr, uid, [case.id], data)
407 self.message_append(cr, uid, cases, _('Escalate'))
410 def message_new(self, cr, uid, msg, custom_values=None, context=None):
411 """Automatically called when new email message arrives"""
414 subject = msg.get('subject') or _('No Title')
415 body = msg.get('body_text')
416 msg_from = msg.get('from')
417 priority = msg.get('priority')
420 'email_from': msg_from,
421 'email_cc': msg.get('cc'),
426 vals['priority'] = priority
427 vals.update(self.message_partner_by_email(cr, uid, msg_from))
428 context.update({'state_to' : 'draft'})
430 if custom_values and isinstance(custom_values, dict):
431 vals.update(custom_values)
433 res_id = self.create(cr, uid, vals, context)
434 self.message_append_dict(cr, uid, [res_id], msg, context=context)
435 self.convert_to_bug(cr, uid, [res_id], context=context)
438 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
443 if isinstance(ids, (str, int, long)):
447 'description': msg['body_text']
449 if msg.get('priority', False):
450 vals['priority'] = msg.get('priority')
453 'cost': 'planned_cost',
454 'revenue': 'planned_revenue',
455 'probability': 'probability'
458 # Reassign the 'open' state to the case if this one is in pending or done
459 for record in self.browse(cr, uid, ids, context=context):
460 if record.state in ('pending', 'done'):
461 record.write({'state' : 'open'})
464 for line in msg['body_text'].split('\n'):
466 res = tools.misc.command_re.match(line)
467 if res and maps.get(res.group(1).lower(), False):
468 key = maps.get(res.group(1).lower())
469 vls[key] = res.group(2).lower()
472 res = self.write(cr, uid, ids, vals)
473 self.message_append_dict(cr, uid, ids, msg, context=context)
476 def copy(self, cr, uid, id, default=None, context=None):
477 issue = self.read(cr, uid, id, ['name'], context=context)
480 default = default.copy()
481 default['name'] = issue['name'] + _(' (copy)')
482 return super(project_issue, self).copy(cr, uid, id, default=default,
487 class project(osv.osv):
488 _inherit = "project.project"
490 def _compute_issue(self, cr, uid, ids, field_name, arg, context=None):
492 issue_pool=self.pool.get('project.issue')
493 for project in self.browse(cr, uid, ids, context=context):
494 issues = issue_pool.search(cr, uid, [('project_id','=',project.id)])
495 res[project.id] = len(issues)
499 '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)]}),
500 'reply_to' : fields.char('Reply-To Email Address', size=256),
501 'issues' : fields.boolean('Issues',help = "If you check this field issues are appears in kanban view"),
502 'total_issues': fields.function(_compute_issue , type='integer',string="Issue"),
509 def open_issues(self, cr, uid, ids, context=None):
510 #Open the View for the Tasks for the project
512 This opens Issues views
513 @return :Dictionary value for issue view
518 data_obj = self.pool.get('ir.model.data')
519 for project in self.browse(cr, uid, ids, context=context):
521 tree_view = data_obj.get_object_reference(cr, uid, 'project_issue', 'project_issue_tree_view')
522 form_view = data_obj.get_object_reference(cr, uid, 'project_issue', 'project_issue_form_view')
523 calander_view = data_obj.get_object_reference(cr, uid, 'project_issue', 'project_issue_calendar_view')
524 search_view = data_obj.get_object_reference(cr, uid, 'project_issue', 'view_project_issue_filter')
525 kanban_view = data_obj.get_object_reference(cr, uid, 'project_issue', 'project_issue_kanban_view')
527 #'search_default_user_id': uid,
528 'search_default_project_id':project.id
534 'view_mode': 'form,tree',
535 'res_model': 'project.issue',
537 'domain':[('project_id','in',ids)],
539 'views': [(kanban_view and kanban_view[1] or False, 'kanban'),(tree_view and tree_view[1] or False, 'tree'),(calander_view and calander_view[1] or False, 'calendar'),(form_view and form_view[1] or False, 'form')],
540 'type': 'ir.actions.act_window',
541 'search_view_id': search_view and search_view[1] or False,
546 def _check_escalation(self, cr, uid, ids, context=None):
547 project_obj = self.browse(cr, uid, ids[0], context=context)
548 if project_obj.project_escalation_id:
549 if project_obj.project_escalation_id.id == project_obj.id:
554 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
558 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: