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 _
31 class project_issue_version(osv.osv):
32 _name = "project.issue.version"
35 'name': fields.char('Version Number', size=32, required=True),
36 'active': fields.boolean('Active', required=False),
41 project_issue_version()
43 class project_issue(crm.crm_case, osv.osv):
44 _name = "project.issue"
45 _description = "Project Issue"
46 _order = "priority, create_date desc"
47 _inherit = ['mailgate.thread']
49 def case_open(self, cr, uid, ids, *args):
51 @param self: The object pointer
52 @param cr: the current row, from the database cursor,
53 @param uid: the current user’s ID for security checks,
54 @param ids: List of case's Ids
55 @param *args: Give Tuple Value
58 res = super(project_issue, self).case_open(cr, uid, ids, *args)
59 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'assigned_to' : uid})
60 for (id, name) in self.name_get(cr, uid, ids):
61 message = _("Issue '%s' has been opened.") % name
62 self.log(cr, uid, id, message)
65 def case_close(self, cr, uid, ids, *args):
67 @param self: The object pointer
68 @param cr: the current row, from the database cursor,
69 @param uid: the current user’s ID for security checks,
70 @param ids: List of case's Ids
71 @param *args: Give Tuple Value
74 res = super(project_issue, self).case_close(cr, uid, ids, *args)
75 for (id, name) in self.name_get(cr, uid, ids):
76 message = _("Issue '%s' has been closed.") % name
77 self.log(cr, uid, id, message)
80 def _compute_day(self, cr, uid, ids, fields, args, context=None):
82 @param cr: the current row, from the database cursor,
83 @param uid: the current user’s ID for security checks,
84 @param ids: List of Openday’s IDs
85 @return: difference between current date and log date
86 @param context: A standard dictionary for contextual values
88 cal_obj = self.pool.get('resource.calendar')
89 res_obj = self.pool.get('resource.resource')
92 for issue in self.browse(cr, uid, ids, context=context):
99 if field in ['working_hours_open','day_open']:
101 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
102 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
103 ans = date_open - date_create
104 date_until = issue.date_open
105 #Calculating no. of working hours to open the issue
106 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
107 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
108 datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
109 elif field in ['working_hours_close','day_close']:
110 if issue.date_closed:
111 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
112 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
113 date_until = issue.date_closed
114 ans = date_close - date_create
115 #Calculating no. of working hours to close the issue
116 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
117 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
118 datetime.strptime(issue.date_closed, '%Y-%m-%d %H:%M:%S'))
122 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
123 if resource_ids and len(resource_ids):
124 resource_id = resource_ids[0]
125 duration = float(ans.days)
126 if issue.project_id and issue.project_id.resource_calendar_id:
127 duration = float(ans.days) * 24
128 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)
130 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
131 for in_time, out_time in new_dates:
132 if in_time.date not in no_days:
133 no_days.append(in_time.date)
134 if out_time > date_until:
136 duration = len(no_days)
137 if field in ['working_hours_open','working_hours_close']:
138 res[issue.id][field] = hours
140 res[issue.id][field] = abs(float(duration))
143 def _get_issue_task(self, cr, uid, ids, context=None):
145 issue_pool = self.pool.get('project.issue')
146 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
147 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
150 def _get_issue_work(self, cr, uid, ids, context=None):
152 issue_pool = self.pool.get('project.issue')
153 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
155 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
158 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
159 task_pool = self.pool.get('project.task')
161 for issue in self.browse(cr, uid, ids, context=context):
164 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
165 res[issue.id] = {'progress' : progress}
169 'id': fields.integer('ID'),
170 'name': fields.char('Issue', size=128, required=True),
171 'active': fields.boolean('Active', required=False),
172 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
173 'write_date': fields.datetime('Update Date', readonly=True),
174 'date_deadline': fields.date('Deadline'),
175 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
176 select=True, help='Sales team to which Case belongs to.\
177 Define Responsible user and Email account for mail gateway.'),
178 'user_id': fields.related('project_id', 'user_id', type='many2one', relation='res.users', store=True, select=1, string='Responsible'),
179 'partner_id': fields.many2one('res.partner', 'Partner'),
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', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('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."),
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 'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
196 " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
197 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
198 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
199 'version_id': fields.many2one('project.issue.version', 'Version'),
200 'partner_name': fields.char("Employee's Name", size=64),
201 'partner_mobile': fields.char('Mobile', size=32),
202 'partner_phone': fields.char('Phone', size=32),
203 'type_id': fields.many2one ('project.task.type', 'Resolution', domain="[('project_ids', '=', project_id)]"),
204 'project_id':fields.many2one('project.project', 'Project'),
205 'duration': fields.float('Duration'),
206 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
207 'day_open': fields.function(_compute_day, string='Days to Open', \
208 method=True, multi='compute_day', type="float", store=True),
209 'day_close': fields.function(_compute_day, string='Days to Close', \
210 method=True, multi='compute_day', type="float", store=True),
211 'assigned_to': fields.many2one('res.users', 'Assigned to', required=False, select=1),
212 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
213 method=True, multi='compute_day', type="float", store=True),
214 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
215 method=True, multi='compute_day', type="float", store=True),
216 'message_ids': fields.one2many('mailgate.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),
227 def _get_project(self, cr, uid, context=None):
228 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
229 if user.context_project_id:
230 return user.context_project_id.id
233 def on_change_project(self, cr, uid, ids, project_id, context=None):
237 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
239 result['value'] = {'user_id' : project.user_id.id}
246 #'user_id': crm.crm_case._get_default_user,
247 'partner_id': crm.crm_case._get_default_partner,
248 'partner_address_id': crm.crm_case._get_default_partner_address,
249 'email_from': crm.crm_case._get_default_email,
251 'section_id': crm.crm_case._get_section,
252 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
253 'priority': crm.AVAILABLE_PRIORITIES[2][0],
254 'project_id':_get_project,
255 'categ_id' : lambda *a: False,
256 #'assigned_to' : lambda obj, cr, uid, context: uid,
259 def convert_issue_task(self, cr, uid, ids, context=None):
260 case_obj = self.pool.get('project.issue')
261 data_obj = self.pool.get('ir.model.data')
262 task_obj = self.pool.get('project.task')
268 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
269 res = data_obj.read(cr, uid, result, ['res_id'])
270 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
271 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
273 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
275 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
277 for bug in case_obj.browse(cr, uid, ids, context=context):
278 new_task_id = task_obj.create(cr, uid, {
280 'partner_id': bug.partner_id.id,
281 'description':bug.description,
283 'project_id': bug.project_id.id,
284 'priority': bug.priority,
285 'user_id': bug.assigned_to.id,
286 'planned_hours': 0.0,
290 'task_id': new_task_id,
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 next_type(self, cr, uid, ids, *args):
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.type_ids or [])
331 self.write(cr, uid, task.id, {'type_id': types[0]})
332 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
333 index = types.index(typeid)
334 self.write(cr, uid, task.id, {'type_id': types[index+1]})
337 def prev_type(self, cr, uid, ids, *args):
338 for task in self.browse(cr, uid, ids):
339 typeid = task.type_id.id
340 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
342 if typeid and typeid in types:
343 index = types.index(typeid)
344 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
348 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
352 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
353 return {'value':{'assigned_to': task.user_id.id,}}
355 def case_escalate(self, cr, uid, ids, *args):
356 """Escalates case to top level
357 @param self: The object pointer
358 @param cr: the current row, from the database cursor,
359 @param uid: the current user’s ID for security checks,
360 @param ids: List of case Ids
361 @param *args: Tuple Value for additional Params
363 cases = self.browse(cr, uid, ids)
366 if case.project_id.project_escalation_id:
367 data['project_id'] = case.project_id.project_escalation_id.id
368 if case.project_id.project_escalation_id.user_id:
369 data['user_id'] = case.project_id.project_escalation_id.user_id.id
371 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
373 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
374 self.write(cr, uid, [case.id], data)
375 self._history(cr, uid, cases, _('Escalate'))
378 def message_new(self, cr, uid, msg, context=None):
380 Automatically calls when new email message arrives
382 @param self: The object pointer
383 @param cr: the current row, from the database cursor,
384 @param uid: the current user’s ID for security checks
388 mailgate_pool = self.pool.get('email.server.tools')
390 subject = msg.get('subject') or _('No Title')
391 body = msg.get('body')
392 msg_from = msg.get('from')
393 priority = msg.get('priority')
397 'email_from': msg_from,
398 'email_cc': msg.get('cc'),
402 if msg.get('priority', False):
403 vals['priority'] = priority
405 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
408 context.update({'state_to' : 'draft'})
409 res = self.create(cr, uid, vals, context=context)
410 self.convert_to_bug(cr, uid, [res], context=context)
412 attachents = msg.get('attachments', [])
413 for attactment in attachents or []:
416 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
417 'datas_fname': attactment,
418 'description': 'Mail attachment',
419 'res_model': self._name,
422 self.pool.get('ir.attachment').create(cr, uid, data_attach)
426 def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
428 @param self: The object pointer
429 @param cr: the current row, from the database cursor,
430 @param uid: the current user’s ID for security checks,
431 @param ids: List of update mail’s IDs
437 if isinstance(ids, (str, int, long)):
441 'description': msg['body']
443 if msg.get('priority', False):
444 vals['priority'] = msg.get('priority')
447 'cost': 'planned_cost',
448 'revenue': 'planned_revenue',
449 'probability': 'probability'
452 # Reassign the 'open' state to the case if this one is in pending or done
453 for record in self.browse(cr, uid, ids, context=context):
454 if record.state in ('pending', 'done'):
455 record.write({'state' : 'open'})
458 for line in msg['body'].split('\n'):
460 res = tools.misc.command_re.match(line)
461 if res and maps.get(res.group(1).lower(), False):
462 key = maps.get(res.group(1).lower())
463 vls[key] = res.group(2).lower()
466 res = self.write(cr, uid, ids, vals)
469 def msg_send(self, cr, uid, id, *args, **argv):
472 @param self: The object pointer
473 @param cr: the current row, from the database cursor,
474 @param uid: the current user’s ID for security checks,
475 @param ids: List of email’s IDs
476 @param *args: Return Tuple Value
477 @param **args: Return Dictionary of Keyword Value
481 def copy(self, cr, uid, id, default=None, context=None):
482 issue = self.read(cr, uid, id, ['name'], context=context)
485 default = default.copy()
486 default['name'] = issue['name'] + _(' (copy)')
487 return super(project_issue, self).copy(cr, uid, id, default=default,
492 class project(osv.osv):
493 _inherit = "project.project"
495 '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)]}),
496 '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)]}),
497 'reply_to' : fields.char('Reply-To Email Address', size=256)
500 def _check_escalation(self, cr, uid, ids, context=None):
501 project_obj = self.browse(cr, uid, ids[0], context=context)
502 if project_obj.project_escalation_id:
503 if project_obj.project_escalation_id.id == project_obj.id:
508 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
512 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: