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 ##############################################################################
26 from datetime import datetime, timedelta
32 from osv import fields,osv,orm
33 from osv.orm import except_orm
34 from tools.translate import _
37 class project_issue(crm.crm_case, osv.osv):
38 _name = "project.issue"
39 _description = "Project Issue"
40 _order = "priority, id desc"
41 _inherit = ['mailgate.thread']
43 def case_open(self, cr, uid, ids, *args):
45 @param self: The object pointer
46 @param cr: the current row, from the database cursor,
47 @param uid: the current user’s ID for security checks,
48 @param ids: List of case's Ids
49 @param *args: Give Tuple Value
52 res = super(project_issue, self).case_open(cr, uid, ids, *args)
53 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
54 for (id, name) in self.name_get(cr, uid, ids):
55 message = _('Issue ') + " '" + name + "' "+ _("is Open.")
56 self.log(cr, uid, id, message)
59 def case_close(self, cr, uid, ids, *args):
61 @param self: The object pointer
62 @param cr: the current row, from the database cursor,
63 @param uid: the current user’s ID for security checks,
64 @param ids: List of case's Ids
65 @param *args: Give Tuple Value
68 res = super(project_issue, self).case_close(cr, uid, ids, *args)
69 for (id, name) in self.name_get(cr, uid, ids):
70 message = _('Issue ') + " '" + name + "' "+ _("is Closed.")
71 self.log(cr, uid, id, message)
74 def _compute_day(self, cr, uid, ids, fields, args, context=None):
78 @param cr: the current row, from the database cursor,
79 @param uid: the current user’s ID for security checks,
80 @param ids: List of Openday’s IDs
81 @return: difference between current date and log date
82 @param context: A standard dictionary for contextual values
84 cal_obj = self.pool.get('resource.calendar')
85 res_obj = self.pool.get('resource.resource')
88 for issue in self.browse(cr, uid, ids, context=context):
95 if field in ['working_hours_open','day_open']:
97 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
98 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
99 ans = date_open - date_create
100 date_until = issue.date_open
101 #Calculating no. of working hours to open the issue
102 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
103 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
104 datetime.strptime(issue.date_open, '%Y-%m-%d %H:%M:%S'))
105 elif field in ['working_hours_close','day_close']:
106 if issue.date_closed:
107 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
108 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
109 date_until = issue.date_closed
110 ans = date_close - date_create
111 #Calculating no. of working hours to close the issue
112 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
113 datetime.strptime(issue.create_date, '%Y-%m-%d %H:%M:%S'),
114 datetime.strptime(issue.date_closed, '%Y-%m-%d %H:%M:%S'))
118 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
119 if resource_ids and len(resource_ids):
120 resource_id = resource_ids[0]
121 duration = float(ans.days)
122 if issue.project_id and issue.project_id.resource_calendar_id:
123 duration = float(ans.days) * 24
124 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)
126 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
127 for in_time, out_time in new_dates:
128 if in_time.date not in no_days:
129 no_days.append(in_time.date)
130 if out_time > date_until:
132 duration = len(no_days)
133 if field in ['working_hours_open','working_hours_close']:
134 res[issue.id][field] = hours
136 res[issue.id][field] = abs(float(duration))
140 'id': fields.integer('ID'),
141 'name': fields.char('Name', size=128, required=True),
142 'active': fields.boolean('Active', required=False),
143 'create_date': fields.datetime('Creation Date', readonly=True),
144 'write_date': fields.datetime('Update Date', readonly=True),
145 'date_deadline': fields.date('Deadline'),
146 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
147 select=True, help='Sales team to which Case belongs to.\
148 Define Responsible user and Email account for mail gateway.'),
149 'user_id': fields.many2one('res.users', 'Responsible'),
150 'partner_id': fields.many2one('res.partner', 'Partner'),
151 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
152 domain="[('partner_id','=',partner_id)]"),
153 'company_id': fields.many2one('res.company', 'Company'),
154 'description': fields.text('Description'),
155 'state': fields.selection([('draft', 'Draft'), ('open', 'To Do'), ('cancel', 'Cancelled'), ('done', 'Closed'),('pending', 'Pending'), ], 'State', size=16, readonly=True,
156 help='The state is set to \'Draft\', when a case is created.\
157 \nIf the case is in progress the state is set to \'Open\'.\
158 \nWhen the case is over, the state is set to \'Done\'.\
159 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
160 'email_from': fields.char('Email', size=128, help="These people will receive email."),
161 '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"),
162 'date_open': fields.datetime('Opened', readonly=True),
163 # Project Issue fields
164 'date_closed': fields.datetime('Closed', readonly=True),
165 'date': fields.datetime('Date'),
166 'canal_id': fields.many2one('res.partner.canal', 'Channel', help="The channels represent the different communication modes available with the customer." \
167 " With each commercial opportunity, you can indicate the canall which is this opportunity source."),
168 'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
169 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Severity'),
170 'type_id': fields.many2one('crm.case.resource.type', 'Version', domain="[('object_id.model', '=', 'project.issue')]"),
171 'partner_name': fields.char("Employee's Name", size=64),
172 'partner_mobile': fields.char('Mobile', size=32),
173 'partner_phone': fields.char('Phone', size=32),
174 'stage_id': fields.many2one ('crm.case.stage', 'Stage', domain="[('object_id.model', '=', 'project.issue')]"),
175 'project_id':fields.many2one('project.project', 'Project'),
176 'duration': fields.float('Duration'),
177 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
178 'day_open': fields.function(_compute_day, string='Days to Open', \
179 method=True, multi='day_open', type="float", store=True),
180 'day_close': fields.function(_compute_day, string='Days to Close', \
181 method=True, multi='day_close', type="float", store=True),
182 'assigned_to': fields.many2one('res.users', 'Assigned to', help='This is the current user to whom the related task have been assigned'),
183 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
184 method=True, multi='working_days_open', type="float", store=True),
185 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
186 method=True, multi='working_days_close', type="float", store=True),
187 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
188 'date_action_last': fields.datetime('Last Action', readonly=1),
189 'date_action_next': fields.datetime('Next Action', readonly=1),
192 def _get_project(self, cr, uid, context):
193 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
194 if user.context_project_id:
195 return user.context_project_id.id
200 'user_id': crm.crm_case._get_default_user,
201 'partner_id': crm.crm_case._get_default_partner,
202 'partner_address_id': crm.crm_case._get_default_partner_address,
203 'email_from': crm.crm_case. _get_default_email,
205 'section_id': crm.crm_case. _get_section,
206 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
207 'priority': crm.AVAILABLE_PRIORITIES[2][0],
208 'project_id':_get_project,
211 def convert_issue_task(self, cr, uid, ids, context=None):
212 case_obj = self.pool.get('project.issue')
213 data_obj = self.pool.get('ir.model.data')
214 task_obj = self.pool.get('project.task')
219 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
220 res = data_obj.read(cr, uid, result, ['res_id'])
221 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
222 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
224 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
226 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
228 for bug in case_obj.browse(cr, uid, ids, context=context):
229 new_task_id = task_obj.create(cr, uid, {
231 'partner_id': bug.partner_id.id,
232 'description':bug.description,
234 'project_id': bug.project_id.id,
235 'priority': bug.priority,
236 'user_id': bug.assigned_to.id,
237 'planned_hours': 0.0,
241 'task_id': new_task_id,
243 case_obj.write(cr, uid, [bug.id], vals)
248 'view_mode': 'form,tree',
249 'res_model': 'project.task',
250 'res_id': int(new_task_id),
252 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
253 'type': 'ir.actions.act_window',
254 'search_view_id': res['res_id'],
258 def _convert(self, cr, uid, ids, xml_id, context=None):
259 data_obj = self.pool.get('ir.model.data')
260 id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
263 categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
265 self.write(cr, uid, ids, {'categ_id': categ_id})
268 def convert_to_feature(self, cr, uid, ids, context=None):
269 return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
271 def convert_to_bug(self, cr, uid, ids, context=None):
272 return self._convert(cr, uid, ids, 'bug_categ', context=context)
274 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
279 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
280 if not stage.on_change:
284 def case_escalate(self, cr, uid, ids, *args):
285 """Escalates case to top level
286 @param self: The object pointer
287 @param cr: the current row, from the database cursor,
288 @param uid: the current user’s ID for security checks,
289 @param ids: List of case Ids
290 @param *args: Tuple Value for additional Params
292 cases = self.browse(cr, uid, ids)
295 if case.project_id.project_escalation_id:
296 data['project_id'] = case.project_id.project_escalation_id.id
297 if case.project_id.project_escalation_id.user_id:
298 data['user_id'] = case.project_id.project_escalation_id.user_id.id
300 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
302 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
303 self.write(cr, uid, [case.id], data)
306 def message_new(self, cr, uid, msg, context):
308 Automatically calls when new email message arrives
310 @param self: The object pointer
311 @param cr: the current row, from the database cursor,
312 @param uid: the current user’s ID for security checks
315 mailgate_pool = self.pool.get('email.server.tools')
317 subject = msg.get('subject') or _('No Title')
318 body = msg.get('body')
319 msg_from = msg.get('from')
320 priority = msg.get('priority')
324 'email_from': msg_from,
325 'email_cc': msg.get('cc'),
329 if msg.get('priority', False):
330 vals['priority'] = priority
332 res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
335 context.update({'state_to' : 'draft'})
336 res = self.create(cr, uid, vals, context)
337 message = _('An Issue created') + " '" + subject + "' " + _("from Mailgate.")
338 self.log(cr, uid, res, message)
339 self.convert_to_bug(cr, uid, [res], context=context)
341 attachents = msg.get('attachments', [])
342 for attactment in attachents or []:
345 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
346 'datas_fname': attactment,
347 'description': 'Mail attachment',
348 'res_model': self._name,
351 self.pool.get('ir.attachment').create(cr, uid, data_attach)
355 def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
359 @param self: The object pointer
360 @param cr: the current row, from the database cursor,
361 @param uid: the current user’s ID for security checks,
362 @param ids: List of update mail’s IDs
365 if isinstance(ids, (str, int, long)):
369 'description': msg['body']
371 if msg.get('priority', False):
372 vals['priority'] = msg.get('priority')
375 'cost': 'planned_cost',
376 'revenue': 'planned_revenue',
377 'probability': 'probability'
380 for line in msg['body'].split('\n'):
382 res = tools.misc.command_re.match(line)
383 if res and maps.get(res.group(1).lower(), False):
384 key = maps.get(res.group(1).lower())
385 vls[key] = res.group(2).lower()
388 res = self.write(cr, uid, ids, vals)
391 def msg_send(self, cr, uid, id, *args, **argv):
394 @param self: The object pointer
395 @param cr: the current row, from the database cursor,
396 @param uid: the current user’s ID for security checks,
397 @param ids: List of email’s IDs
398 @param *args: Return Tuple Value
399 @param **args: Return Dictionary of Keyword Value
405 class project(osv.osv):
406 _inherit = "project.project"
408 '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)]}),
409 '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)]}),
410 'reply_to' : fields.char('Reply-To Email Address', size=256)
413 def _check_escalation(self, cr, uid, ids):
414 project_obj = self.browse(cr, uid, ids[0])
415 if project_obj.project_escalation_id:
416 if project_obj.project_escalation_id.id == project_obj.id:
421 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
425 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: