Merge remote-tracking branch 'odoo/7.0' into 7.0
[odoo/odoo.git] / addons / project_issue / project_issue.py
1  #-*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from openerp.addons.base_status.base_stage import base_stage
23 from openerp.addons.project.project import _TASK_STATE
24 from openerp.addons.crm import crm
25 from datetime import datetime
26 from openerp.osv import fields, osv, orm
27 from openerp.tools.translate import _
28 import binascii
29 import time
30 from openerp import tools
31 from openerp.tools import html2plaintext
32
33 class project_issue_version(osv.osv):
34     _name = "project.issue.version"
35     _order = "name desc"
36     _columns = {
37         'name': fields.char('Version Number', size=32, required=True),
38         'active': fields.boolean('Active', required=False),
39     }
40     _defaults = {
41         'active': 1,
42     }
43 project_issue_version()
44
45 class project_issue(base_stage, osv.osv):
46     _name = "project.issue"
47     _description = "Project Issue"
48     _order = "priority, create_date desc"
49     _inherit = ['mail.thread', 'ir.needaction_mixin']
50
51     _track = {
52         'state': {
53             'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'],
54             'project_issue.mt_issue_closed': lambda self, cr, uid, obj, ctx=None:  obj['state'] == 'done',
55             'project_issue.mt_issue_started': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open',
56         },
57         'stage_id': {
58             'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'done', 'open'],
59         },
60         'kanban_state': {
61             'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj['kanban_state'] == 'blocked',
62         },
63     }
64
65     def create(self, cr, uid, vals, context=None):
66         if context is None:
67             context = {}
68         if vals.get('project_id') and not context.get('default_project_id'):
69             context['default_project_id'] = vals.get('project_id')
70
71         # context: no_log, because subtype already handle this
72         create_context = dict(context, mail_create_nolog=True)
73         return super(project_issue, self).create(cr, uid, vals, context=create_context)
74
75     def _get_default_partner(self, cr, uid, context=None):
76         """ Override of base_stage to add project specific behavior """
77         project_id = self._get_default_project_id(cr, uid, context)
78         if project_id:
79             project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
80             if project and project.partner_id:
81                 return project.partner_id.id
82         return super(project_issue, self)._get_default_partner(cr, uid, context=context)
83
84     def _get_default_project_id(self, cr, uid, context=None):
85         """ Gives default project by checking if present in the context """
86         return self._resolve_project_id_from_context(cr, uid, context=context)
87
88     def _get_default_stage_id(self, cr, uid, context=None):
89         """ Gives default stage_id """
90         project_id = self._get_default_project_id(cr, uid, context=context)
91         return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
92
93     def _resolve_project_id_from_context(self, cr, uid, context=None):
94         """ Returns ID of project based on the value of 'default_project_id'
95             context key, or None if it cannot be resolved to a single
96             project.
97         """
98         if context is None:
99             context = {}
100         if type(context.get('default_project_id')) in (int, long):
101             return context.get('default_project_id')
102         if isinstance(context.get('default_project_id'), basestring):
103             project_name = context['default_project_id']
104             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
105             if len(project_ids) == 1:
106                 return int(project_ids[0][0])
107         return None
108
109     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
110         access_rights_uid = access_rights_uid or uid
111         stage_obj = self.pool.get('project.task.type')
112         order = stage_obj._order
113         # lame hack to allow reverting search, should just work in the trivial case
114         if read_group_order == 'stage_id desc':
115             order = "%s desc" % order
116         # retrieve section_id from the context and write the domain
117         # - ('id', 'in', 'ids'): add columns that should be present
118         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
119         # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
120         search_domain = []
121         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
122         if project_id:
123             search_domain += ['|', ('project_ids', '=', project_id)]
124         search_domain += [('id', 'in', ids)]
125         # perform search
126         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
127         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
128         # restore order of the search
129         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
130
131         fold = {}
132         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
133             fold[stage.id] = stage.fold or False
134         return result, fold
135
136     def _compute_day(self, cr, uid, ids, fields, args, context=None):
137         """
138         @param cr: the current row, from the database cursor,
139         @param uid: the current user’s ID for security checks,
140         @param ids: List of Openday’s IDs
141         @return: difference between current date and log date
142         @param context: A standard dictionary for contextual values
143         """
144         cal_obj = self.pool.get('resource.calendar')
145         res_obj = self.pool.get('resource.resource')
146
147         res = {}
148         for issue in self.browse(cr, uid, ids, context=context):
149
150             # if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None
151             if not issue.project_id or not issue.project_id.resource_calendar_id:
152                 working_hours = None
153             else:
154                 working_hours = issue.project_id.resource_calendar_id.id
155
156             res[issue.id] = {}
157             for field in fields:
158                 duration = 0
159                 ans = False
160                 hours = 0
161
162                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
163                 if field in ['working_hours_open','day_open']:
164                     if issue.date_open:
165                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
166                         ans = date_open - date_create
167                         date_until = issue.date_open
168                         #Calculating no. of working hours to open the issue
169                         hours = cal_obj._interval_hours_get(cr, uid, working_hours,
170                                                            date_create,
171                                                            date_open,
172                                                            timezone_from_uid=issue.user_id.id or uid,
173                                                            exclude_leaves=False,
174                                                            context=context)
175                 elif field in ['working_hours_close','day_close']:
176                     if issue.date_closed:
177                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
178                         date_until = issue.date_closed
179                         ans = date_close - date_create
180                         #Calculating no. of working hours to close the issue
181                         hours = cal_obj._interval_hours_get(cr, uid, working_hours,
182                                                            date_create,
183                                                            date_close,
184                                                            timezone_from_uid=issue.user_id.id or uid,
185                                                            exclude_leaves=False,
186                                                            context=context)
187                 elif field in ['days_since_creation']:
188                     if issue.create_date:
189                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
190                         res[issue.id][field] = days_since_creation.days
191                     continue
192
193                 elif field in ['inactivity_days']:
194                     res[issue.id][field] = 0
195                     if issue.date_action_last:
196                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
197                         res[issue.id][field] = inactive_days.days
198                     continue
199                 if ans:
200                     resource_id = False
201                     if issue.user_id:
202                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
203                         if resource_ids and len(resource_ids):
204                             resource_id = resource_ids[0]
205                     duration = float(ans.days) + float(ans.seconds)/(24*3600)
206
207                 if field in ['working_hours_open','working_hours_close']:
208                     res[issue.id][field] = hours
209                 elif field in ['day_open','day_close']:
210                     res[issue.id][field] = duration
211
212         return res
213
214     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
215         task_pool = self.pool.get('project.task')
216         res = {}
217         for issue in self.browse(cr, uid, ids, context=context):
218             progress = 0.0
219             if issue.task_id:
220                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
221             res[issue.id] = {'progress' : progress}
222         return res
223
224     def on_change_project(self, cr, uid, ids, project_id, context=None):
225         if project_id:
226             project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
227             if project and project.partner_id:
228                 return {'value': {'partner_id': project.partner_id.id}}
229         return {}
230
231     def _get_issue_task(self, cr, uid, ids, context=None):
232         issues = []
233         issue_pool = self.pool.get('project.issue')
234         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
235             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
236         return issues
237
238     def _get_issue_work(self, cr, uid, ids, context=None):
239         issues = []
240         issue_pool = self.pool.get('project.issue')
241         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
242             if work.task_id:
243                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
244         return issues
245
246     _columns = {
247         'id': fields.integer('ID', readonly=True),
248         'name': fields.char('Issue', size=128, required=True),
249         'active': fields.boolean('Active', required=False),
250         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
251         'write_date': fields.datetime('Update Date', readonly=True),
252         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
253                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
254         'date_deadline': fields.date('Deadline'),
255         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
256                         select=True, help='Sales team to which Case belongs to.\
257                              Define Responsible user and Email account for mail gateway.'),
258         'partner_id': fields.many2one('res.partner', 'Contact', select=1),
259         'company_id': fields.many2one('res.company', 'Company'),
260         'description': fields.text('Private Note'),
261         'state': fields.related('stage_id', 'state', type="selection", store=True,
262                 selection=_TASK_STATE, string="Status", readonly=True, select=True,
263                 help='The status is set to \'Draft\', when a case is created.\
264                       If the case is in progress the status is set to \'Open\'.\
265                       When the case is over, the status is set to \'Done\'.\
266                       If the case needs to be reviewed then the status is \
267                       set to \'Pending\'.'),
268         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
269                                          track_visibility='onchange',
270                                          help="A Issue's kanban state indicates special situations affecting it:\n"
271                                               " * Normal is the default situation\n"
272                                               " * Blocked indicates something is preventing the progress of this issue\n"
273                                               " * Ready for next stage indicates the issue is ready to be pulled to the next stage",
274                                          readonly=True, required=False),
275         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
276         '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"),
277         'date_open': fields.datetime('Opened', readonly=True,select=True),
278         # Project Issue fields
279         'date_closed': fields.datetime('Closed', readonly=True,select=True),
280         'date': fields.datetime('Date'),
281         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
282         'categ_ids': fields.many2many('project.category', string='Tags'),
283         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
284         'version_id': fields.many2one('project.issue.version', 'Version'),
285         'stage_id': fields.many2one ('project.task.type', 'Stage',
286                         track_visibility='onchange', select=True,
287                         domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
288         'project_id': fields.many2one('project.project', 'Project', track_visibility='onchange', select=True),
289         'duration': fields.float('Duration'),
290         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
291         'day_open': fields.function(_compute_day, string='Days to Open', \
292                                 multi='compute_day', type="float", store=True),
293         'day_close': fields.function(_compute_day, string='Days to Close', \
294                                 multi='compute_day', type="float", store=True),
295         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1, track_visibility='onchange'),
296         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
297                                 multi='compute_day', type="float", store=True),
298         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
299                                 multi='compute_day', type="float", store=True),
300         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
301                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
302         'color': fields.integer('Color Index'),
303         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
304         'date_action_last': fields.datetime('Last Action', readonly=1),
305         'date_action_next': fields.datetime('Next Action', readonly=1),
306         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
307             store = {
308                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
309                 'project.task': (_get_issue_task, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
310                 'project.task.work': (_get_issue_work, ['hours'], 10),
311             }),
312     }
313
314     _defaults = {
315         'active': 1,
316         'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
317         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
318         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
319         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
320         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
321         'priority': crm.AVAILABLE_PRIORITIES[2][0],
322         'kanban_state': 'normal',
323         'user_id': lambda obj, cr, uid, context: uid,
324     }
325
326     _group_by_full = {
327         'stage_id': _read_group_stage_ids
328     }
329
330     def set_priority(self, cr, uid, ids, priority, *args):
331         """Set lead priority
332         """
333         return self.write(cr, uid, ids, {'priority' : priority})
334
335     def set_high_priority(self, cr, uid, ids, *args):
336         """Set lead priority to high
337         """
338         return self.set_priority(cr, uid, ids, '1')
339
340     def set_normal_priority(self, cr, uid, ids, *args):
341         """Set lead priority to normal
342         """
343         return self.set_priority(cr, uid, ids, '3')
344
345     def convert_issue_task(self, cr, uid, ids, context=None):
346         if context is None:
347             context = {}
348
349         case_obj = self.pool.get('project.issue')
350         data_obj = self.pool.get('ir.model.data')
351         task_obj = self.pool.get('project.task')
352
353         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
354         res = data_obj.read(cr, uid, result, ['res_id'])
355         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
356         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
357         if id2:
358             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
359         if id3:
360             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
361
362         for bug in case_obj.browse(cr, uid, ids, context=context):
363             new_task_id = task_obj.create(cr, uid, {
364                 'name': bug.name,
365                 'partner_id': bug.partner_id.id,
366                 'description':bug.description,
367                 'date_deadline': bug.date,
368                 'project_id': bug.project_id.id,
369                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
370                 'priority': str(int(bug.priority) - 1),
371                 'user_id': bug.user_id.id,
372                 'planned_hours': 0.0,
373             })
374             vals = {
375                 'task_id': new_task_id,
376                 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
377             }
378             message = _("Project issue <b>converted</b> to task.")
379             self.message_post(cr, uid, [bug.id], body=message, context=context)
380             case_obj.write(cr, uid, [bug.id], vals, context=context)
381
382         return  {
383             'name': _('Tasks'),
384             'view_type': 'form',
385             'view_mode': 'form,tree',
386             'res_model': 'project.task',
387             'res_id': int(new_task_id),
388             'view_id': False,
389             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
390             'type': 'ir.actions.act_window',
391             'search_view_id': res['res_id'],
392             'nodestroy': True
393         }
394
395     def copy(self, cr, uid, id, default=None, context=None):
396         issue = self.read(cr, uid, id, ['name'], context=context)
397         if not default:
398             default = {}
399         default = default.copy()
400         default.update(name=_('%s (copy)') % (issue['name']))
401         return super(project_issue, self).copy(cr, uid, id, default=default,
402                 context=context)
403
404     def write(self, cr, uid, ids, vals, context=None):
405     
406         #Update last action date every time the user changes the stage
407         if 'stage_id' in vals:
408             vals['date_action_last'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
409             if 'kanban_state' not in vals:
410                 vals.update(kanban_state='normal')
411             state = self.pool.get('project.task.type').browse(cr, uid, vals['stage_id'], context=context).state
412             for issue in self.browse(cr, uid, ids, context=context):
413                 # Change from draft to not draft EXCEPT cancelled: The issue has been opened -> set the opening date
414                 if issue.state == 'draft' and state not in ('draft', 'cancelled'):
415                     vals['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
416                 # Change from not done to done: The issue has been closed -> set the closing date
417                 if issue.state != 'done' and state == 'done':
418                     vals['date_closed'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
419
420         return super(project_issue, self).write(cr, uid, ids, vals, context)
421
422     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
423         if not task_id:
424             return {'value': {}}
425         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
426         return {'value': {'user_id': task.user_id.id, }}
427
428     def case_reset(self, cr, uid, ids, context=None):
429         """Resets case as draft
430         """
431         res = super(project_issue, self).case_reset(cr, uid, ids, context)
432         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
433         return res
434
435     # -------------------------------------------------------
436     # Stage management
437     # -------------------------------------------------------
438
439     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
440         return self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
441
442     def set_kanban_state_normal(self, cr, uid, ids, context=None):
443         return self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
444
445     def set_kanban_state_done(self, cr, uid, ids, context=None):
446         return self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
447
448     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
449         """ Override of the base.stage method
450             Parameter of the stage search taken from the issue:
451             - type: stage type must be the same or 'both'
452             - section_id: if set, stages must belong to this section or
453               be a default case
454         """
455         if isinstance(cases, (int, long)):
456             cases = self.browse(cr, uid, cases, context=context)
457         # collect all section_ids
458         section_ids = []
459         if section_id:
460             section_ids.append(section_id)
461         for task in cases:
462             if task.project_id:
463                 section_ids.append(task.project_id.id)
464         # OR all section_ids and OR with case_default
465         search_domain = []
466         if section_ids:
467             search_domain += [('|')] * (len(section_ids)-1)
468             for section_id in section_ids:
469                 search_domain.append(('project_ids', '=', section_id))
470         search_domain += list(domain)
471         # perform search, return the first found
472         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
473         if stage_ids:
474             return stage_ids[0]
475         return False
476
477     def case_cancel(self, cr, uid, ids, context=None):
478         """ Cancels case """
479         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
480         return True
481
482     def case_escalate(self, cr, uid, ids, context=None):
483         cases = self.browse(cr, uid, ids)
484         for case in cases:
485             data = {}
486             if case.project_id.project_escalation_id:
487                 data['project_id'] = case.project_id.project_escalation_id.id
488                 if case.project_id.project_escalation_id.user_id:
489                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
490                 if case.task_id:
491                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
492             else:
493                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
494             self.case_set(cr, uid, ids, 'draft', data, context=context)
495         return True
496
497     # -------------------------------------------------------
498     # Mail gateway
499     # -------------------------------------------------------
500
501     def message_get_reply_to(self, cr, uid, ids, context=None):
502         """ Override to get the reply_to of the parent project. """
503         return [issue.project_id.message_get_reply_to()[0] if issue.project_id else False
504                     for issue in self.browse(cr, uid, ids, context=context)]
505
506     def message_get_suggested_recipients(self, cr, uid, ids, context=None):
507         recipients = super(project_issue, self).message_get_suggested_recipients(cr, uid, ids, context=context)
508         try:
509             for issue in self.browse(cr, uid, ids, context=context):
510                 if issue.partner_id:
511                     self._message_add_suggested_recipient(cr, uid, recipients, issue, partner=issue.partner_id, reason=_('Customer'))
512                 elif issue.email_from:
513                     self._message_add_suggested_recipient(cr, uid, recipients, issue, email=issue.email_from, reason=_('Customer Email'))
514         except (osv.except_osv, orm.except_orm):  # no read access rights -> just ignore suggested recipients because this imply modifying followers
515             pass
516         return recipients
517
518     def message_new(self, cr, uid, msg, custom_values=None, context=None):
519         """ Overrides mail_thread message_new that is called by the mailgateway
520             through message_process.
521             This override updates the document according to the email.
522         """
523         if custom_values is None:
524             custom_values = {}
525         if context is None:
526             context = {}
527         context['state_to'] = 'draft'
528
529         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
530
531         defaults = {
532             'name':  msg.get('subject') or _("No Subject"),
533             'description': desc,
534             'email_from': msg.get('from'),
535             'email_cc': msg.get('cc'),
536             'partner_id': msg.get('author_id', False),
537             'user_id': False,
538         }
539         defaults.update(custom_values)
540         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
541         return res_id
542
543     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs):
544         """ Overrides mail_thread message_post so that we can set the date of last action field when
545             a new message is posted on the issue.
546         """
547         if context is None:
548             context = {}
549         
550         res = super(project_issue, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
551         
552         if thread_id and subtype:
553             self.write(cr, uid, thread_id, {'date_action_last': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)    
554         
555         return res   
556
557 class project(osv.osv):
558     _inherit = "project.project"
559
560     def _get_alias_models(self, cr, uid, context=None):
561         return [('project.task', "Tasks"), ("project.issue", "Issues")]
562
563     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
564         res = dict.fromkeys(ids, 0)
565         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
566         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
567             if issue.state not in ('done', 'cancelled'):
568                 res[issue.project_id.id] += 1
569         return res
570
571     _columns = {
572         '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)]}),
573         'issue_count': fields.function(_issue_count, type='integer', string="Unclosed Issues"),
574     }
575
576     def _check_escalation(self, cr, uid, ids, context=None):
577         project_obj = self.browse(cr, uid, ids[0], context=context)
578         if project_obj.project_escalation_id:
579             if project_obj.project_escalation_id.id == project_obj.id:
580                 return False
581         return True
582
583     _constraints = [
584         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
585     ]
586
587 project()
588
589 class account_analytic_account(osv.osv):
590     _inherit = 'account.analytic.account'
591     _description = 'Analytic Account'
592
593     _columns = {
594         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
595     }
596
597     def on_change_template(self, cr, uid, ids, template_id, context=None):
598         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
599         if template_id and 'value' in res:
600             template = self.browse(cr, uid, template_id, context=context)
601             res['value']['use_issues'] = template.use_issues
602         return res
603
604     def _trigger_project_creation(self, cr, uid, vals, context=None):
605         if context is None: context = {}
606         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
607         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
608
609 account_analytic_account()
610
611 class project_project(osv.osv):
612     _inherit = 'project.project'
613     _defaults = {
614         'use_issues': True
615     }
616
617 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: