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 = ['mailgate.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 mailgate_pool = self.pool.get('email.server.tools')
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 = mailgate_pool.get_partner(cr, uid, msg.get('from'))
397 context.update({'state_to' : 'draft'})
398 res = self.create(cr, uid, vals, context=context)
399 self.convert_to_bug(cr, uid, [res], context=context)
401 attachents = msg.get('attachments', [])
402 for attactment in attachents or []:
405 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
406 'datas_fname': attactment,
407 'description': 'Mail attachment',
408 'res_model': self._name,
411 self.pool.get('ir.attachment').create(cr, uid, data_attach)
415 def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
417 @param self: The object pointer
418 @param cr: the current row, from the database cursor,
419 @param uid: the current user’s ID for security checks,
420 @param ids: List of update mail’s IDs
426 if isinstance(ids, (str, int, long)):
430 'description': msg['body']
432 if msg.get('priority', False):
433 vals['priority'] = msg.get('priority')
436 'cost': 'planned_cost',
437 'revenue': 'planned_revenue',
438 'probability': 'probability'
441 # Reassign the 'open' state to the case if this one is in pending or done
442 for record in self.browse(cr, uid, ids, context=context):
443 if record.state in ('pending', 'done'):
444 record.write({'state' : 'open'})
447 for line in msg['body'].split('\n'):
449 res = tools.misc.command_re.match(line)
450 if res and maps.get(res.group(1).lower(), False):
451 key = maps.get(res.group(1).lower())
452 vls[key] = res.group(2).lower()
455 res = self.write(cr, uid, ids, vals)
458 def msg_send(self, cr, uid, id, *args, **argv):
461 @param self: The object pointer
462 @param cr: the current row, from the database cursor,
463 @param uid: the current user’s ID for security checks,
464 @param ids: List of email’s IDs
465 @param *args: Return Tuple Value
466 @param **args: Return Dictionary of Keyword Value
470 def copy(self, cr, uid, id, default=None, context=None):
471 issue = self.read(cr, uid, id, ['name'], context=context)
474 default = default.copy()
475 default['name'] = issue['name'] + _(' (copy)')
476 return super(project_issue, self).copy(cr, uid, id, default=default,
481 class project(osv.osv):
482 _inherit = "project.project"
484 '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)]}),
485 '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)]}),
486 'reply_to' : fields.char('Reply-To Email Address', size=256)
489 def _check_escalation(self, cr, uid, ids, context=None):
490 project_obj = self.browse(cr, uid, ids[0], context=context)
491 if project_obj.project_escalation_id:
492 if project_obj.project_escalation_id.id == project_obj.id:
497 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
501 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: