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.mail_compose_message.SUPPORTED_MODELS.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, create_date desc"
49 _inherit = ['mail.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'), 'user_id' : uid})
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'))
121 elif field in ['days_since_creation']:
122 if issue.create_date:
123 days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
124 res[issue.id][field] = days_since_creation.days
127 elif field in ['inactivity_days']:
128 res[issue.id][field] = 0
129 if issue.date_action_last:
130 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
131 res[issue.id][field] = inactive_days.days
136 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
137 if resource_ids and len(resource_ids):
138 resource_id = resource_ids[0]
139 duration = float(ans.days)
140 if issue.project_id and issue.project_id.resource_calendar_id:
141 duration = float(ans.days) * 24
142 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)
144 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
145 for in_time, out_time in new_dates:
146 if in_time.date not in no_days:
147 no_days.append(in_time.date)
148 if out_time > date_until:
150 duration = len(no_days)
151 if field in ['working_hours_open','working_hours_close']:
152 res[issue.id][field] = hours
154 res[issue.id][field] = abs(float(duration))
157 def _get_issue_task(self, cr, uid, ids, context=None):
159 issue_pool = self.pool.get('project.issue')
160 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
161 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
164 def _get_issue_work(self, cr, uid, ids, context=None):
166 issue_pool = self.pool.get('project.issue')
167 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
169 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
172 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
173 task_pool = self.pool.get('project.task')
175 for issue in self.browse(cr, uid, ids, context=context):
178 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
179 res[issue.id] = {'progress' : progress}
183 'id': fields.integer('ID', readonly=True),
184 'name': fields.char('Issue', size=128, required=True),
185 'active': fields.boolean('Active', required=False),
186 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
187 'write_date': fields.datetime('Update Date', readonly=True),
188 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
189 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
190 'date_deadline': fields.date('Deadline'),
191 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
192 select=True, help='Sales team to which Case belongs to.\
193 Define Responsible user and Email account for mail gateway.'),
194 'partner_id': fields.many2one('res.partner', 'Partner', select=1),
195 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
196 domain="[('partner_id','=',partner_id)]"),
197 'company_id': fields.many2one('res.company', 'Company'),
198 'description': fields.text('Description'),
199 'state': fields.selection([('draft', 'New'), ('open', 'To Solve'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
200 help='The state is set to \'Draft\', when a case is created.\
201 \nIf the case is in progress the state is set to \'Open\'.\
202 \nWhen the case is over, the state is set to \'Done\'.\
203 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
204 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
205 '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"),
206 'date_open': fields.datetime('Opened', readonly=True,select=True),
207 # Project Issue fields
208 'date_closed': fields.datetime('Closed', readonly=True,select=True),
209 'date': fields.datetime('Date'),
210 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
211 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
212 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
213 'version_id': fields.many2one('project.issue.version', 'Version'),
214 'type_id': fields.many2one ('project.task.type', 'Resolution', domain="[('project_ids', '=', project_id)]"),
215 'project_id':fields.many2one('project.project', 'Project'),
216 'duration': fields.float('Duration'),
217 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
218 'day_open': fields.function(_compute_day, string='Days to Open', \
219 multi='compute_day', type="float", store=True),
220 'day_close': fields.function(_compute_day, string='Days to Close', \
221 multi='compute_day', type="float", store=True),
222 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
223 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
224 multi='compute_day', type="float", store=True),
225 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
226 multi='compute_day', type="float", store=True),
227 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
228 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
229 'color': fields.integer('Color Index'),
230 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
231 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
232 'date_action_last': fields.datetime('Last Action', readonly=1),
233 'date_action_next': fields.datetime('Next Action', readonly=1),
234 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
236 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
237 'project.task': (_get_issue_task, ['progress'], 10),
238 'project.task.work': (_get_issue_work, ['hours'], 10),
242 def _get_project(self, cr, uid, context=None):
243 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
244 if user.context_project_id:
245 return user.context_project_id.id
248 def on_change_project(self, cr, uid, ids, project_id, context=None):
254 'partner_id': crm.crm_case._get_default_partner,
255 'partner_address_id': crm.crm_case._get_default_partner_address,
256 'email_from': crm.crm_case._get_default_email,
258 'section_id': crm.crm_case._get_section,
259 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
260 'priority': crm.AVAILABLE_PRIORITIES[2][0],
261 'project_id':_get_project,
262 'categ_id' : lambda *a: False,
265 def set_priority(self, cr, uid, ids, priority):
268 return self.write(cr, uid, ids, {'priority' : priority})
270 def set_high_priority(self, cr, uid, ids, *args):
271 """Set lead priority to high
273 return self.set_priority(cr, uid, ids, '1')
275 def set_normal_priority(self, cr, uid, ids, *args):
276 """Set lead priority to normal
278 return self.set_priority(cr, uid, ids, '3')
280 def convert_issue_task(self, cr, uid, ids, context=None):
281 case_obj = self.pool.get('project.issue')
282 data_obj = self.pool.get('ir.model.data')
283 task_obj = self.pool.get('project.task')
289 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
290 res = data_obj.read(cr, uid, result, ['res_id'])
291 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
292 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
294 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
296 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
298 for bug in case_obj.browse(cr, uid, ids, context=context):
299 new_task_id = task_obj.create(cr, uid, {
301 'partner_id': bug.partner_id.id,
302 'description':bug.description,
304 'project_id': bug.project_id.id,
305 'priority': bug.priority,
306 'user_id': bug.user_id.id,
307 'planned_hours': 0.0,
311 'task_id': new_task_id,
314 case_obj.write(cr, uid, [bug.id], vals)
319 'view_mode': 'form,tree',
320 'res_model': 'project.task',
321 'res_id': int(new_task_id),
323 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
324 'type': 'ir.actions.act_window',
325 'search_view_id': res['res_id'],
330 def _convert(self, cr, uid, ids, xml_id, context=None):
331 data_obj = self.pool.get('ir.model.data')
332 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
335 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
337 self.write(cr, uid, ids, {'categ_id': categ_id})
340 def convert_to_feature(self, cr, uid, ids, context=None):
341 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
343 def convert_to_bug(self, cr, uid, ids, context=None):
344 return self._convert(cr, uid, ids, 'bug_categ', context=context)
346 def next_type(self, cr, uid, ids, *args):
347 for task in self.browse(cr, uid, ids):
348 typeid = task.type_id.id
349 types = map(lambda x:x.id, task.project_id.type_ids or [])
352 self.write(cr, uid, task.id, {'type_id': types[0]})
353 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
354 index = types.index(typeid)
355 self.write(cr, uid, task.id, {'type_id': types[index+1]})
358 def prev_type(self, cr, uid, ids, *args):
359 for task in self.browse(cr, uid, ids):
360 typeid = task.type_id.id
361 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
363 if typeid and typeid in types:
364 index = types.index(typeid)
365 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
369 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
373 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
374 return {'value':{'user_id': task.user_id.id,}}
376 def case_escalate(self, cr, uid, ids, *args):
377 """Escalates case to top level
378 @param self: The object pointer
379 @param cr: the current row, from the database cursor,
380 @param uid: the current user’s ID for security checks,
381 @param ids: List of case Ids
382 @param *args: Tuple Value for additional Params
384 cases = self.browse(cr, uid, ids)
387 if case.project_id.project_escalation_id:
388 data['project_id'] = case.project_id.project_escalation_id.id
389 if case.project_id.project_escalation_id.user_id:
390 data['user_id'] = case.project_id.project_escalation_id.user_id.id
392 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
394 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
395 self.write(cr, uid, [case.id], data)
396 self.message_append(cr, uid, cases, _('Escalate'))
399 def message_new(self, cr, uid, msg, custom_values=None, context=None):
400 """Automatically called when new email message arrives"""
403 subject = msg.get('subject') or _('No Title')
404 body = msg.get('body_text')
405 msg_from = msg.get('from')
406 priority = msg.get('priority')
409 'email_from': msg_from,
410 'email_cc': msg.get('cc'),
415 vals['priority'] = priority
416 vals.update(self.message_partner_by_email(cr, uid, msg_from))
417 context.update({'state_to' : 'draft'})
419 if custom_values and isinstance(custom_values, dict):
420 vals.update(custom_values)
422 res_id = self.create(cr, uid, vals, context)
423 self.message_append_dict(cr, uid, [res_id], msg, context=context)
424 self.convert_to_bug(cr, uid, [res_id], context=context)
427 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
432 if isinstance(ids, (str, int, long)):
436 'description': msg['body_text']
438 if msg.get('priority', False):
439 vals['priority'] = msg.get('priority')
442 'cost': 'planned_cost',
443 'revenue': 'planned_revenue',
444 'probability': 'probability'
447 # Reassign the 'open' state to the case if this one is in pending or done
448 for record in self.browse(cr, uid, ids, context=context):
449 if record.state in ('pending', 'done'):
450 record.write({'state' : 'open'})
453 for line in msg['body_text'].split('\n'):
455 res = tools.misc.command_re.match(line)
456 if res and maps.get(res.group(1).lower(), False):
457 key = maps.get(res.group(1).lower())
458 vls[key] = res.group(2).lower()
461 res = self.write(cr, uid, ids, vals)
462 self.message_append_dict(cr, uid, ids, msg, context=context)
465 def copy(self, cr, uid, id, default=None, context=None):
466 issue = self.read(cr, uid, id, ['name'], context=context)
469 default = default.copy()
470 default['name'] = issue['name'] + _(' (copy)')
471 return super(project_issue, self).copy(cr, uid, id, default=default,
476 class project(osv.osv):
477 _inherit = "project.project"
479 '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)]}),
480 '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)]}),
481 'reply_to' : fields.char('Reply-To Email Address', size=256)
484 def _check_escalation(self, cr, uid, ids, context=None):
485 project_obj = self.browse(cr, uid, ids[0], context=context)
486 if project_obj.project_escalation_id:
487 if project_obj.project_escalation_id.id == project_obj.id:
492 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
496 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: