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.email_compose_message.email_model.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, id desc"
49 _inherit = ['email.thread']
51 def case_open(self, cr, uid, ids, *args):
53 @param self: The object pointer
54 @param cr: the current row, from the database cursor,
55 @param uid: the current user’s ID for security checks,
56 @param ids: List of case's Ids
57 @param *args: Give Tuple Value
60 res = super(project_issue, self).case_open(cr, uid, ids, *args)
61 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
62 for (id, name) in self.name_get(cr, uid, ids):
63 message = _("Issue '%s' has been opened.") % name
64 self.log(cr, uid, id, message)
67 def case_close(self, cr, uid, ids, *args):
69 @param self: The object pointer
70 @param cr: the current row, from the database cursor,
71 @param uid: the current user’s ID for security checks,
72 @param ids: List of case's Ids
73 @param *args: Give Tuple Value
76 res = super(project_issue, self).case_close(cr, uid, ids, *args)
77 for (id, name) in self.name_get(cr, uid, ids):
78 message = _("Issue '%s' has been closed.") % name
79 self.log(cr, uid, id, message)
82 def _compute_day(self, cr, uid, ids, fields, args, context=None):
84 @param cr: the current row, from the database cursor,
85 @param uid: the current user’s ID for security checks,
86 @param ids: List of Openday’s IDs
87 @return: difference between current date and log date
88 @param context: A standard dictionary for contextual values
90 cal_obj = self.pool.get('resource.calendar')
91 res_obj = self.pool.get('resource.resource')
94 for issue in self.browse(cr, uid, ids, context=context):
101 if field in ['working_hours_open','day_open']:
103 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
104 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
105 ans = date_open - date_create
106 date_until = issue.date_open
107 #Calculating no. of working hours to open the issue
108 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
109 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
110 datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
111 elif field in ['working_hours_close','day_close']:
112 if issue.date_closed:
113 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
114 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
115 date_until = issue.date_closed
116 ans = date_close - date_create
117 #Calculating no. of working hours to close the issue
118 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
119 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
120 datetime.strptime(issue.date_closed, '%Y-%m-%d %H:%M:%S'))
124 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
125 if resource_ids and len(resource_ids):
126 resource_id = resource_ids[0]
127 duration = float(ans.days)
128 if issue.project_id and issue.project_id.resource_calendar_id:
129 duration = float(ans.days) * 24
130 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)
132 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
133 for in_time, out_time in new_dates:
134 if in_time.date not in no_days:
135 no_days.append(in_time.date)
136 if out_time > date_until:
138 duration = len(no_days)
139 if field in ['working_hours_open','working_hours_close']:
140 res[issue.id][field] = hours
142 res[issue.id][field] = abs(float(duration))
145 def _get_issue_task(self, cr, uid, ids, context=None):
147 issue_pool = self.pool.get('project.issue')
148 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
149 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
152 def _get_issue_work(self, cr, uid, ids, context=None):
154 issue_pool = self.pool.get('project.issue')
155 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
157 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
160 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
161 task_pool = self.pool.get('project.task')
163 for issue in self.browse(cr, uid, ids, context=context):
166 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
167 res[issue.id] = {'progress' : progress}
171 'id': fields.integer('ID'),
172 'name': fields.char('Issue', size=128, required=True),
173 'active': fields.boolean('Active', required=False),
174 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
175 'write_date': fields.datetime('Update Date', readonly=True),
176 'date_deadline': fields.date('Deadline'),
177 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
178 select=True, help='Sales team to which Case belongs to.\
179 Define Responsible user and Email account for mail gateway.'),
180 'user_id': fields.many2one('res.users', 'Responsible'),
181 'partner_id': fields.many2one('res.partner', 'Partner'),
182 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
183 domain="[('partner_id','=',partner_id)]"),
184 'company_id': fields.many2one('res.company', 'Company'),
185 'description': fields.text('Description'),
186 'state': fields.selection([('draft', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
187 help='The state is set to \'Draft\', when a case is created.\
188 \nIf the case is in progress the state is set to \'Open\'.\
189 \nWhen the case is over, the state is set to \'Done\'.\
190 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
191 'email_from': fields.char('Email', size=128, help="These people will receive email."),
192 '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"),
193 'date_open': fields.datetime('Opened', readonly=True,select=True),
194 # Project Issue fields
195 'date_closed': fields.datetime('Closed', readonly=True,select=True),
196 'date': fields.datetime('Date'),
197 'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
198 " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
199 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
200 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
201 'version_id': fields.many2one('project.issue.version', 'Version'),
202 'partner_name': fields.char("Employee's Name", size=64),
203 'partner_mobile': fields.char('Mobile', size=32),
204 'partner_phone': fields.char('Phone', size=32),
205 'type_id': fields.many2one ('project.task.type', 'Resolution'),
206 'project_id':fields.many2one('project.project', 'Project'),
207 'duration': fields.float('Duration'),
208 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
209 'day_open': fields.function(_compute_day, string='Days to Open', \
210 method=True, multi='day_open', type="float", store=True),
211 'day_close': fields.function(_compute_day, string='Days to Close', \
212 method=True, multi='day_close', type="float", store=True),
213 'assigned_to': fields.related('task_id', 'user_id', string = 'Assigned to', type="many2one", relation="res.users", store=True, help='This is the current user to whom the related task have been assigned'),
214 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
215 method=True, multi='working_days_open', type="float", store=True),
216 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
217 method=True, multi='working_days_close', type="float", store=True),
218 'message_ids': fields.one2many('email.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
219 'date_action_last': fields.datetime('Last Action', readonly=1),
220 'date_action_next': fields.datetime('Next Action', readonly=1),
221 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
223 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
224 'project.task': (_get_issue_task, ['progress'], 10),
225 'project.task.work': (_get_issue_work, ['hours'], 10),
229 def _get_project(self, cr, uid, context=None):
230 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
231 if user.context_project_id:
232 return user.context_project_id.id
237 'user_id': crm.crm_case._get_default_user,
238 'partner_id': crm.crm_case._get_default_partner,
239 'partner_address_id': crm.crm_case._get_default_partner_address,
240 'email_from': crm.crm_case. _get_default_email,
242 'section_id': crm.crm_case. _get_section,
243 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
244 'priority': crm.AVAILABLE_PRIORITIES[2][0],
245 'project_id':_get_project,
248 def convert_issue_task(self, cr, uid, ids, context=None):
249 case_obj = self.pool.get('project.issue')
250 data_obj = self.pool.get('ir.model.data')
251 task_obj = self.pool.get('project.task')
257 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
258 res = data_obj.read(cr, uid, result, ['res_id'])
259 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
260 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
262 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
264 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
266 for bug in case_obj.browse(cr, uid, ids, context=context):
267 new_task_id = task_obj.create(cr, uid, {
269 'partner_id': bug.partner_id.id,
270 'description':bug.description,
272 'project_id': bug.project_id.id,
273 'priority': bug.priority,
274 'user_id': bug.assigned_to.id,
275 'planned_hours': 0.0,
279 'task_id': new_task_id,
282 case_obj.write(cr, uid, [bug.id], vals)
287 'view_mode': 'form,tree',
288 'res_model': 'project.task',
289 'res_id': int(new_task_id),
291 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
292 'type': 'ir.actions.act_window',
293 'search_view_id': res['res_id'],
298 def _convert(self, cr, uid, ids, xml_id, context=None):
299 data_obj = self.pool.get('ir.model.data')
300 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
303 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
305 self.write(cr, uid, ids, {'categ_id': categ_id})
308 def convert_to_feature(self, cr, uid, ids, context=None):
309 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
311 def convert_to_bug(self, cr, uid, ids, context=None):
312 return self._convert(cr, uid, ids, 'bug_categ', context=context)
314 def next_type(self, cr, uid, ids, *args):
315 for task in self.browse(cr, uid, ids):
316 typeid = task.type_id.id
317 types = map(lambda x:x.id, task.project_id.type_ids or [])
320 self.write(cr, uid, task.id, {'type_id': types[0]})
321 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
322 index = types.index(typeid)
323 self.write(cr, uid, task.id, {'type_id': types[index+1]})
326 def prev_type(self, cr, uid, ids, *args):
327 for task in self.browse(cr, uid, ids):
328 typeid = task.type_id.id
329 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
331 if typeid and typeid in types:
332 index = types.index(typeid)
333 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
337 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
341 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
342 return {'value':{'assigned_to': task.user_id.id,}}
344 def case_escalate(self, cr, uid, ids, *args):
345 """Escalates case to top level
346 @param self: The object pointer
347 @param cr: the current row, from the database cursor,
348 @param uid: the current user’s ID for security checks,
349 @param ids: List of case Ids
350 @param *args: Tuple Value for additional Params
352 cases = self.browse(cr, uid, ids)
355 if case.project_id.project_escalation_id:
356 data['project_id'] = case.project_id.project_escalation_id.id
357 if case.project_id.project_escalation_id.user_id:
358 data['user_id'] = case.project_id.project_escalation_id.user_id.id
360 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
362 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
363 self.write(cr, uid, [case.id], data)
364 self._history(cr, uid, cases, _('Escalate'))
367 def message_new(self, cr, uid, msg, context=None):
369 Automatically calls when new email message arrives
371 @param self: The object pointer
372 @param cr: the current row, from the database cursor,
373 @param uid: the current user’s ID for security checks
377 thread_pool = self.pool.get('email.thread')
379 subject = msg.get('subject') or _('No Title')
380 body = msg.get('body')
381 msg_from = msg.get('from')
382 priority = msg.get('priority')
386 'email_from': msg_from,
387 'email_cc': msg.get('cc'),
391 if msg.get('priority', False):
392 vals['priority'] = priority
394 res = thread_pool.get_partner(cr, uid, msg.get('from'))
397 context.update({'state_to' : 'draft'})
399 res_id = self.create(cr, uid, vals, context)
401 attachments = msg.get('attachments', {})
402 self.history(cr, uid, [res_id], _('receive'), history=True,
403 subject = msg.get('subject'),
404 email = msg.get('to'),
405 details = msg.get('body'),
406 email_from = msg.get('from'),
407 email_cc = msg.get('cc'),
408 message_id = msg.get('message-id'),
409 references = msg.get('references', False) or msg.get('in-reply-to', False),
410 attach = attachments,
411 email_date = msg.get('date'),
413 self.convert_to_bug(cr, uid, [res_id], context=context)
416 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
418 @param self: The object pointer
419 @param cr: the current row, from the database cursor,
420 @param uid: the current user’s ID for security checks,
421 @param ids: List of update mail’s IDs
427 if isinstance(ids, (str, int, long)):
431 'description': msg['body']
433 if msg.get('priority', False):
434 vals['priority'] = msg.get('priority')
437 'cost': 'planned_cost',
438 'revenue': 'planned_revenue',
439 'probability': 'probability'
442 # Reassign the 'open' state to the case if this one is in pending or done
443 for record in self.browse(cr, uid, ids, context=context):
444 if record.state in ('pending', 'done'):
445 record.write({'state' : 'open'})
448 for line in msg['body'].split('\n'):
450 res = tools.misc.command_re.match(line)
451 if res and maps.get(res.group(1).lower(), False):
452 key = maps.get(res.group(1).lower())
453 vls[key] = res.group(2).lower()
456 res = self.write(cr, uid, ids, vals)
459 def copy(self, cr, uid, id, default=None, context=None):
460 issue = self.read(cr, uid, id, ['name'], context=context)
463 default = default.copy()
464 default['name'] = issue['name'] + _(' (copy)')
465 return super(project_issue, self).copy(cr, uid, id, default=default,
470 class project(osv.osv):
471 _inherit = "project.project"
473 '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)]}),
474 '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)]}),
475 'reply_to' : fields.char('Reply-To Email Address', size=256)
478 def _check_escalation(self, cr, uid, ids, context=None):
479 project_obj = self.browse(cr, uid, ids[0], context=context)
480 if project_obj.project_escalation_id:
481 if project_obj.project_escalation_id.id == project_obj.id:
486 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
490 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: