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, id 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')})
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):
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):
149 issue_pool = self.pool.get('project.issue')
150 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
151 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
154 def _get_issue_work(self, cr, uid, ids, context=None):
158 issue_pool = self.pool.get('project.issue')
159 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
161 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
164 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
165 task_pool = self.pool.get('project.task')
167 for issue in self.browse(cr, uid, ids, context=context):
170 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
171 res[issue.id] = {'progress' : progress}
175 'id': fields.integer('ID'),
176 'name': fields.char('Issue', size=128, required=True),
177 'active': fields.boolean('Active', required=False),
178 'create_date': fields.datetime('Creation Date', readonly=True),
179 'write_date': fields.datetime('Update Date', readonly=True),
180 'date_deadline': fields.date('Deadline'),
181 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
182 select=True, help='Sales team to which Case belongs to.\
183 Define Responsible user and Email account for mail gateway.'),
184 'user_id': fields.many2one('res.users', 'Responsible'),
185 'partner_id': fields.many2one('res.partner', 'Partner'),
186 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
187 domain="[('partner_id','=',partner_id)]"),
188 'company_id': fields.many2one('res.company', 'Company'),
189 'description': fields.text('Description'),
190 'state': fields.selection([('draft', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
191 help='The state is set to \'Draft\', when a case is created.\
192 \nIf the case is in progress the state is set to \'Open\'.\
193 \nWhen the case is over, the state is set to \'Done\'.\
194 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
195 'email_from': fields.char('Email', size=128, help="These people will receive email."),
196 '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"),
197 'date_open': fields.datetime('Opened', readonly=True),
198 # Project Issue fields
199 'date_closed': fields.datetime('Closed', readonly=True),
200 'date': fields.datetime('Date'),
201 'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
202 " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
203 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
204 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
205 'version_id': fields.many2one('project.issue.version', 'Version'),
206 'partner_name': fields.char("Employee's Name", size=64),
207 'partner_mobile': fields.char('Mobile', size=32),
208 'partner_phone': fields.char('Phone', size=32),
209 'type_id': fields.many2one ('project.task.type', 'Resolution'),
210 'project_id':fields.many2one('project.project', 'Project'),
211 'duration': fields.float('Duration'),
212 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
213 'day_open': fields.function(_compute_day, string='Days to Open', \
214 method=True, multi='day_open', type="float", store=True),
215 'day_close': fields.function(_compute_day, string='Days to Close', \
216 method=True, multi='day_close', type="float", store=True),
217 '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'),
218 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
219 method=True, multi='working_days_open', type="float", store=True),
220 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
221 method=True, multi='working_days_close', type="float", store=True),
222 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
223 'date_action_last': fields.datetime('Last Action', readonly=1),
224 'date_action_next': fields.datetime('Next Action', readonly=1),
225 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
227 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
228 'project.task': (_get_issue_task, ['progress'], 10),
229 'project.task.work': (_get_issue_work, ['hours'], 10),
233 def _get_project(self, cr, uid, context=None):
236 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
237 if user.context_project_id:
238 return user.context_project_id.id
243 'user_id': crm.crm_case._get_default_user,
244 'partner_id': crm.crm_case._get_default_partner,
245 'partner_address_id': crm.crm_case._get_default_partner_address,
246 'email_from': crm.crm_case. _get_default_email,
248 'section_id': crm.crm_case. _get_section,
249 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
250 'priority': crm.AVAILABLE_PRIORITIES[2][0],
251 'project_id':_get_project,
254 def convert_issue_task(self, cr, uid, ids, context=None):
255 case_obj = self.pool.get('project.issue')
256 data_obj = self.pool.get('ir.model.data')
257 task_obj = self.pool.get('project.task')
263 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
264 res = data_obj.read(cr, uid, result, ['res_id'])
265 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
266 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
268 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
270 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
272 for bug in case_obj.browse(cr, uid, ids, context=context):
273 new_task_id = task_obj.create(cr, uid, {
275 'partner_id': bug.partner_id.id,
276 'description':bug.description,
278 'project_id': bug.project_id.id,
279 'priority': bug.priority,
280 'user_id': bug.assigned_to.id,
281 'planned_hours': 0.0,
285 'task_id': new_task_id,
288 case_obj.write(cr, uid, [bug.id], vals)
293 'view_mode': 'form,tree',
294 'res_model': 'project.task',
295 'res_id': int(new_task_id),
297 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
298 'type': 'ir.actions.act_window',
299 'search_view_id': res['res_id'],
304 def _convert(self, cr, uid, ids, xml_id, context=None):
307 data_obj = self.pool.get('ir.model.data')
308 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
311 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
313 self.write(cr, uid, ids, {'categ_id': categ_id})
316 def convert_to_feature(self, cr, uid, ids, context=None):
319 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
321 def convert_to_bug(self, cr, uid, ids, context=None):
324 return self._convert(cr, uid, ids, 'bug_categ', context=context)
326 def next_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.type_ids or [])
332 self.write(cr, uid, task.id, {'type_id': types[0]})
333 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
334 index = types.index(typeid)
335 self.write(cr, uid, task.id, {'type_id': types[index+1]})
338 def prev_type(self, cr, uid, ids, *args):
339 for task in self.browse(cr, uid, ids):
340 typeid = task.type_id.id
341 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
343 if typeid and typeid in types:
344 index = types.index(typeid)
345 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
349 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
355 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
356 return {'value':{'assigned_to': task.user_id.id,}}
358 def case_escalate(self, cr, uid, ids, *args):
359 """Escalates case to top level
360 @param self: The object pointer
361 @param cr: the current row, from the database cursor,
362 @param uid: the current user’s ID for security checks,
363 @param ids: List of case Ids
364 @param *args: Tuple Value for additional Params
366 cases = self.browse(cr, uid, ids)
369 if case.project_id.project_escalation_id:
370 data['project_id'] = case.project_id.project_escalation_id.id
371 if case.project_id.project_escalation_id.user_id:
372 data['user_id'] = case.project_id.project_escalation_id.user_id.id
374 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
376 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
377 self.write(cr, uid, [case.id], data)
378 self._history(cr, uid, cases, _('Escalate'))
381 def message_new(self, cr, uid, msg, context=None):
383 Automatically calls when new email message arrives
385 @param self: The object pointer
386 @param cr: the current row, from the database cursor,
387 @param uid: the current user’s ID for security checks
391 mailgate_pool = self.pool.get('email.server.tools')
393 subject = msg.get('subject') or _('No Title')
394 body = msg.get('body')
395 msg_from = msg.get('from')
396 priority = msg.get('priority')
400 'email_from': msg_from,
401 'email_cc': msg.get('cc'),
405 if msg.get('priority', False):
406 vals['priority'] = priority
408 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
411 context.update({'state_to' : 'draft'})
412 res = self.create(cr, uid, vals, context=context)
413 self.convert_to_bug(cr, uid, [res], context=context)
415 attachents = msg.get('attachments', [])
416 for attactment in attachents or []:
419 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
420 'datas_fname': attactment,
421 'description': 'Mail attachment',
422 'res_model': self._name,
425 self.pool.get('ir.attachment').create(cr, uid, data_attach)
429 def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
433 @param self: The object pointer
434 @param cr: the current row, from the database cursor,
435 @param uid: the current user’s ID for security checks,
436 @param ids: List of update mail’s IDs
439 if isinstance(ids, (str, int, long)):
443 'description': msg['body']
445 if msg.get('priority', False):
446 vals['priority'] = msg.get('priority')
449 'cost': 'planned_cost',
450 'revenue': 'planned_revenue',
451 'probability': 'probability'
454 for line in msg['body'].split('\n'):
456 res = tools.misc.command_re.match(line)
457 if res and maps.get(res.group(1).lower(), False):
458 key = maps.get(res.group(1).lower())
459 vls[key] = res.group(2).lower()
462 res = self.write(cr, uid, ids, vals)
465 def msg_send(self, cr, uid, id, *args, **argv):
468 @param self: The object pointer
469 @param cr: the current row, from the database cursor,
470 @param uid: the current user’s ID for security checks,
471 @param ids: List of email’s IDs
472 @param *args: Return Tuple Value
473 @param **args: Return Dictionary of Keyword Value
477 def copy(self, cr, uid, id, default=None, context=None):
480 issue = self.read(cr, uid, id, ['name'], context=context)
483 default = default.copy()
484 default['name'] = issue['name'] + _(' (copy)')
485 return super(project_issue, self).copy(cr, uid, id, default=default,
490 class project(osv.osv):
491 _inherit = "project.project"
493 '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)]}),
494 '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)]}),
495 'reply_to' : fields.char('Reply-To Email Address', size=256)
498 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: