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'))
119 elif field in ['inactivity_days']:
120 res[issue.id][field] = 0
121 if issue.date_action_last:
122 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
123 res[issue.id][field] = inactive_days.days
128 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
129 if resource_ids and len(resource_ids):
130 resource_id = resource_ids[0]
131 duration = float(ans.days)
132 if issue.project_id and issue.project_id.resource_calendar_id:
133 duration = float(ans.days) * 24
134 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)
136 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
137 for in_time, out_time in new_dates:
138 if in_time.date not in no_days:
139 no_days.append(in_time.date)
140 if out_time > date_until:
142 duration = len(no_days)
143 if field in ['working_hours_open','working_hours_close']:
144 res[issue.id][field] = hours
146 res[issue.id][field] = abs(float(duration))
149 def _get_issue_task(self, cr, uid, ids, context=None):
151 issue_pool = self.pool.get('project.issue')
152 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
153 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
156 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,select=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.related('project_id', 'user_id', type='many2one', relation='res.users', store=True, select=1, string='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,select=True),
198 # Project Issue fields
199 'date_closed': fields.datetime('Closed', readonly=True,select=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', domain="[('project_ids', '=', project_id)]"),
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 multi='compute_day', type="float", store=True),
215 'day_close': fields.function(_compute_day, string='Days to Close', \
216 multi='compute_day', type="float", store=True),
217 'assigned_to': fields.many2one('res.users', 'Assigned to', required=False, select=1),
218 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
219 multi='compute_day', type="float", store=True),
220 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
221 multi='compute_day', type="float", store=True),
222 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
223 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
224 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
225 'date_action_last': fields.datetime('Last Action', readonly=1),
226 'date_action_next': fields.datetime('Next Action', readonly=1),
227 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
229 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
230 'project.task': (_get_issue_task, ['progress'], 10),
231 'project.task.work': (_get_issue_work, ['hours'], 10),
235 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
241 def on_change_project(self, cr, uid, ids, project_id, context=None):
245 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
247 result['value'] = {'user_id' : project.user_id.id}
254 #'user_id': crm.crm_case._get_default_user,
255 'partner_id': crm.crm_case._get_default_partner,
256 'partner_address_id': crm.crm_case._get_default_partner_address,
257 'email_from': crm.crm_case._get_default_email,
259 'section_id': crm.crm_case._get_section,
260 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
261 'priority': crm.AVAILABLE_PRIORITIES[2][0],
262 'project_id':_get_project,
263 'categ_id' : lambda *a: False,
264 #'assigned_to' : lambda obj, cr, uid, context: uid,
267 def convert_issue_task(self, cr, uid, ids, context=None):
268 case_obj = self.pool.get('project.issue')
269 data_obj = self.pool.get('ir.model.data')
270 task_obj = self.pool.get('project.task')
276 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
277 res = data_obj.read(cr, uid, result, ['res_id'])
278 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
279 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
281 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
283 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
285 for bug in case_obj.browse(cr, uid, ids, context=context):
286 new_task_id = task_obj.create(cr, uid, {
288 'partner_id': bug.partner_id.id,
289 'description':bug.description,
291 'project_id': bug.project_id.id,
292 'priority': bug.priority,
293 'user_id': bug.assigned_to.id,
294 'planned_hours': 0.0,
298 'task_id': new_task_id,
301 case_obj.write(cr, uid, [bug.id], vals)
306 'view_mode': 'form,tree',
307 'res_model': 'project.task',
308 'res_id': int(new_task_id),
310 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
311 'type': 'ir.actions.act_window',
312 'search_view_id': res['res_id'],
317 def _convert(self, cr, uid, ids, xml_id, context=None):
318 data_obj = self.pool.get('ir.model.data')
319 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
322 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
324 self.write(cr, uid, ids, {'categ_id': categ_id})
327 def convert_to_feature(self, cr, uid, ids, context=None):
328 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
330 def convert_to_bug(self, cr, uid, ids, context=None):
331 return self._convert(cr, uid, ids, 'bug_categ', context=context)
333 def next_type(self, cr, uid, ids, *args):
334 for task in self.browse(cr, uid, ids):
335 typeid = task.type_id.id
336 types = map(lambda x:x.id, task.project_id.type_ids or [])
339 self.write(cr, uid, task.id, {'type_id': types[0]})
340 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
341 index = types.index(typeid)
342 self.write(cr, uid, task.id, {'type_id': types[index+1]})
345 def prev_type(self, cr, uid, ids, *args):
346 for task in self.browse(cr, uid, ids):
347 typeid = task.type_id.id
348 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
350 if typeid and typeid in types:
351 index = types.index(typeid)
352 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
356 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
360 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
361 return {'value':{'assigned_to': task.user_id.id,}}
363 def case_escalate(self, cr, uid, ids, *args):
364 """Escalates case to top level
365 @param self: The object pointer
366 @param cr: the current row, from the database cursor,
367 @param uid: the current user’s ID for security checks,
368 @param ids: List of case Ids
369 @param *args: Tuple Value for additional Params
371 cases = self.browse(cr, uid, ids)
374 if case.project_id.project_escalation_id:
375 data['project_id'] = case.project_id.project_escalation_id.id
376 if case.project_id.project_escalation_id.user_id:
377 data['user_id'] = case.project_id.project_escalation_id.user_id.id
379 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
381 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
382 self.write(cr, uid, [case.id], data)
383 self._history(cr, uid, cases, _('Escalate'))
386 def message_new(self, cr, uid, msg, context=None):
388 Automatically calls when new email message arrives
390 @param self: The object pointer
391 @param cr: the current row, from the database cursor,
392 @param uid: the current user’s ID for security checks
396 mailgate_pool = self.pool.get('email.server.tools')
398 subject = msg.get('subject') or _('No Title')
399 body = msg.get('body')
400 msg_from = msg.get('from')
401 priority = msg.get('priority')
405 'email_from': msg_from,
406 'email_cc': msg.get('cc'),
410 if msg.get('priority', False):
411 vals['priority'] = priority
413 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
416 context.update({'state_to' : 'draft'})
417 res = self.create(cr, uid, vals, context=context)
418 self.convert_to_bug(cr, uid, [res], context=context)
420 attachents = msg.get('attachments', [])
421 for attactment in attachents or []:
424 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
425 'datas_fname': attactment,
426 'description': 'Mail attachment',
427 'res_model': self._name,
430 self.pool.get('ir.attachment').create(cr, uid, data_attach)
434 def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
436 @param self: The object pointer
437 @param cr: the current row, from the database cursor,
438 @param uid: the current user’s ID for security checks,
439 @param ids: List of update mail’s IDs
445 if isinstance(ids, (str, int, long)):
449 'description': msg['body']
451 if msg.get('priority', False):
452 vals['priority'] = msg.get('priority')
455 'cost': 'planned_cost',
456 'revenue': 'planned_revenue',
457 'probability': 'probability'
460 # Reassign the 'open' state to the case if this one is in pending or done
461 for record in self.browse(cr, uid, ids, context=context):
462 if record.state in ('pending', 'done'):
463 record.write({'state' : 'open'})
466 for line in msg['body'].split('\n'):
468 res = tools.misc.command_re.match(line)
469 if res and maps.get(res.group(1).lower(), False):
470 key = maps.get(res.group(1).lower())
471 vls[key] = res.group(2).lower()
474 res = self.write(cr, uid, ids, vals)
477 def msg_send(self, cr, uid, id, *args, **argv):
480 @param self: The object pointer
481 @param cr: the current row, from the database cursor,
482 @param uid: the current user’s ID for security checks,
483 @param ids: List of email’s IDs
484 @param *args: Return Tuple Value
485 @param **args: Return Dictionary of Keyword Value
489 def copy(self, cr, uid, id, default=None, context=None):
490 issue = self.read(cr, uid, id, ['name'], context=context)
493 default = default.copy()
494 default['name'] = issue['name'] + _(' (copy)')
495 return super(project_issue, self).copy(cr, uid, id, default=default,
500 class project(osv.osv):
501 _inherit = "project.project"
503 '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)]}),
504 '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)]}),
505 'reply_to' : fields.char('Reply-To Email Address', size=256)
508 def _check_escalation(self, cr, uid, ids, context=None):
509 project_obj = self.browse(cr, uid, ids[0], context=context)
510 if project_obj.project_escalation_id:
511 if project_obj.project_escalation_id.id == project_obj.id:
516 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
520 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: