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):
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.many2one('res.users', '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'),
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='day_open', type="float", store=True),
209 'day_close': fields.function(_compute_day, string='Days to Close', \
210 method=True, multi='day_close', type="float", store=True),
211 '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'),
212 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
213 method=True, multi='working_days_open', type="float", store=True),
214 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
215 method=True, multi='working_days_close', 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, method=True, 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
235 'user_id': crm.crm_case._get_default_user,
236 'partner_id': crm.crm_case._get_default_partner,
237 'partner_address_id': crm.crm_case._get_default_partner_address,
238 'email_from': crm.crm_case. _get_default_email,
240 'section_id': crm.crm_case. _get_section,
241 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
242 'priority': crm.AVAILABLE_PRIORITIES[2][0],
243 'project_id':_get_project,
246 def convert_issue_task(self, cr, uid, ids, context=None):
247 case_obj = self.pool.get('project.issue')
248 data_obj = self.pool.get('ir.model.data')
249 task_obj = self.pool.get('project.task')
255 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
256 res = data_obj.read(cr, uid, result, ['res_id'])
257 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
258 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
260 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
262 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
264 for bug in case_obj.browse(cr, uid, ids, context=context):
265 new_task_id = task_obj.create(cr, uid, {
267 'partner_id': bug.partner_id.id,
268 'description':bug.description,
270 'project_id': bug.project_id.id,
271 'priority': bug.priority,
272 'user_id': bug.assigned_to.id,
273 'planned_hours': 0.0,
277 'task_id': new_task_id,
280 case_obj.write(cr, uid, [bug.id], vals)
285 'view_mode': 'form,tree',
286 'res_model': 'project.task',
287 'res_id': int(new_task_id),
289 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
290 'type': 'ir.actions.act_window',
291 'search_view_id': res['res_id'],
296 def _convert(self, cr, uid, ids, xml_id, context=None):
297 data_obj = self.pool.get('ir.model.data')
298 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
301 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
303 self.write(cr, uid, ids, {'categ_id': categ_id})
306 def convert_to_feature(self, cr, uid, ids, context=None):
307 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
309 def convert_to_bug(self, cr, uid, ids, context=None):
310 return self._convert(cr, uid, ids, 'bug_categ', context=context)
312 def next_type(self, cr, uid, ids, *args):
313 for task in self.browse(cr, uid, ids):
314 typeid = task.type_id.id
315 types = map(lambda x:x.id, task.project_id.type_ids or [])
318 self.write(cr, uid, task.id, {'type_id': types[0]})
319 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
320 index = types.index(typeid)
321 self.write(cr, uid, task.id, {'type_id': types[index+1]})
324 def prev_type(self, cr, uid, ids, *args):
325 for task in self.browse(cr, uid, ids):
326 typeid = task.type_id.id
327 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
329 if typeid and typeid in types:
330 index = types.index(typeid)
331 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
335 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
339 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
340 return {'value':{'assigned_to': task.user_id.id,}}
342 def case_escalate(self, cr, uid, ids, *args):
343 """Escalates case to top level
344 @param self: The object pointer
345 @param cr: the current row, from the database cursor,
346 @param uid: the current user’s ID for security checks,
347 @param ids: List of case Ids
348 @param *args: Tuple Value for additional Params
350 cases = self.browse(cr, uid, ids)
353 if case.project_id.project_escalation_id:
354 data['project_id'] = case.project_id.project_escalation_id.id
355 if case.project_id.project_escalation_id.user_id:
356 data['user_id'] = case.project_id.project_escalation_id.user_id.id
358 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
360 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
361 self.write(cr, uid, [case.id], data)
362 self._history(cr, uid, cases, _('Escalate'))
365 def message_new(self, cr, uid, msg, context=None):
367 Automatically calls when new email message arrives
369 @param self: The object pointer
370 @param cr: the current row, from the database cursor,
371 @param uid: the current user’s ID for security checks
375 mailgate_pool = self.pool.get('email.server.tools')
377 subject = msg.get('subject') or _('No Title')
378 body = msg.get('body')
379 msg_from = msg.get('from')
380 priority = msg.get('priority')
384 'email_from': msg_from,
385 'email_cc': msg.get('cc'),
389 if msg.get('priority', False):
390 vals['priority'] = priority
392 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
395 context.update({'state_to' : 'draft'})
396 res = self.create(cr, uid, vals, context=context)
397 self.convert_to_bug(cr, uid, [res], context=context)
399 attachents = msg.get('attachments', [])
401 for attactment in attachents or []:
404 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
405 'datas_fname': attactment,
406 'description': 'Mail attachment',
407 'res_model': self._name,
410 att_ids.append(self.pool.get('ir.attachment').create(cr, uid, data_attach))
414 def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
416 @param self: The object pointer
417 @param cr: the current row, from the database cursor,
418 @param uid: the current user’s ID for security checks,
419 @param ids: List of update mail’s IDs
425 if isinstance(ids, (str, int, long)):
429 'description': msg['body']
431 if msg.get('priority', False):
432 vals['priority'] = msg.get('priority')
435 'cost': 'planned_cost',
436 'revenue': 'planned_revenue',
437 'probability': 'probability'
440 # Reassign the 'open' state to the case if this one is in pending or done
441 for record in self.browse(cr, uid, ids, context=context):
442 if record.state in ('pending', 'done'):
443 record.write({'state' : 'open'})
446 for line in msg['body'].split('\n'):
448 res = tools.misc.command_re.match(line)
449 if res and maps.get(res.group(1).lower(), False):
450 key = maps.get(res.group(1).lower())
451 vls[key] = res.group(2).lower()
454 res = self.write(cr, uid, ids, vals)
457 def msg_send(self, cr, uid, id, *args, **argv):
460 @param self: The object pointer
461 @param cr: the current row, from the database cursor,
462 @param uid: the current user’s ID for security checks,
463 @param ids: List of email’s IDs
464 @param *args: Return Tuple Value
465 @param **args: Return Dictionary of Keyword Value
469 def copy(self, cr, uid, id, default=None, context=None):
470 issue = self.read(cr, uid, id, ['name'], context=context)
473 default = default.copy()
474 default['name'] = issue['name'] + _(' (copy)')
475 return super(project_issue, self).copy(cr, uid, id, default=default,
480 class project(osv.osv):
481 _inherit = "project.project"
483 '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)]}),
484 '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)]}),
485 'reply_to' : fields.char('Reply-To Email Address', size=256)
488 def _check_escalation(self, cr, uid, ids, context=None):
489 project_obj = self.browse(cr, uid, ids[0], context=context)
490 if project_obj.project_escalation_id:
491 if project_obj.project_escalation_id.id == project_obj.id:
496 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
500 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: