[MERGE] forward port of branch 7.0 up to revid 10037 mat@openerp.com-20140507133552...
authorChristophe Simonis <chs@openerp.com>
Wed, 7 May 2014 15:33:48 +0000 (17:33 +0200)
committerChristophe Simonis <chs@openerp.com>
Wed, 7 May 2014 15:33:48 +0000 (17:33 +0200)
bzr revid: chs@openerp.com-20140507153348-g2qw61dkxvps13br

1  2 
addons/account/account_move_line.py
addons/hr/hr.py
addons/hr_timesheet_sheet/hr_timesheet_sheet.py
addons/mail/mail_thread.py
addons/mail/res_config.py
addons/stock/stock.py
addons/warning/warning.py

@@@ -26,7 -26,7 +26,7 @@@ from operator import itemgette
  
  from lxml import etree
  
 -from openerp import netsvc
 +from openerp import workflow
  from openerp.osv import fields, osv, orm
  from openerp.tools.translate import _
  import openerp.addons.decimal_precision as dp
@@@ -515,7 -515,8 +515,7 @@@ class account_move_line(osv.osv)
          if context.get('period_id', False):
              return context['period_id']
          account_period_obj = self.pool.get('account.period')
 -        ctx = dict(context, account_period_prefer_normal=True)
 -        ids = account_period_obj.find(cr, uid, context=ctx)
 +        ids = account_period_obj.find(cr, uid, context=context)
          period_id = False
          if ids:
              period_id = ids[0]
              'line_id': map(lambda x: (4, x, False), ids),
              'line_partial_ids': map(lambda x: (3, x, False), ids)
          })
          # the id of the move.reconcile is written in the move.line (self) by the create method above
          # because of the way the line_id are defined: (4, x, False)
          for id in ids:
 -            wf_service.trg_trigger(uid, 'account.move.line', id, cr)
 +            workflow.trg_trigger(uid, 'account.move.line', id, cr)
  
          if lines and lines[0]:
              partner_id = lines[0].partner_id and lines[0].partner_id.id or False
          if context is None:
              context = {}
          period_pool = self.pool.get('account.period')
 -        ctx = dict(context, account_period_prefer_normal=True)
 -        pids = period_pool.find(cr, user, date, context=ctx)
 +        pids = period_pool.find(cr, user, date, context=context)
          if pids:
              res.update({
                  'period_id':pids[0]
              if opening_reconciliation:
                  obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
              obj_move_rec.unlink(cr, uid, unlink_ids)
-             if all_moves:
+             if len(all_moves) >= 2:
                  obj_move_line.reconcile_partial(cr, uid, all_moves, 'auto',context=context)
          return True
  
                  bool(journal.currency),bool(journal.analytic_journal_id)))
          return result
  
 -account_move_line()
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --combined addons/hr/hr.py
  #
  ##############################################################################
  
 -from openerp import addons
  import logging
 +
 +from openerp.modules.module import get_module_resource
  from openerp.osv import fields, osv
  from openerp.tools.translate import _
  from openerp import tools
 +from openerp.tools.translate import _
  
  _logger = logging.getLogger(__name__)
  
 +
  class hr_employee_category(osv.osv):
  
      def name_get(self, cr, uid, ids, context=None):
@@@ -51,9 -48,9 +51,9 @@@
      _name = "hr.employee.category"
      _description = "Employee Category"
      _columns = {
 -        'name': fields.char("Category", size=64, required=True),
 +        'name': fields.char("Employee Tag", size=64, required=True),
          'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
 -        'parent_id': fields.many2one('hr.employee.category', 'Parent Category', select=True),
 +        'parent_id': fields.many2one('hr.employee.category', 'Parent Employee Tag', select=True),
          'child_ids': fields.one2many('hr.employee.category', 'parent_id', 'Child Categories'),
          'employee_ids': fields.many2many('hr.employee', 'employee_category_rel', 'category_id', 'emp_id', 'Employees'),
      }
@@@ -72,6 -69,7 +72,6 @@@
          (_check_recursion, 'Error! You cannot create recursive Categories.', ['parent_id'])
      ]
  
 -hr_employee_category()
  
  class hr_job(osv.osv):
  
              store = {
                  'hr.job': (lambda self,cr,uid,ids,c=None: ids, ['no_of_recruitment'], 10),
                  'hr.employee': (_get_job_position, ['job_id'], 10),
 -            },
 +            }, type='integer',
              multi='no_of_employee'),
          'no_of_employee': fields.function(_no_of_employee, string="Current Number of Employees",
              help='Number of employees currently occupying this job position.',
              store = {
                  'hr.employee': (_get_job_position, ['job_id'], 10),
 -            },
 +            }, type='integer',
              multi='no_of_employee'),
 -        'no_of_recruitment': fields.float('Expected in Recruitment', help='Number of new employees you expect to recruit.'),
 +        'no_of_recruitment': fields.integer('Expected in Recruitment', help='Number of new employees you expect to recruit.'),
          'employee_ids': fields.one2many('hr.employee', 'job_id', 'Employees', groups='base.group_user'),
          'description': fields.text('Job Description'),
          'requirements': fields.text('Requirements'),
          'company_id': fields.many2one('res.company', 'Company'),
          'state': fields.selection([('open', 'No Recruitment'), ('recruit', 'Recruitement in Progress')], 'Status', readonly=True, required=True,
              help="By default 'In position', set it to 'In Recruitment' if recruitment process is going on for this job position."),
 +        'write_date': fields.datetime('Update Date', readonly=True),
      }
      _defaults = {
          'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.job', context=c),
 +        'no_of_recruitment': 0,
          'state': 'open',
      }
  
      _sql_constraints = [
 -        ('name_company_uniq', 'unique(name, company_id)', 'The name of the job position must be unique per company!'),
 +        ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
      ]
  
  
          self.write(cr, uid, ids, {'state': 'open', 'no_of_recruitment': 0})
          return True
  
 -hr_job()
  
  class hr_employee(osv.osv):
      _name = "hr.employee"
      _description = "Employee"
 +    _order = 'name_related'
      _inherits = {'resource.resource': "resource_id"}
 +    _inherit = ['mail.thread']
 +
 +    _mail_post_access = 'read'
  
      def _get_image(self, cr, uid, ids, name, args, context=None):
          result = dict.fromkeys(ids, False)
          for obj in self.browse(cr, uid, ids, context=context):
              result[obj.id] = tools.image_get_resized_images(obj.image)
          return result
 -    
 +
      def _set_image(self, cr, uid, id, name, value, args, context=None):
          return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
 -    
 +
      _columns = {
          #we need a related field in order to be able to sort the employee by name
          'name_related': fields.related('resource_id', 'name', type='char', string='Name', readonly=True, store=True),
          'sinid': fields.char('SIN No', size=32, help="Social Insurance Number"),
          'identification_id': fields.char('Identification No', size=32),
          'otherid': fields.char('Other Id', size=64),
 -        'gender': fields.selection([('male', 'Male'),('female', 'Female')], 'Gender'),
 +        'gender': fields.selection([('male', 'Male'), ('female', 'Female')], 'Gender'),
          'marital': fields.selection([('single', 'Single'), ('married', 'Married'), ('widower', 'Widower'), ('divorced', 'Divorced')], 'Marital Status'),
 -        'department_id':fields.many2one('hr.department', 'Department'),
 +        'department_id': fields.many2one('hr.department', 'Department'),
          'address_id': fields.many2one('res.partner', 'Working Address'),
          'address_home_id': fields.many2one('res.partner', 'Home Address'),
 -        'bank_account_id':fields.many2one('res.partner.bank', 'Bank Account Number', domain="[('partner_id','=',address_home_id)]", help="Employee bank salary account"),
 +        'bank_account_id': fields.many2one('res.partner.bank', 'Bank Account Number', domain="[('partner_id','=',address_home_id)]", help="Employee bank salary account"),
          'work_phone': fields.char('Work Phone', size=32, readonly=False),
          'mobile_phone': fields.char('Work Mobile', size=32, readonly=False),
          'work_email': fields.char('Work Email', size=240),
          'child_ids': fields.one2many('hr.employee', 'parent_id', 'Subordinates'),
          'resource_id': fields.many2one('resource.resource', 'Resource', ondelete='cascade', required=True),
          'coach_id': fields.many2one('hr.employee', 'Coach'),
 -        'job_id': fields.many2one('hr.job', 'Job'),
 +        'job_id': fields.many2one('hr.job', 'Job Title'),
          # image: all image fields are base64 encoded and PIL-supported
          'image': fields.binary("Photo",
              help="This field holds the image used as photo for the employee, limited to 1024x1024px."),
              help="Small-sized photo of the employee. It is automatically "\
                   "resized as a 64x64px image, with aspect ratio preserved. "\
                   "Use this field anywhere a small image is required."),
 -        'passport_id':fields.char('Passport No', size=64),
 +        'passport_id': fields.char('Passport No', size=64),
          'color': fields.integer('Color Index'),
          'city': fields.related('address_id', 'city', type='char', string='City'),
          'login': fields.related('user_id', 'login', type='char', string='Login', readonly=1),
          'last_login': fields.related('user_id', 'date', type='datetime', string='Latest Connection', readonly=1),
      }
  
 -    _order='name_related'
 +    def _get_default_image(self, cr, uid, context=None):
 +        image_path = get_module_resource('hr', 'static/src/img', 'default_image.png')
 +        return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
 +
 +    defaults = {
 +        'active': 1,
 +        'image': _get_default_image,
 +        'color': 0,
 +    }
+     
+     def copy_data(self, cr, uid, ids, default=None, context=None):
+         if default is None:
+             default = {}
+         default.update({'child_ids': False})
+         return super(hr_employee, self).copy_data(cr, uid, ids, default, context=context)
+         
      def create(self, cr, uid, data, context=None):
 -        employee_id = super(hr_employee, self).create(cr, uid, data, context=context)
 -        try:
 -            (model, mail_group_id) = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'group_all_employees')
 -            employee = self.browse(cr, uid, employee_id, context=context)
 -            self.pool.get('mail.group').message_post(cr, uid, [mail_group_id],
 -                body=_('Welcome to %s! Please help him/her take the first steps with OpenERP!') % (employee.name),
 -                subtype='mail.mt_comment', context=context)
 -        except:
 -            pass # group deleted: do not push a message
 +        if context is None:
 +            context = {}
 +        create_ctx = dict(context, mail_create_nolog=True)
 +        employee_id = super(hr_employee, self).create(cr, uid, data, context=create_ctx)
 +        employee = self.browse(cr, uid, employee_id, context=context)
 +        if employee.user_id:
 +            res_users = self.pool['res.users']
 +            # send a copy to every user of the company
 +            # TODO: post to the `Whole Company` mail.group when we'll be able to link to the employee record  
 +            _model, group_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user')
 +            user_ids = res_users.search(cr, uid, [('company_id', '=', employee.user_id.company_id.id),
 +                                                  ('groups_id', 'in', group_id)])
 +            partner_ids = list(set(u.partner_id.id for u in res_users.browse(cr, uid, user_ids, context=context)))
 +        else:
 +            partner_ids = []
 +        self.message_post(cr, uid, [employee_id],
 +            body=_('Welcome to %s! Please help him/her take the first steps with OpenERP!') % (employee.name),
 +            partner_ids=partner_ids,
 +            subtype='mail.mt_comment', context=context
 +        )
          return employee_id
  
      def unlink(self, cr, uid, ids, context=None):
              company_id = self.pool.get('res.company').browse(cr, uid, company, context=context)
              address = self.pool.get('res.partner').address_get(cr, uid, [company_id.partner_id.id], ['default'])
              address_id = address and address['default'] or False
 -        return {'value': {'address_id' : address_id}}
 +        return {'value': {'address_id': address_id}}
  
      def onchange_department_id(self, cr, uid, ids, department_id, context=None):
          value = {'parent_id': False}
          work_email = False
          if user_id:
              work_email = self.pool.get('res.users').browse(cr, uid, user_id, context=context).email
 -        return {'value': {'work_email' : work_email}}
 -
 -    def _get_default_image(self, cr, uid, context=None):
 -        image_path = addons.get_module_resource('hr', 'static/src/img', 'default_image.png')
 -        return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
 -
 -    _defaults = {
 -        'active': 1,
 -        'image': _get_default_image,
 -        'color': 0,
 -    }
 +        return {'value': {'work_email': work_email}}
 +
 +    def action_follow(self, cr, uid, ids, context=None):
 +        """ Wrapper because message_subscribe_users take a user_ids=None
 +            that receive the context without the wrapper. """
 +        return self.message_subscribe_users(cr, uid, ids, context=context)
 +
 +    def action_unfollow(self, cr, uid, ids, context=None):
 +        """ Wrapper because message_unsubscribe_users take a user_ids=None
 +            that receive the context without the wrapper. """
 +        return self.message_unsubscribe_users(cr, uid, ids, context=context)
 +
 +    def get_suggested_thread(self, cr, uid, removed_suggested_threads=None, context=None):
 +        """Show the suggestion of employees if display_employees_suggestions if the
 +        user perference allows it. """
 +        user = self.pool.get('res.users').browse(cr, uid, uid, context)
 +        if not user.display_employees_suggestions:
 +            return []
 +        else:
 +            return super(hr_employee, self).get_suggested_thread(cr, uid, removed_suggested_threads, context)
 +
 +    def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None):
 +        """ Overwrite of the original method to always follow user_id field,
 +        even when not track_visibility so that a user will follow it's employee
 +        """
 +        user_field_lst = []
 +        for name, column_info in self._all_columns.items():
 +            if name in auto_follow_fields and name in updated_fields and column_info.column._obj == 'res.users':
 +                user_field_lst.append(name)
 +        return user_field_lst
  
      def _check_recursion(self, cr, uid, ids, context=None):
          level = 100
          (_check_recursion, 'Error! You cannot create recursive hierarchy of Employee(s).', ['parent_id']),
      ]
  
 -hr_employee()
  
  class hr_department(osv.osv):
 -    _description = "Department"
 -    _inherit = 'hr.department'
 +
 +    def _dept_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
 +        res = self.name_get(cr, uid, ids, context=context)
 +        return dict(res)
 +
 +    _name = "hr.department"
      _columns = {
 +        'name': fields.char('Department Name', size=64, required=True),
 +        'complete_name': fields.function(_dept_name_get_fnc, type="char", string='Name'),
 +        'company_id': fields.many2one('res.company', 'Company', select=True, required=False),
 +        'parent_id': fields.many2one('hr.department', 'Parent Department', select=True),
 +        'child_ids': fields.one2many('hr.department', 'parent_id', 'Child Departments'),
          'manager_id': fields.many2one('hr.employee', 'Manager'),
          'member_ids': fields.one2many('hr.employee', 'department_id', 'Members', readonly=True),
 +        'jobs_ids': fields.one2many('hr.job', 'department_id', 'Jobs'),
 +        'note': fields.text('Note'),
      }
  
 +    _defaults = {
 +        'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.department', context=c),
 +    }
 +
 +    def _check_recursion(self, cr, uid, ids, context=None):
 +        if context is None:
 +            context = {}
 +        level = 100
 +        while len(ids):
 +            cr.execute('select distinct parent_id from hr_department where id IN %s',(tuple(ids),))
 +            ids = filter(None, map(lambda x:x[0], cr.fetchall()))
 +            if not level:
 +                return False
 +            level -= 1
 +        return True
 +
 +    _constraints = [
 +        (_check_recursion, 'Error! You cannot create recursive departments.', ['parent_id'])
 +    ]
 +
 +    def name_get(self, cr, uid, ids, context=None):
 +        if context is None:
 +            context = {}
 +        if not ids:
 +            return []
 +        reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
 +        res = []
 +        for record in reads:
 +            name = record['name']
 +            if record['parent_id']:
 +                name = record['parent_id'][1]+' / '+name
 +            res.append((record['id'], name))
 +        return res
 +
-     def copy(self, cr, uid, ids, default=None, context=None):
+     def copy_data(self, cr, uid, ids, default=None, context=None):
          if default is None:
              default = {}
-         default = default.copy()
          default['member_ids'] = []
-         return super(hr_department, self).copy(cr, uid, ids, default, context=context)
+         return super(hr_department, self).copy_data(cr, uid, ids, default, context=context)
  
 +
  class res_users(osv.osv):
      _name = 'res.users'
      _inherit = 'res.users'
  
+     def copy_data(self, cr, uid, ids, default=None, context=None):
+         if default is None:
+             default = {}
+         default.update({'employee_ids': False})
+         return super(res_users, self).copy_data(cr, uid, ids, default, context=context)
+     
 -    def create(self, cr, uid, data, context=None):
 -        user_id = super(res_users, self).create(cr, uid, data, context=context)
 -
 -        # add shortcut unless 'noshortcut' is True in context
 -        if not(context and context.get('noshortcut', False)):
 -            data_obj = self.pool.get('ir.model.data')
 -            try:
 -                data_id = data_obj._get_id(cr, uid, 'hr', 'ir_ui_view_sc_employee')
 -                view_id  = data_obj.browse(cr, uid, data_id, context=context).res_id
 -                self.pool.get('ir.ui.view_sc').copy(cr, uid, view_id, default = {
 -                                            'user_id': user_id}, context=context)
 -            except:
 -                # Tolerate a missing shortcut. See product/product.py for similar code.
 -                _logger.debug('Skipped meetings shortcut for user "%s".', data.get('name','<new'))
 -
 -        return user_id
 -
      _columns = {
          'employee_ids': fields.one2many('hr.employee', 'user_id', 'Related employees'),
 -        }
 -
 -res_users()
 +    }
  
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -28,6 -28,7 +28,6 @@@ import pyt
  from openerp.osv import fields, osv
  from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
  from openerp.tools.translate import _
 -from openerp import netsvc
  
  class hr_timesheet_sheet(osv.osv):
      _name = "hr_timesheet_sheet.sheet"
              self.check_employee_attendance_state(cr, uid, sheet.id, context=context)
              di = sheet.user_id.company_id.timesheet_max_difference
              if (abs(sheet.total_difference) < di) or not di:
 -                wf_service = netsvc.LocalService("workflow")
 -                wf_service.trg_validate(uid, 'hr_timesheet_sheet.sheet', sheet.id, 'confirm', cr)
 +                self.signal_confirm(cr, uid, [sheet.id])
              else:
                  raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
          return True
  
      def action_set_to_draft(self, cr, uid, ids, *args):
          self.write(cr, uid, ids, {'state': 'draft'})
 -        wf_service = netsvc.LocalService('workflow')
 -        for id in ids:
 -            wf_service.trg_create(uid, self._name, id, cr)
 +        self.create_workflow(cr, uid, ids)
          return True
  
      def name_get(self, cr, uid, ids, context=None):
@@@ -358,6 -362,7 +358,6 @@@ class hr_timesheet_line(osv.osv)
          return dict([(el, self.on_change_account_id(cr, uid, ids, el, context.get('user_id', uid))) for el in account_ids])
  
  
 -hr_timesheet_line()
  
  class hr_attendance(osv.osv):
      _inherit = "hr.attendance"
              employee = employee_obj.browse(cr, uid, employee_id, context=context)
              tz = employee.user_id.partner_id.tz
  
+         if not date:
+             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
          att_tz = timezone(tz or 'utc')
  
          attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
                  raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet'))
          return True
  
 -hr_attendance()
  
  class hr_timesheet_sheet_sheet_day(osv.osv):
      _name = "hr_timesheet_sheet.sheet.day"
                          GROUP BY name, sheet_id
                  )) AS bar""")
  
 -hr_timesheet_sheet_sheet_day()
  
  
  class hr_timesheet_sheet_sheet_account(osv.osv):
              group by l.account_id, s.id, l.to_invoice
          )""")
  
 -hr_timesheet_sheet_sheet_account()
  
  
  
@@@ -612,5 -623,6 +615,5 @@@ class res_company(osv.osv)
          'timesheet_max_difference': lambda *args: 0.0
      }
  
 -res_company()
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -23,13 -23,9 +23,13 @@@ import base6
  import datetime
  import dateutil
  import email
 +try:
 +    import simplejson as json
 +except ImportError:
 +    import json
 +from lxml import etree
  import logging
  import pytz
 -import re
  import socket
  import time
  import xmlrpclib
@@@ -77,7 -73,6 +77,7 @@@ class mail_thread(osv.AbstractModel)
      _name = 'mail.thread'
      _description = 'Email Thread'
      _mail_flat_thread = True
 +    _mail_post_access = 'write'
  
      # Automatic logging system if mail installed
      # _track = {
      #   :param function lambda: returns whether the tracking should record using this subtype
      _track = {}
  
 +    def get_empty_list_help(self, cr, uid, help, context=None):
 +        """ Override of BaseModel.get_empty_list_help() to generate an help message
 +            that adds alias information. """
 +        model = context.get('empty_list_help_model')
 +        res_id = context.get('empty_list_help_id')
 +        ir_config_parameter = self.pool.get("ir.config_parameter")
 +        catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
 +        document_name = context.get('empty_list_help_document_name', _('document'))
 +        alias = None
 +
 +        if catchall_domain and model and res_id:  # specific res_id -> find its alias (i.e. section_id specified)
 +            object_id = self.pool.get(model).browse(cr, uid, res_id, context=context)
 +            # check that the alias effectively creates new records
 +            if object_id.alias_id and object_id.alias_id.alias_name and \
 +                    object_id.alias_id.alias_model_id and \
 +                    object_id.alias_id.alias_model_id.model == self._name and \
 +                    object_id.alias_id.alias_force_thread_id == 0:
 +                alias = object_id.alias_id
 +        elif catchall_domain and model:  # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id)
 +            alias_obj = self.pool.get('mail.alias')
 +            alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False)], context=context, order='id ASC')
 +            if alias_ids and len(alias_ids) == 1:
 +                alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
 +
 +        if alias:
 +            alias_email = alias.name_get()[0][1]
 +            return _("""<p class='oe_view_nocontent_create'>
 +                            Click here to add new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a>
 +                        </p>
 +                        %(static_help)s"""
 +                    ) % {
 +                        'document': document_name,
 +                        'email': alias_email,
 +                        'static_help': help or ''
 +                    }
 +
 +        if document_name != 'document' and help and help.find("oe_view_nocontent_create") == -1:
 +            return _("<p class='oe_view_nocontent_create'>Click here to add new %(document)s</p>%(static_help)s") % {
 +                        'document': document_name,
 +                        'static_help': help or '',
 +                    }
 +
 +        return help
 +
      def _get_message_data(self, cr, uid, ids, name, args, context=None):
          """ Computes:
              - message_unread: has uid unread message for the document
              res[id].pop('message_unread_count', None)
          return res
  
 -    def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
 +    def read_followers_data(self, cr, uid, follower_ids, context=None):
 +        result = []
 +        technical_group = self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'group_no_one', context=context)
 +        for follower in self.pool.get('res.partner').browse(cr, uid, follower_ids, context=context):
 +            is_editable = uid in map(lambda x: x.id, technical_group.users)
 +            is_uid = uid in map(lambda x: x.id, follower.user_ids)
 +            data = (follower.id,
 +                    follower.name,
 +                    {'is_editable': is_editable, 'is_uid': is_uid},
 +                    )
 +            result.append(data)
 +        return result
 +
 +    def _get_subscription_data(self, cr, uid, ids, name, args, user_pid=None, context=None):
          """ Computes:
              - message_subtype_data: data about document subtypes: which are
                  available, which are followed if any """
          res = dict((id, dict(message_subtype_data='')) for id in ids)
 -        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
 +        if user_pid is None:
 +            user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
  
          # find current model subtypes, add them to a dictionary
          subtype_obj = self.pool.get('mail.message.subtype')
          ], context=context)
          for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
              thread_subtype_dict = res[fol.res_id]['message_subtype_data']
 -            for subtype in fol.subtype_ids:
 +            for subtype in [st for st in fol.subtype_ids if st.name in thread_subtype_dict]:
                  thread_subtype_dict[subtype.name]['followed'] = True
              res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
  
          self.message_subscribe(cr, uid, [id], list(new-old), context=context)
  
      def _search_followers(self, cr, uid, obj, name, args, context):
 +        """Search function for message_follower_ids
 +
 +        Do not use with operator 'not in'. Use instead message_is_followers
 +        """
          fol_obj = self.pool.get('mail.followers')
          res = []
          for field, operator, value in args:
              assert field == name
 +            # TOFIX make it work with not in
 +            assert operator != "not in", "Do not search message_follower_ids with 'not in'"
              fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
              res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
              res.append(('id', 'in', res_ids))
          return res
  
 +    def _search_is_follower(self, cr, uid, obj, name, args, context):
 +        """Search function for message_is_follower"""
 +        res = []
 +        for field, operator, value in args:
 +            assert field == name
 +            partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
 +            if (operator == '=' and value) or (operator == '!=' and not value):  # is a follower
 +                res_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
 +            else:  # is not a follower or unknown domain
 +                mail_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
 +                res_ids = self.search(cr, uid, [('id', 'not in', mail_ids)], context=context)
 +            res.append(('id', 'in', res_ids))
 +        return res
 +
      _columns = {
 -        'message_is_follower': fields.function(_get_followers,
 -            type='boolean', string='Is a Follower', multi='_get_followers,'),
 +        'message_is_follower': fields.function(_get_followers, type='boolean',
 +            fnct_search=_search_is_follower, string='Is a Follower', multi='_get_followers,'),
          'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
 -                fnct_search=_search_followers, type='many2many', priority=-10,
 -                obj='res.partner', string='Followers', multi='_get_followers'),
 +            fnct_search=_search_followers, type='many2many', priority=-10,
 +            obj='res.partner', string='Followers', multi='_get_followers'),
          'message_ids': fields.one2many('mail.message', 'res_id',
              domain=lambda self: [('model', '=', self._name)],
              auto_join=True,
                   "be inserted in kanban views."),
      }
  
 +    def _get_user_chatter_options(self, cr, uid, context=None):
 +        options = {
 +            'display_log_button': False
 +        }
 +        group_ids = self.pool.get('res.users').browse(cr, uid, uid, context=context).groups_id
 +        group_user_id = self.pool.get("ir.model.data").get_object_reference(cr, uid, 'base', 'group_user')[1]
 +        is_employee = group_user_id in [group.id for group in group_ids]
 +        if is_employee:
 +            options['display_log_button'] = True
 +        return options
 +
 +    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
 +        res = super(mail_thread, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
 +        if view_type == 'form':
 +            doc = etree.XML(res['arch'])
 +            for node in doc.xpath("//field[@name='message_ids']"):
 +                options = json.loads(node.get('options', '{}'))
 +                options.update(self._get_user_chatter_options(cr, uid, context=context))
 +                node.set('options', json.dumps(options))
 +            res['arch'] = etree.tostring(doc)
 +        return res
 +
      #------------------------------------------------------
      # CRUD overrides for automatic subscription and logging
      #------------------------------------------------------
              context = {}
          if isinstance(ids, (int, long)):
              ids = [ids]
 -
          # Track initial values of tracked fields
          track_ctx = dict(context)
          if 'lang' not in track_ctx:
              track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
          tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
          if tracked_fields:
 -            initial = self.read(cr, uid, ids, tracked_fields.keys(), context=track_ctx)
 -            initial_values = dict((item['id'], item) for item in initial)
 +            records = self.browse(cr, uid, ids, context=track_ctx)
 +            initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
  
          # Perform write, update followers
          result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
          fol_obj.unlink(cr, SUPERUSER_ID, fol_ids, context=context)
          return res
  
-     def copy(self, cr, uid, id, default=None, context=None):
+     def copy_data(self, cr, uid, id, default=None, context=None):
          # avoid tracking multiple temporary changes during copy
          context = dict(context or {}, mail_notrack=True)
  
          default = default or {}
          default['message_ids'] = []
          default['message_follower_ids'] = []
-         return super(mail_thread, self).copy(cr, uid, id, default=default, context=context)
+         return super(mail_thread, self).copy_data(cr, uid, id, default=default, context=context)
  
      #------------------------------------------------------
      # Automatically log tracked fields
              if not value:
                  return ''
              if col_info['type'] == 'many2one':
 -                return value[1]
 +                return value.name_get()[0][1]
              if col_info['type'] == 'selection':
                  return dict(col_info['selection'])[value]
              return value
          if not tracked_fields:
              return True
  
 -        for record in self.read(cr, uid, ids, tracked_fields.keys(), context=context):
 -            initial = initial_values[record['id']]
 -            changes = []
 +        for browse_record in self.browse(cr, uid, ids, context=context):
 +            initial = initial_values[browse_record.id]
 +            changes = set()
              tracked_values = {}
  
              # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
              for col_name, col_info in tracked_fields.items():
 -                if record[col_name] == initial[col_name] and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
 +                initial_value = initial[col_name]
 +                record_value = getattr(browse_record, col_name)
 +
 +                if record_value == initial_value and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
                      tracked_values[col_name] = dict(col_info=col_info['string'],
 -                                                        new_value=convert_for_display(record[col_name], col_info))
 -                elif record[col_name] != initial[col_name]:
 +                                                        new_value=convert_for_display(record_value, col_info))
 +                elif record_value != initial_value and (record_value or initial_value):  # because browse null != False
                      if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']:
                          tracked_values[col_name] = dict(col_info=col_info['string'],
 -                                                            old_value=convert_for_display(initial[col_name], col_info),
 -                                                            new_value=convert_for_display(record[col_name], col_info))
 +                                                            old_value=convert_for_display(initial_value, col_info),
 +                                                            new_value=convert_for_display(record_value, col_info))
                      if col_name in tracked_fields:
 -                        changes.append(col_name)
 +                        changes.add(col_name)
              if not changes:
                  continue
  
                  if field not in changes:
                      continue
                  for subtype, method in track_info.items():
 -                    if method(self, cr, uid, record, context):
 +                    if method(self, cr, uid, browse_record, context):
                          subtypes.append(subtype)
  
              posted = False
              for subtype in subtypes:
 -                try:
 -                    subtype_rec = self.pool.get('ir.model.data').get_object(cr, uid, subtype.split('.')[0], subtype.split('.')[1], context=context)
 -                except ValueError, e:
 -                    _logger.debug('subtype %s not found, giving error "%s"' % (subtype, e))
 +                subtype_rec = self.pool.get('ir.model.data').xmlid_to_object(cr, uid, subtype, context=context)
 +                if not (subtype_rec and subtype_rec.exists()):
 +                    _logger.debug('subtype %s not found' % subtype)
                      continue
                  message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
 -                self.message_post(cr, uid, record['id'], body=message, subtype=subtype, context=context)
 +                self.message_post(cr, uid, browse_record.id, body=message, subtype=subtype, context=context)
                  posted = True
              if not posted:
                  message = format_message('', tracked_values)
 -                self.message_post(cr, uid, record['id'], body=message, context=context)
 +                self.message_post(cr, uid, browse_record.id, body=message, context=context)
          return True
  
      #------------------------------------------------------
          ir_attachment_obj.unlink(cr, uid, attach_ids, context=context)
          return True
  
 +    def check_mail_message_access(self, cr, uid, mids, operation, model_obj=None, context=None):
 +        """ mail.message check permission rules for related document. This method is
 +            meant to be inherited in order to implement addons-specific behavior.
 +            A common behavior would be to allow creating messages when having read
 +            access rule on the document, for portal document such as issues. """
 +        if not model_obj:
 +            model_obj = self
 +        if hasattr(self, '_mail_post_access'):
 +            create_allow = self._mail_post_access
 +        else:
 +            create_allow = 'write'
 +
 +        if operation in ['write', 'unlink']:
 +            check_operation = 'write'
 +        elif operation == 'create' and create_allow in ['create', 'read', 'write', 'unlink']:
 +            check_operation = create_allow
 +        elif operation == 'create':
 +            check_operation = 'write'
 +        else:
 +            check_operation = operation
 +
 +        model_obj.check_access_rights(cr, uid, check_operation)
 +        model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
 +
 +    def _get_formview_action(self, cr, uid, id, model=None, context=None):
 +        """ Return an action to open the document. This method is meant to be
 +            overridden in addons that want to give specific view ids for example.
 +
 +            :param int id: id of the document to open
 +            :param string model: specific model that overrides self._name
 +        """
 +        return {
 +                'type': 'ir.actions.act_window',
 +                'res_model': model or self._name,
 +                'view_type': 'form',
 +                'view_mode': 'form',
 +                'views': [(False, 'form')],
 +                'target': 'current',
 +                'res_id': id,
 +            }
 +
 +    def _get_inbox_action_xml_id(self, cr, uid, context=None):
 +        """ When redirecting towards the Inbox, choose which action xml_id has
 +            to be fetched. This method is meant to be inherited, at least in portal
 +            because portal users have a different Inbox action than classic users. """
 +        return ('mail', 'action_mail_inbox_feeds')
 +
 +    def message_redirect_action(self, cr, uid, context=None):
 +        """ For a given message, return an action that either
 +            - opens the form view of the related document if model, res_id, and
 +              read access to the document
 +            - opens the Inbox with a default search on the conversation if model,
 +              res_id
 +            - opens the Inbox with context propagated
 +
 +        """
 +        if context is None:
 +            context = {}
 +
 +        # default action is the Inbox action
 +        self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
 +        act_model, act_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, *self._get_inbox_action_xml_id(cr, uid, context=context))
 +        action = self.pool.get(act_model).read(cr, uid, act_id, [])
 +        params = context.get('params')
 +        msg_id = model = res_id = None
 +
 +        if params:
 +            msg_id = params.get('message_id')
 +            model = params.get('model')
 +            res_id = params.get('res_id')
 +        if not msg_id and not (model and res_id):
 +            return action
 +        if msg_id and not (model and res_id):
 +            msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
 +            if msg.exists():
 +                model, res_id = msg.model, msg.res_id
 +
 +        # if model + res_id found: try to redirect to the document or fallback on the Inbox
 +        if model and res_id:
 +            model_obj = self.pool.get(model)
 +            if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
 +                try:
 +                    model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context)
 +                    if not hasattr(model_obj, '_get_formview_action'):
 +                        action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context)
 +                    else:
 +                        action = model_obj._get_formview_action(cr, uid, res_id, context=context)
 +                except (osv.except_osv, orm.except_orm):
 +                    pass
 +            action.update({
 +                'context': {
 +                    'search_default_model': model,
 +                    'search_default_res_id': res_id,
 +                }
 +            })
 +        return action
 +
      #------------------------------------------------------
      # Email specific
      #------------------------------------------------------
  
      def message_get_reply_to(self, cr, uid, ids, context=None):
 +        """ Returns the preferred reply-to email address that is basically
 +            the alias of the document, if it exists. """
          if not self._inherits.get('mail.alias'):
              return [False for id in ids]
          return ["%s@%s" % (record['alias_name'], record['alias_domain'])
          """ Used by the plugin addon, based for plugin_outlook and others. """
          ret_dict = {}
          for model_name in self.pool.obj_list():
 -            model = self.pool.get(model_name)
 +            model = self.pool[model_name]
              if hasattr(model, "message_process") and hasattr(model, "message_post"):
                  ret_dict[model_name] = model._description
          return ret_dict
      def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None):
          """ Find partners related to some header fields of the message.
  
 -            TDE TODO: merge me with other partner finding methods in 8.0 """
 -        partner_obj = self.pool.get('res.partner')
 -        partner_ids = []
 +            :param string message: an email.message instance """
          s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
 -        for email_address in tools.email_split(s):
 -            related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context)
 -            if not related_partners:
 -                related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context)
 -            partner_ids += related_partners
 -        return partner_ids
 +        return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
 +
 +    def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
 +        """ Verify route validity. Check and rules:
 +            1 - if thread_id -> check that document effectively exists; otherwise
 +                fallback on a message_new by resetting thread_id
 +            2 - check that message_update exists if thread_id is set; or at least
 +                that message_new exist
 +            [ - find author_id if udpate_author is set]
 +            3 - if there is an alias, check alias_contact:
 +                'followers' and thread_id:
 +                    check on target document that the author is in the followers
 +                'followers' and alias_parent_thread_id:
 +                    check on alias parent document that the author is in the
 +                    followers
 +                'partners': check that author_id id set
 +        """
 +
 +        assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
 +        assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
 +
 +        message_id = message.get('Message-Id')
 +        email_from = decode_header(message, 'From')
 +        author_id = message_dict.get('author_id')
 +        model, thread_id, alias = route[0], route[1], route[4]
 +        model_pool = None
 +
 +        def _create_bounce_email():
 +            mail_mail = self.pool.get('mail.mail')
 +            mail_id = mail_mail.create(cr, uid, {
 +                            'body_html': '<div><p>Hello,</p>'
 +                                '<p>The following email sent to %s cannot be accepted because this is '
 +                                'a private email address. Only allowed people can contact us at this address.</p></div>'
 +                                '<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
 +                            'subject': 'Re: %s' % message.get('subject'),
 +                            'email_to': message.get('from'),
 +                            'auto_delete': True,
 +                        }, context=context)
 +            mail_mail.send(cr, uid, [mail_id], context=context)
 +
 +        def _warn(message):
 +            _logger.warning('Routing mail with Message-Id %s: route %s: %s',
 +                                message_id, route, message)
 +
 +        # Wrong model
 +        if model and not model in self.pool:
 +            if assert_model:
 +                assert model in self.pool, 'Routing: unknown target model %s' % model
 +            _warn('unknown target model %s' % model)
 +            return ()
 +        elif model:
 +            model_pool = self.pool[model]
 +
 +        # Private message: should not contain any thread_id
 +        if not model and thread_id:
 +            if assert_model:
 +                if thread_id: 
 +                    raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id)
 +            _warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id)
 +            thread_id = 0
 +        # Private message: should have a parent_id (only answers)
 +        if not model and not message_dict.get('parent_id'):
 +            if assert_model:
 +                if not message_dict.get('parent_id'):
 +                    raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
 +            _warn('posting a message without model should be with a parent_id (private mesage), skipping')
 +            return ()
 +
 +        # Existing Document: check if exists; if not, fallback on create if allowed
 +        if thread_id and not model_pool.exists(cr, uid, thread_id):
 +            if create_fallback:
 +                _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
 +                thread_id = None
 +            elif assert_model:
 +                assert model_pool.exists(cr, uid, thread_id), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
 +            else:
 +                _warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
 +                return ()
 +
 +        # Existing Document: check model accepts the mailgateway
 +        if thread_id and model and not hasattr(model_pool, 'message_update'):
 +            if create_fallback:
 +                _warn('model %s does not accept document update, fall back on document creation' % model)
 +                thread_id = None
 +            elif assert_model:
 +                assert hasattr(model_pool, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
 +            else:
 +                _warn('model %s does not accept document update, skipping' % model)
 +                return ()
 +
 +        # New Document: check model accepts the mailgateway
 +        if not thread_id and model and not hasattr(model_pool, 'message_new'):
 +            if assert_model:
 +                if not hasattr(model_pool, 'message_new'):
 +                    raise ValueError(
 +                        'Model %s does not accept document creation, crashing' % model
 +                    )
 +            _warn('model %s does not accept document creation, skipping' % model)
 +            return ()
  
 -    def _message_find_user_id(self, cr, uid, message, context=None):
 -        """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """
 -        from_local_part = tools.email_split(decode(message.get('From')))[0]
 -        # FP Note: canonification required, the minimu: .lower()
 -        user_ids = self.pool.get('res.users').search(cr, uid, ['|',
 -            ('login', '=', from_local_part),
 -            ('email', '=', from_local_part)], context=context)
 -        return user_ids[0] if user_ids else uid
 +        # Update message author if asked
 +        # We do it now because we need it for aliases (contact settings)
 +        if not author_id and update_author:
 +            author_ids = self._find_partner_from_emails(cr, uid, thread_id, [email_from], model=model, context=context)
 +            if author_ids:
 +                author_id = author_ids[0]
 +                message_dict['author_id'] = author_id
  
 -    def message_route(self, cr, uid, message, model=None, thread_id=None,
 +        # Alias: check alias_contact settings
 +        if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id):
 +            if thread_id:
 +                obj = self.pool[model].browse(cr, uid, thread_id, context=context)
 +            else:
 +                obj = self.pool[alias.alias_parent_model_id.model].browse(cr, uid, alias.alias_parent_thread_id, context=context)
 +            if not author_id or not author_id in [fol.id for fol in obj.message_follower_ids]:
 +                _warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
 +                _create_bounce_email()
 +                return ()
 +        elif alias and alias.alias_contact == 'partners' and not author_id:
 +            _warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
 +            _create_bounce_email()
 +            return ()
 +
 +        return (model, thread_id, route[2], route[3], route[4])
 +
 +    def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
                        custom_values=None, context=None):
          """Attempt to figure out the correct target model, thread_id,
          custom_values and user_id to use for an incoming message.
               4. If all the above fails, raise an exception.
  
             :param string message: an email.message instance
 +           :param dict message_dict: dictionary holding message variables
             :param string model: the fallback model to use if the message
                 does not match any of the currently configured mail aliases
                 (may be None if a matching alias is supposed to be present)
             :param int thread_id: optional ID of the record/thread from ``model``
                 to which this mail should be attached. Only used if the message
                 does not reply to an existing thread and does not match any mail alias.
 -           :return: list of [model, thread_id, custom_values, user_id]
 +           :return: list of [model, thread_id, custom_values, user_id, alias]
  
          :raises: ValueError, TypeError
          """
          if not isinstance(message, Message):
              raise TypeError('message must be an email.message.Message at this point')
 +        mail_msg_obj = self.pool['mail.message']
 +        fallback_model = model
 +
 +        # Get email.message.Message variables for future processing
          message_id = message.get('Message-Id')
          email_from = decode_header(message, 'From')
          email_to = decode_header(message, 'To')
          references = decode_header(message, 'References')
          in_reply_to = decode_header(message, 'In-Reply-To')
 -
 -        # 1. Verify if this is a reply to an existing thread
          thread_references = references or in_reply_to
 +
 +        # 1. message is a reply to an existing message (exact match of message_id)
 +        msg_references = thread_references.split()
 +        mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
 +        if mail_message_ids:
 +            original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
 +            model, thread_id = original_msg.model, original_msg.res_id
 +            _logger.info(
 +                'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
 +                email_from, email_to, message_id, model, thread_id, custom_values, uid)
 +            route = self.message_route_verify(
 +                cr, uid, message, message_dict,
 +                (model, thread_id, custom_values, uid, None),
 +                update_author=True, assert_model=True, create_fallback=True, context=context)
 +            return route and [route] or []
 +
 +        # 2. message is a reply to an existign thread (6.1 compatibility)
          ref_match = thread_references and tools.reference_re.search(thread_references)
          if ref_match:
              reply_thread_id = int(ref_match.group(1))
 -            reply_model = ref_match.group(2) or model
 +            reply_model = ref_match.group(2) or fallback_model
              reply_hostname = ref_match.group(3)
              local_hostname = socket.gethostname()
              # do not match forwarded emails from another OpenERP system (thread_id collision!)
              if local_hostname == reply_hostname:
                  thread_id, model = reply_thread_id, reply_model
 -                model_pool = self.pool.get(model)
 -                if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
 -                    and hasattr(model_pool, 'message_update'):
 -                    _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
 -                                    email_from, email_to, message_id, model, thread_id, custom_values, uid)
 -                    return [(model, thread_id, custom_values, uid)]
 -
 -        # Verify whether this is a reply to a private message
 +                if thread_id and model in self.pool:
 +                    model_obj = self.pool[model]
 +                    compat_mail_msg_ids = mail_msg_obj.search(
 +                        cr, uid, [
 +                            ('message_id', '=', False),
 +                            ('model', '=', model),
 +                            ('res_id', '=', thread_id),
 +                        ], context=context)
 +                    if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
 +                        _logger.info(
 +                            'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
 +                            email_from, email_to, message_id, model, thread_id, custom_values, uid)
 +                        route = self.message_route_verify(
 +                            cr, uid, message, message_dict,
 +                            (model, thread_id, custom_values, uid, None),
 +                            update_author=True, assert_model=True, create_fallback=True, context=context)
 +                        return route and [route] or []
 +
 +        # 2. Reply to a private message
          if in_reply_to:
 -            message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
 -            if message_ids:
 -                message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
 +            mail_message_ids = mail_msg_obj.search(cr, uid, [
 +                                ('message_id', '=', in_reply_to),
 +                                '!', ('message_id', 'ilike', 'reply_to')
 +                            ], limit=1, context=context)
 +            if mail_message_ids:
 +                mail_message = mail_msg_obj.browse(cr, uid, mail_message_ids[0], context=context)
                  _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
 -                                email_from, email_to, message_id, message.id, custom_values, uid)
 -                return [(message.model, message.res_id, custom_values, uid)]
 +                                email_from, email_to, message_id, mail_message.id, custom_values, uid)
 +                route = self.message_route_verify(cr, uid, message, message_dict,
 +                                (mail_message.model, mail_message.res_id, custom_values, uid, None),
 +                                update_author=True, assert_model=True, create_fallback=True, context=context)
 +                return route and [route] or []
  
 -        # 2. Look for a matching mail.alias entry
 +        # 3. Look for a matching mail.alias entry
          # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
          # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
          rcpt_tos = \
                          # user_id = self._message_find_user_id(cr, uid, message, context=context)
                          user_id = uid
                          _logger.info('No matching user_id for the alias %s', alias.alias_name)
 -                    routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
 -                                   eval(alias.alias_defaults), user_id))
 -                _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
 -                                email_from, email_to, message_id, routes)
 +                    route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
 +                    _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
 +                                email_from, email_to, message_id, route)
 +                    route = self.message_route_verify(cr, uid, message, message_dict, route,
 +                                update_author=True, assert_model=True, create_fallback=True, context=context)
 +                    if route:
 +                        routes.append(route)
                  return routes
  
 -        # 3. Fallback to the provided parameters, if they work
 -        model_pool = self.pool.get(model)
 +        # 4. Fallback to the provided parameters, if they work
          if not thread_id:
              # Legacy: fallback to matching [ID] in the Subject
              match = tools.res_re.search(decode_header(message, 'Subject'))
                  thread_id = int(thread_id)
              except:
                  thread_id = False
 -        if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
 -            raise ValueError(
 +        _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
 +                    email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
 +        route = self.message_route_verify(cr, uid, message, message_dict,
 +                        (fallback_model, thread_id, custom_values, uid, None),
 +                        update_author=True, assert_model=True, context=context)
 +        if route:
 +            return [route]
 +
 +        # AssertionError if no routes found and if no bounce occured
 +        raise ValueError(
                  'No possible route found for incoming message from %s to %s (Message-Id %s:). '
                  'Create an appropriate mail.alias or force the destination model.' %
                  (email_from, email_to, message_id)
              )
 -        if thread_id and not model_pool.exists(cr, uid, thread_id):
 -            _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
 -                                thread_id, message_id)
 -            thread_id = None
 -        _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
 -                        email_from, email_to, message_id, model, thread_id, custom_values, uid)
 -        return [(model, thread_id, custom_values, uid)]
 +
 +    def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
 +        # postpone setting message_dict.partner_ids after message_post, to avoid double notifications
 +        partner_ids = message_dict.pop('partner_ids', [])
 +        thread_id = False
 +        for model, thread_id, custom_values, user_id, alias in routes:
 +            if self._name == 'mail.thread':
 +                context.update({'thread_model': model})
 +            if model:
 +                model_pool = self.pool[model]
 +                if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
 +                    raise ValueError(
 +                        "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" %
 +                        (message_dict['message_id'], model)
 +                    )
 +
 +                # disabled subscriptions during message_new/update to avoid having the system user running the
 +                # email gateway become a follower of all inbound messages
 +                nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
 +                if thread_id and hasattr(model_pool, 'message_update'):
 +                    model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
 +                else:
 +                    thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
 +            else:
 +                if thread_id:
 +                    raise ValueError("Posting a message without model should be with a null res_id, to create a private message.")
 +                model_pool = self.pool.get('mail.thread')
 +            if not hasattr(model_pool, 'message_post'):
 +                context['thread_model'] = model
 +                model_pool = self.pool['mail.thread']
 +            new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **message_dict)
 +
 +            if partner_ids:
 +                # postponed after message_post, because this is an external message and we don't want to create
 +                # duplicate emails due to notifications
 +                self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
 +        return thread_id
  
      def message_process(self, cr, uid, model, message, custom_values=None,
                          save_original=False, strip_attachments=False,
                 to which this mail should be attached. When provided, this
                 overrides the automatic detection based on the message
                 headers.
 -
 -        :raises: ValueError, TypeError
          """
          if context is None:
              context = {}
          msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
          if strip_attachments:
              msg.pop('attachments', None)
 +
          if msg.get('message_id'):   # should always be True as message_parse generate one if missing
              existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
                                                                  ('message_id', '=', msg.get('message_id')),
                                                                  ], context=context)
              if existing_msg_ids:
 -                _logger.info('Ignored mail from %s to %s with Message-Id %s:: found duplicated Message-Id during processing',
 +                _logger.info('Ignored mail from %s to %s with Message-Id %s: found duplicated Message-Id during processing',
                                  msg.get('from'), msg.get('to'), msg.get('message_id'))
                  return False
  
          # find possible routes for the message
 -        routes = self.message_route(cr, uid, msg_txt, model,
 -                                    thread_id, custom_values,
 -                                    context=context)
 -
 -        # postpone setting msg.partner_ids after message_post, to avoid double notifications
 -        partner_ids = msg.pop('partner_ids', [])
 -
 -        thread_id = False
 -        for model, thread_id, custom_values, user_id in routes:
 -            if self._name == 'mail.thread':
 -                context.update({'thread_model': model})
 -            if model:
 -                model_pool = self.pool.get(model)
 -                if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
 -                    raise ValueError(
 -                        "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" %
 -                        (msg['message_id'], model)
 -                    )
 -
 -                # disabled subscriptions during message_new/update to avoid having the system user running the
 -                # email gateway become a follower of all inbound messages
 -                nosub_ctx = dict(context, mail_create_nosubscribe=True)
 -                if thread_id and hasattr(model_pool, 'message_update'):
 -                    model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
 -                else:
 -                    nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
 -                    thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
 -            else:
 -                if thread_id:
 -                    raise ValueError("Posting a message without model should be with a null res_id, to create a private message.")
 -                model_pool = self.pool.get('mail.thread')
 -            new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
 -
 -            if partner_ids:
 -                # postponed after message_post, because this is an external message and we don't want to create
 -                # duplicate emails due to notifications
 -                self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
 -
 +        routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
 +        thread_id = self.message_route_process(cr, uid, msg_txt, msg, routes, context=context)
          return thread_id
  
      def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
          if isinstance(custom_values, dict):
              data = custom_values.copy()
          model = context.get('thread_model') or self._name
 -        model_pool = self.pool.get(model)
 +        model_pool = self.pool[model]
          fields = model_pool.fields_get(cr, uid, context=context)
          if 'name' in fields and not data.get('name'):
              data['name'] = msg_dict.get('subject', '')
          """
          msg_dict = {
              'type': 'email',
 -            'author_id': False,
          }
          if not isinstance(message, Message):
              if isinstance(message, unicode):
          msg_dict['from'] = decode(message.get('from'))
          msg_dict['to'] = decode(message.get('to'))
          msg_dict['cc'] = decode(message.get('cc'))
 -
 -        if message.get('From'):
 -            author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
 -            if author_ids:
 -                msg_dict['author_id'] = author_ids[0]
 -            msg_dict['email_from'] = decode(message.get('from'))
 +        msg_dict['email_from'] = decode(message.get('from'))
          partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
          msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
  
                  partner_id, partner_name<partner_email> or partner_name, reason """
          if email and not partner:
              # get partner info from email
 -            partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context, res_id=obj.id)
 -            if partner_info and partner_info[0].get('partner_id'):
 -                partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info[0]['partner_id']], context=context)[0]
 +            partner_info = self.message_partner_info_from_emails(cr, uid, obj.id, [email], context=context)[0]
 +            if partner_info.get('partner_id'):
 +                partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info['partner_id']], context=context)[0]
          if email and email in [val[1] for val in result[obj.id]]:  # already existing email -> skip
              return result
          if partner and partner in obj.message_follower_ids:  # recipient already in the followers -> skip
                  self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
          return result
  
 -    def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None, res_id=None):
 -        """ Wrapper with weird order parameter because of 7.0 fix.
 +    def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True):
 +        """ Utility method to find partners from email addresses. The rules are :
 +            1 - check in document (model | self, id) followers
 +            2 - try to find a matching partner that is also an user
 +            3 - try to find a matching partner
  
 -            TDE TODO: remove me in 8.0 """
 -        return self.message_find_partner_from_emails(cr, uid, res_id, emails, link_mail=link_mail, context=context)
 +            :param list emails: list of email addresses
 +            :param string model: model to fetch related record; by default self
 +                is used.
 +            :param boolean check_followers: check in document followers
 +        """
 +        partner_obj = self.pool['res.partner']
 +        partner_ids = []
 +        obj = None
 +        if id and (model or self._name != 'mail.thread') and check_followers:
 +            if model:
 +                obj = self.pool[model].browse(cr, uid, id, context=context)
 +            else:
 +                obj = self.browse(cr, uid, id, context=context)
 +        for contact in emails:
 +            partner_id = False
 +            email_address = tools.email_split(contact)
 +            if not email_address:
 +                partner_ids.append(partner_id)
 +                continue
 +            email_address = email_address[0]
 +            # first try: check in document's followers
 +            if obj:
 +                for follower in obj.message_follower_ids:
 +                    if follower.email == email_address:
 +                        partner_id = follower.id
 +            # second try: check in partners that are also users
 +            if not partner_id:
 +                ids = partner_obj.search(cr, SUPERUSER_ID, [
 +                                                ('email', 'ilike', email_address),
 +                                                ('user_ids', '!=', False)
 +                                            ], limit=1, context=context)
 +                if ids:
 +                    partner_id = ids[0]
 +            # third try: check in partners
 +            if not partner_id:
 +                ids = partner_obj.search(cr, SUPERUSER_ID, [
 +                                                ('email', 'ilike', email_address)
 +                                            ], limit=1, context=context)
 +                if ids:
 +                    partner_id = ids[0]
 +            partner_ids.append(partner_id)
 +        return partner_ids
  
 -    def message_find_partner_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
 +    def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
          """ Convert a list of emails into a list partner_ids and a list
              new_partner_ids. The return value is non conventional because
              it is meant to be used by the mail widget.
  
 -            :return dict: partner_ids and new_partner_ids
 -
 -            TDE TODO: merge me with other partner finding methods in 8.0 """
 +            :return dict: partner_ids and new_partner_ids """
          mail_message_obj = self.pool.get('mail.message')
 -        partner_obj = self.pool.get('res.partner')
 +        partner_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context)
          result = list()
 -        if id and self._name != 'mail.thread':
 -            obj = self.browse(cr, SUPERUSER_ID, id, context=context)
 -        else:
 -            obj = None
 -        for email in emails:
 -            partner_info = {'full_name': email, 'partner_id': False}
 -            m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
 -            if not m:
 -                continue
 -            email_address = m.group(3)
 -            # first try: check in document's followers
 -            if obj:
 -                for follower in obj.message_follower_ids:
 -                    if follower.email == email_address:
 -                        partner_info['partner_id'] = follower.id
 -            # second try: check in partners
 -            if not partner_info.get('partner_id'):
 -                ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context)
 -                if not ids:
 -                    ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address)], limit=1, context=context)
 -                if ids:
 -                    partner_info['partner_id'] = ids[0]
 +        for idx in range(len(emails)):
 +            email_address = emails[idx]
 +            partner_id = partner_ids[idx]
 +            partner_info = {'full_name': email_address, 'partner_id': partner_id}
              result.append(partner_info)
  
              # link mail with this from mail to the new partner id
              if link_mail and partner_info['partner_id']:
                  message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
                                      '|',
 -                                    ('email_from', '=', email),
 -                                    ('email_from', 'ilike', '<%s>' % email),
 +                                    ('email_from', '=', email_address),
 +                                    ('email_from', 'ilike', '<%s>' % email_address),
                                      ('author_id', '=', False)
                                  ], context=context)
                  if message_ids:
                      mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': partner_info['partner_id']}, context=context)
          return result
  
 +    def _message_preprocess_attachments(self, cr, uid, attachments, attachment_ids, attach_model, attach_res_id, context=None):
 +        """ Preprocess attachments for mail_thread.message_post() or mail_mail.create().
 +
 +        :param list attachments: list of attachment tuples in the form ``(name,content)``,
 +                                 where content is NOT base64 encoded
 +        :param list attachment_ids: a list of attachment ids, not in tomany command form
 +        :param str attach_model: the model of the attachments parent record
 +        :param integer attach_res_id: the id of the attachments parent record
 +        """
 +        Attachment = self.pool['ir.attachment']
 +        m2m_attachment_ids = []
 +        if attachment_ids:
 +            filtered_attachment_ids = Attachment.search(cr, SUPERUSER_ID, [
 +                ('res_model', '=', 'mail.compose.message'),
 +                ('create_uid', '=', uid),
 +                ('id', 'in', attachment_ids)], context=context)
 +            if filtered_attachment_ids:
 +                Attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': attach_model, 'res_id': attach_res_id}, context=context)
 +            m2m_attachment_ids += [(4, id) for id in attachment_ids]
 +        # Handle attachments parameter, that is a dictionary of attachments
 +        for name, content in attachments:
 +            if isinstance(content, unicode):
 +                content = content.encode('utf-8')
 +            data_attach = {
 +                'name': name,
 +                'datas': base64.b64encode(str(content)),
 +                'datas_fname': name,
 +                'description': name,
 +                'res_model': attach_model,
 +                'res_id': attach_res_id,
 +            }
 +            m2m_attachment_ids.append((0, 0, data_attach))
 +        return m2m_attachment_ids
 +
      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):
 +                     subtype=None, parent_id=False, attachments=None, context=None,
 +                     content_subtype='html', **kwargs):
          """ Post a new message in an existing thread, returning the new
              mail.message ID.
  
          model = False
          if thread_id:
              model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
 -            if model != self._name:
 +            if model != self._name and hasattr(self.pool[model], 'message_post'):
                  del context['thread_model']
 -                return self.pool.get(model).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)
 -
 -        # 0: Parse email-from, try to find a better author_id based on document's followers for incoming emails
 -        email_from = kwargs.get('email_from')
 -        if email_from and thread_id and type == 'email' and kwargs.get('author_id'):
 -            email_list = tools.email_split(email_from)
 -            doc = self.browse(cr, uid, thread_id, context=context)
 -            if email_list and doc:
 -                author_ids = self.pool.get('res.partner').search(cr, uid, [
 -                                        ('email', 'ilike', email_list[0]),
 -                                        ('id', 'in', [f.id for f in doc.message_follower_ids])
 -                                    ], limit=1, context=context)
 -                if author_ids:
 -                    kwargs['author_id'] = author_ids[0]
 +                return self.pool[model].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)
 +
 +        #0: Find the message's author, because we need it for private discussion
          author_id = kwargs.get('author_id')
          if author_id is None:  # keep False values
              author_id = self.pool.get('mail.message')._get_default_author(cr, uid, context=context)
  
          # 3. Attachments
          #   - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
 -        attachment_ids = kwargs.pop('attachment_ids', []) or []  # because we could receive None (some old code sends None)
 -        if attachment_ids:
 -            filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
 -                ('res_model', '=', 'mail.compose.message'),
 -                ('create_uid', '=', uid),
 -                ('id', 'in', attachment_ids)], context=context)
 -            if filtered_attachment_ids:
 -                ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
 -        attachment_ids = [(4, id) for id in attachment_ids]
 -        # Handle attachments parameter, that is a dictionary of attachments
 -        for name, content in attachments:
 -            if isinstance(content, unicode):
 -                content = content.encode('utf-8')
 -            data_attach = {
 -                'name': name,
 -                'datas': base64.b64encode(str(content)),
 -                'datas_fname': name,
 -                'description': name,
 -                'res_model': model,
 -                'res_id': thread_id,
 -            }
 -            attachment_ids.append((0, 0, data_attach))
 +        attachment_ids = self._message_preprocess_attachments(cr, uid, attachments, kwargs.pop('attachment_ids', []), model, thread_id, context)
  
          # 4: mail.message.subtype
          subtype_id = False
          return msg_id
  
      #------------------------------------------------------
      # Followers API
      #------------------------------------------------------
  
 -    def message_get_subscription_data(self, cr, uid, ids, context=None):
 +    def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
          """ Wrapper to get subtypes data. """
 -        return self._get_subscription_data(cr, uid, ids, None, None, context=context)
 +        return self._get_subscription_data(cr, uid, ids, None, None, user_pid=user_pid, context=context)
  
      def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
          """ Wrapper on message_subscribe, using users. If user_ids is not
          ''', (ids, self._name, partner_id))
          return True
  
 -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
 +    #------------------------------------------------------
 +    # Thread suggestion
 +    #------------------------------------------------------
 +
 +    def get_suggested_thread(self, cr, uid, removed_suggested_threads=None, context=None):
 +        """Return a list of suggested threads, sorted by the numbers of followers"""
 +        if context is None:
 +            context = {}
 +
 +        # TDE HACK: originally by MAT from portal/mail_mail.py but not working until the inheritance graph bug is not solved in trunk
 +        # TDE FIXME: relocate in portal when it won't be necessary to reload the hr.employee model in an additional bridge module
 +        if self.pool['res.groups']._all_columns.get('is_portal'):
 +            user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
 +            if any(group.is_portal for group in user.groups_id):
 +                return []
 +
 +        threads = []
 +        if removed_suggested_threads is None:
 +            removed_suggested_threads = []
 +
 +        thread_ids = self.search(cr, uid, [('id', 'not in', removed_suggested_threads), ('message_is_follower', '=', False)], context=context)
 +        for thread in self.browse(cr, uid, thread_ids, context=context):
 +            data = {
 +                'id': thread.id,
 +                'popularity': len(thread.message_follower_ids),
 +                'name': thread.name,
 +                'image_small': thread.image_small
 +            }
 +            threads.append(data)
 +        return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]
@@@ -23,25 -23,24 +23,25 @@@ import urlpars
  
  from openerp.osv import osv, fields
  
 +
  class project_configuration(osv.TransientModel):
      _inherit = 'base.config.settings'
  
      _columns = {
 -        'alias_domain' : fields.char('Alias Domain',
 +        'alias_domain': fields.char('Alias Domain',
                                       help="If you have setup a catch-all email domain redirected to "
                                            "the OpenERP server, enter the domain name here."),
      }
  
      def get_default_alias_domain(self, cr, uid, ids, context=None):
-         alias_domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", context=context)
-         if not alias_domain:
+         alias_domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", default=None, context=context)
+         if alias_domain is None:
              domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "web.base.url", context=context)
              try:
                  alias_domain = urlparse.urlsplit(domain).netloc.split(':')[0]
              except Exception:
                  pass
-         return {'alias_domain': alias_domain}
+         return {'alias_domain': alias_domain or False}
  
      def set_alias_domain(self, cr, uid, ids, context=None):
          config_parameters = self.pool.get("ir.config_parameter")
diff --combined addons/stock/stock.py
@@@ -27,7 -27,7 +27,7 @@@ from itertools import groupb
  
  from openerp.osv import fields, osv, orm
  from openerp.tools.translate import _
 -from openerp import netsvc
 +from openerp import workflow
  from openerp import tools
  from openerp.tools import float_compare, DEFAULT_SERVER_DATETIME_FORMAT
  import openerp.addons.decimal_precision as dp
@@@ -49,6 -49,7 +49,6 @@@ class stock_incoterms(osv.osv)
          'active': True,
      }
  
 -stock_incoterms()
  
  class stock_journal(osv.osv):
      _name = "stock.journal"
@@@ -61,6 -62,7 +61,6 @@@
          'user_id': lambda s, c, u, ctx: u
      }
  
 -stock_journal()
  
  #----------------------------------------------------------
  # Stock Location
@@@ -159,10 -161,6 +159,10 @@@ class stock_location(osv.osv)
                         \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
                        """, select = True),
           # temporarily removed, as it's unused: 'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True),
 +
 +        # as discussed on bug 765559, the main purpose of this field is to allow sorting the list of locations
 +        # according to the displayed names, and reversing that sort by clicking on a column. It does not work for
 +        # translated values though - so it needs fixing.
          'complete_name': fields.function(_complete_name, type='char', size=256, string="Location Name",
                              store={'stock.location': (_get_sublocations, ['name', 'location_id'], 10)}),
  
                      continue
          return False
  
 -stock_location()
  
  
  class stock_tracking(osv.osv):
          """
          return self.pool.get('action.traceability').action_traceability(cr,uid,ids,context)
  
 -stock_tracking()
  
  #----------------------------------------------------------
  # Stock Picking
@@@ -631,7 -631,7 +631,7 @@@ class stock_picking(osv.osv)
          return res
  
      def create(self, cr, user, vals, context=None):
 -        if ('name' not in vals) or (vals.get('name')=='/'):
 +        if ('name' not in vals) or (vals.get('name')=='/') or (vals.get('name') == False):
              seq_obj_name =  self._name
              vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name)
          new_id = super(stock_picking, self).create(cr, user, vals, context)
          """ Changes state of picking to available if all moves are confirmed.
          @return: True
          """
 -        wf_service = netsvc.LocalService("workflow")
          for pick in self.browse(cr, uid, ids):
              if pick.state == 'draft':
 -                wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_confirm', cr)
 +                self.signal_button_confirm(cr, uid, [pick.id])
              move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
              if not move_ids:
                  raise osv.except_osv(_('Warning!'),_('Not enough stock, unable to reserve the products.'))
          """ Changes state of picking to available if moves are confirmed or waiting.
          @return: True
          """
          for pick in self.browse(cr, uid, ids):
              move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','waiting']]
              self.pool.get('stock.move').force_assign(cr, uid, move_ids)
 -            wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
 +            workflow.trg_write(uid, 'stock.picking', pick.id, cr)
          return True
  
      def draft_force_assign(self, cr, uid, ids, *args):
          """ Confirms picking directly from draft state.
          @return: True
          """
          for pick in self.browse(cr, uid, ids):
              if not pick.move_lines:
                  raise osv.except_osv(_('Error!'),_('You cannot process picking without stock moves.'))
 -            wf_service.trg_validate(uid, 'stock.picking', pick.id,
 -                'button_confirm', cr)
 +            self.signal_button_confirm(cr, uid, [pick.id])
          return True
  
      def draft_validate(self, cr, uid, ids, context=None):
          """ Validates picking directly from draft state.
          @return: True
          """
 -        wf_service = netsvc.LocalService("workflow")
          self.draft_force_assign(cr, uid, ids)
          for pick in self.browse(cr, uid, ids, context=context):
              move_ids = [x.id for x in pick.move_lines]
              self.pool.get('stock.move').force_assign(cr, uid, move_ids)
 -            wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
 +            workflow.trg_write(uid, 'stock.picking', pick.id, cr)
          return self.action_process(
              cr, uid, ids, context=context)
      def cancel_assign(self, cr, uid, ids, *args):
          """ Cancels picking and moves.
          @return: True
          """
          for pick in self.browse(cr, uid, ids):
              move_ids = [x.id for x in pick.move_lines]
              self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
 -            wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
 +            workflow.trg_write(uid, 'stock.picking', pick.id, cr)
          return True
  
      def action_assign_wkf(self, cr, uid, ids, context=None):
          currency_obj = self.pool.get('res.currency')
          uom_obj = self.pool.get('product.uom')
          sequence_obj = self.pool.get('ir.sequence')
 -        wf_service = netsvc.LocalService("workflow")
          for pick in self.browse(cr, uid, ids, context=context):
              new_picking = None
              complete, too_many, too_few = [], [], []
  
              # At first we confirm the new picking (if necessary)
              if new_picking:
 -                wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
 +                self.signal_button_confirm(cr, uid, [new_picking])
                  # Then we finish the good picking
                  self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
                  self.action_move(cr, uid, [new_picking], context=context)
 -                wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr)
 -                wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
 +                self.signal_button_done(cr, uid, [new_picking])
 +                workflow.trg_write(uid, 'stock.picking', pick.id, cr)
                  delivered_pack_id = pick.id
                  back_order_name = self.browse(cr, uid, delivered_pack_id, context=context).name
                  self.message_post(cr, uid, new_picking, body=_("Back order <em>%s</em> has been <b>created</b>.") % (back_order_name), context=context)
              else:
                  self.action_move(cr, uid, [pick.id], context=context)
 -                wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
 +                self.signal_button_done(cr, uid, [pick.id])
                  delivered_pack_id = pick.id
  
              delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
@@@ -1500,6 -1507,7 +1500,6 @@@ class stock_production_lot(osv.osv)
          default.update(date=time.strftime('%Y-%m-%d %H:%M:%S'), move_ids=[])
          return super(stock_production_lot, self).copy(cr, uid, id, default=default, context=context)
  
 -stock_production_lot()
  
  class stock_production_lot_revision(osv.osv):
      _name = 'stock.production.lot.revision'
          'date': fields.date.context_today,
      }
  
 -stock_production_lot_revision()
  
  # ----------------------------------------------------
  # Move
@@@ -2066,6 -2075,7 +2066,6 @@@ class stock_move(osv.osv)
          res_obj = self.pool.get('res.company')
          location_obj = self.pool.get('stock.location')
          move_obj = self.pool.get('stock.move')
 -        wf_service = netsvc.LocalService("workflow")
          new_moves = []
          if context is None:
              context = {}
                      })
                      new_moves.append(self.browse(cr, uid, [new_id])[0])
                  if pickid:
 -                    wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
 +                    self.pool.get('stock.picking').signal_button_confirm(cr, uid, [pickid])
          if new_moves:
              new_moves += self.create_chained_picking(cr, uid, new_moves, context)
          return new_moves
          @return: True
          """
          self.write(cr, uid, ids, {'state': 'assigned'})
 -        wf_service = netsvc.LocalService('workflow')
          for move in self.browse(cr, uid, ids, context):
              if move.picking_id:
 -                wf_service.trg_write(uid, 'stock.picking', move.picking_id.id, cr)
 +                workflow.trg_write(uid, 'stock.picking', move.picking_id.id, cr)
          return True
  
      def cancel_assign(self, cr, uid, ids, context=None):
          # fix for bug lp:707031
          # called write of related picking because changing move availability does
          # not trigger workflow of picking in order to change the state of picking
 -        wf_service = netsvc.LocalService('workflow')
          for move in self.browse(cr, uid, ids, context):
              if move.picking_id:
 -                wf_service.trg_write(uid, 'stock.picking', move.picking_id.id, cr)
 +                workflow.trg_write(uid, 'stock.picking', move.picking_id.id, cr)
          return True
  
      #
  
          if count:
              for pick_id in pickings:
 -                wf_service = netsvc.LocalService("workflow")
 -                wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
 +                workflow.trg_write(uid, 'stock.picking', pick_id, cr)
          return count
  
      def setlast_tracking(self, cr, uid, ids, context=None):
              if move.move_dest_id and move.move_dest_id.state == 'waiting':
                  self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
                  if context.get('call_unlink',False) and move.move_dest_id.picking_id:
 -                    wf_service = netsvc.LocalService("workflow")
 -                    wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
 +                    workflow.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
          self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
          if not context.get('call_unlink',False):
              for pick in self.pool.get('stock.picking').browse(cr, uid, list(pickings), context=context):
                  if all(move.state == 'cancel' for move in pick.move_lines):
                      self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'}, context=context)
  
 -        wf_service = netsvc.LocalService("workflow")
          for id in ids:
 -            wf_service.trg_trigger(uid, 'stock.move', id, cr)
 +            workflow.trg_trigger(uid, 'stock.move', id, cr)
          return True
  
      def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
  
          # if product is set to average price and a specific value was entered in the picking wizard,
          # we use it
-         if move.product_id.cost_method == 'average' and move.price_unit:
+         if move.location_dest_id.usage != 'internal' and move.product_id.cost_method == 'average':
+             reference_amount = qty * move.product_id.standard_price
+         elif move.product_id.cost_method == 'average' and move.price_unit:
              reference_amount = qty * move.price_unit
              reference_currency_id = move.price_currency_id.id or reference_currency_id
  
          """
          picking_ids = []
          move_ids = []
 -        wf_service = netsvc.LocalService("workflow")
          if context is None:
              context = {}
  
                      if move.move_dest_id.state in ('waiting', 'confirmed'):
                          self.force_assign(cr, uid, [move.move_dest_id.id], context=context)
                          if move.move_dest_id.picking_id:
 -                            wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
 +                            workflow.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
                          if move.move_dest_id.auto_validate:
                              self.action_done(cr, uid, [move.move_dest_id.id], context=context)
  
  
          self.write(cr, uid, move_ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
          for id in move_ids:
 -             wf_service.trg_trigger(uid, 'stock.move', id, cr)
 +             workflow.trg_trigger(uid, 'stock.move', id, cr)
  
          for pick_id in picking_ids:
 -            wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
 +            workflow.trg_write(uid, 'stock.picking', pick_id, cr)
  
          return True
  
          product_obj = self.pool.get('product.product')
          currency_obj = self.pool.get('res.currency')
          uom_obj = self.pool.get('product.uom')
 -        wf_service = netsvc.LocalService("workflow")
  
          if context is None:
              context = {}
                  res = cr.fetchall()
                  if len(res) == len(move.picking_id.move_lines):
                      picking_obj.action_move(cr, uid, [move.picking_id.id])
 -                    wf_service.trg_validate(uid, 'stock.picking', move.picking_id.id, 'button_done', cr)
 +                    picking_obj.signal_button_done(cr, uid, [move.picking_id.id])
  
          return [move.id for move in complete]
  
 -stock_move()
  
  class stock_inventory(osv.osv):
      _name = "stock.inventory"
              self.write(cr, uid, [inv.id], {'state': 'cancel'}, context=context)
          return True
  
 -stock_inventory()
  
  class stock_inventory_line(osv.osv):
      _name = "stock.inventory.line"
          result = {'product_qty': amount, 'product_uom': uom, 'prod_lot_id': False}
          return {'value': result}
  
 -stock_inventory_line()
  
  #----------------------------------------------------------
  # Stock Warehouse
@@@ -2982,6 -3004,7 +2984,6 @@@ class stock_warehouse(osv.osv)
          'lot_output_id': _default_lot_output_id,
      }
  
 -stock_warehouse()
  
  #----------------------------------------------------------
  # "Empty" Classes that are used to vary from the original stock.picking  (that are dedicated to the internal pickings)
@@@ -3010,25 -3033,15 +3012,25 @@@ class stock_picking_in(osv.osv)
          #override in order to redirect the check of acces rules on the stock.picking object
          return self.pool.get('stock.picking').check_access_rule(cr, uid, ids, operation, context=context)
  
 -    def _workflow_trigger(self, cr, uid, ids, trigger, context=None):
 -        #override in order to trigger the workflow of stock.picking at the end of create, write and unlink operation
 -        #instead of it's own workflow (which is not existing)
 -        return self.pool.get('stock.picking')._workflow_trigger(cr, uid, ids, trigger, context=context)
 +    def create_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').create_workflow(cr, uid, ids, context=context)
 +
 +    def delete_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').delete_workflow(cr, uid, ids, context=context)
  
 -    def _workflow_signal(self, cr, uid, ids, signal, context=None):
 -        #override in order to fire the workflow signal on given stock.picking workflow instance
 -        #instead of it's own workflow (which is not existing)
 -        return self.pool.get('stock.picking')._workflow_signal(cr, uid, ids, signal, context=context)
 +    def step_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').step_workflow(cr, uid, ids, context=context)
 +
 +    def signal_workflow(self, cr, uid, ids, signal, context=None):
 +        # overridden in order to fire the workflow signal on given stock.picking workflow instance
 +        # instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').signal_workflow(cr, uid, ids, signal, context=context)
  
      def message_post(self, *args, **kwargs):
          """Post the message on stock.picking to be able to see it in the form view when using the chatter"""
@@@ -3093,25 -3106,15 +3095,25 @@@ class stock_picking_out(osv.osv)
          #override in order to redirect the check of acces rules on the stock.picking object
          return self.pool.get('stock.picking').check_access_rule(cr, uid, ids, operation, context=context)
  
 -    def _workflow_trigger(self, cr, uid, ids, trigger, context=None):
 -        #override in order to trigger the workflow of stock.picking at the end of create, write and unlink operation
 -        #instead of it's own workflow (which is not existing)
 -        return self.pool.get('stock.picking')._workflow_trigger(cr, uid, ids, trigger, context=context)
 -
 -    def _workflow_signal(self, cr, uid, ids, signal, context=None):
 -        #override in order to fire the workflow signal on given stock.picking workflow instance
 -        #instead of it's own workflow (which is not existing)
 -        return self.pool.get('stock.picking')._workflow_signal(cr, uid, ids, signal, context=context)
 +    def create_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').create_workflow(cr, uid, ids, context=context)
 +
 +    def delete_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').delete_workflow(cr, uid, ids, context=context)
 +
 +    def step_workflow(self, cr, uid, ids, context=None):
 +        # overridden in order to trigger the workflow of stock.picking at the end of create,
 +        # write and unlink operation instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').step_workflow(cr, uid, ids, context=context)
 +
 +    def signal_workflow(self, cr, uid, ids, signal, context=None):
 +        # overridden in order to fire the workflow signal on given stock.picking workflow instance
 +        # instead of its own workflow (which is not existing)
 +        return self.pool.get('stock.picking').signal_workflow(cr, uid, ids, signal, context=context)
  
      def message_post(self, *args, **kwargs):
          """Post the message on stock.picking to be able to see it in the form view when using the chatter"""
@@@ -50,6 -50,7 +50,6 @@@ class res_partner(osv.osv)
           'invoice_warn' : 'no-message',
      }
  
 -res_partner()
  
  
  class sale_order(osv.osv):
          message = False
          partner = self.pool.get('res.partner').browse(cr, uid, part, context=context)
          if partner.sale_warn != 'no-message':
-             if partner.sale_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.sale_warn_msg)
              title =  _("Warning for %s") % partner.name
              message = partner.sale_warn_msg
              warning = {
                      'title': title,
                      'message': message,
              }
+             if partner.sale_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
  
          result =  super(sale_order, self).onchange_partner_id(cr, uid, ids, part, context=context)
  
@@@ -78,6 -79,7 +78,6 @@@
              warning['message'] = message and message + ' ' + result['warning']['message'] or result['warning']['message']
  
          return {'value': result.get('value',{}), 'warning':warning}
 -sale_order()
  
  
  class purchase_order(osv.osv):
          message = False
          partner = self.pool.get('res.partner').browse(cr, uid, part)
          if partner.purchase_warn != 'no-message':
-             if partner.purchase_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.purchase_warn_msg)
              title = _("Warning for %s") % partner.name
              message = partner.purchase_warn_msg
              warning = {
                  'title': title,
                  'message': message
                  }
+             if partner.purchase_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
          result =  super(purchase_order, self).onchange_partner_id(cr, uid, ids, part)
  
          if result.get('warning',False):
  
          return {'value': result.get('value',{}), 'warning':warning}
  
 -purchase_order()
  
  
  class account_invoice(osv.osv):
          message = False
          partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
          if partner.invoice_warn != 'no-message':
-             if partner.invoice_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.invoice_warn_msg)
              title = _("Warning for %s") % partner.name
              message = partner.invoice_warn_msg
              warning = {
                  'title': title,
                  'message': message
                  }
+             if partner.invoice_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
          result =  super(account_invoice, self).onchange_partner_id(cr, uid, ids, type, partner_id,
              date_invoice=date_invoice, payment_term=payment_term, 
              partner_bank_id=partner_bank_id, company_id=company_id)
  
          return {'value': result.get('value',{}), 'warning':warning}
  
 -account_invoice()
  
  class stock_picking(osv.osv):
      _inherit = 'stock.picking'
          title = False
          message = False
          if partner.picking_warn != 'no-message':
-             if partner.picking_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg)
              title = _("Warning for %s") % partner.name
              message = partner.picking_warn_msg
              warning = {
                  'title': title,
                  'message': message
              }
+             if partner.picking_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
          result =  super(stock_picking, self).onchange_partner_in(cr, uid, ids, partner_id, context)
          if result.get('warning',False):
              warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title']
  
          return {'value': result.get('value',{}), 'warning':warning}
  
 -stock_picking()
  
  # FIXME:(class stock_picking_in and stock_picking_out) this is a temporary workaround because of a framework bug (ref: lp:996816). 
  # It should be removed as soon as the bug is fixed
@@@ -183,14 -191,15 +186,15 @@@ class stock_picking_in(osv.osv)
          title = False
          message = False
          if partner.picking_warn != 'no-message':
-             if partner.picking_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg)
              title = _("Warning for %s") % partner.name
              message = partner.picking_warn_msg
              warning = {
                  'title': title,
                  'message': message
              }
+             if partner.picking_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
          result =  super(stock_picking_in, self).onchange_partner_in(cr, uid, ids, partner_id, context)
          if result.get('warning',False):
              warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title']
@@@ -209,14 -218,15 +213,15 @@@ class stock_picking_out(osv.osv)
          title = False
          message = False
          if partner.picking_warn != 'no-message':
-             if partner.picking_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (partner.name), partner.picking_warn_msg)
              title = _("Warning for %s") % partner.name
              message = partner.picking_warn_msg
              warning = {
                  'title': title,
                  'message': message
              }
+             if partner.picking_warn == 'block':
+                 return {'value': {'partner_id': False}, 'warning': warning}
          result =  super(stock_picking_out, self).onchange_partner_in(cr, uid, ids, partner_id, context)
          if result.get('warning',False):
              warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title']
@@@ -238,6 -248,7 +243,6 @@@ class product_product(osv.osv)
           'purchase_line_warn' : 'no-message',
      }
  
 -product_product()
  
  class sale_order_line(osv.osv):
      _inherit = 'sale.order.line'
          message = False
  
          if product_info.sale_line_warn != 'no-message':
-             if product_info.sale_line_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (product_info.name), product_info.sale_line_warn_msg)
              title = _("Warning for %s") % product_info.name
              message = product_info.sale_line_warn_msg
              warning['title'] = title
              warning['message'] = message
+             if product_info.sale_line_warn == 'block':
+                 return {'value': {'product_id': False}, 'warning': warning}
  
          result =  super(sale_order_line, self).product_id_change( cr, uid, ids, pricelist, product, qty,
              uom, qty_uos, uos, name, partner_id,
  
          return {'value': result.get('value',{}), 'warning':warning}
  
 -sale_order_line()
  
  class purchase_order_line(osv.osv):
      _inherit = 'purchase.order.line'
          message = False
  
          if product_info.purchase_line_warn != 'no-message':
-             if product_info.purchase_line_warn == 'block':
-                 raise osv.except_osv(_('Alert for %s!') % (product_info.name), product_info.purchase_line_warn_msg)
              title = _("Warning for %s") % product_info.name
              message = product_info.purchase_line_warn_msg
              warning['title'] = title
              warning['message'] = message
+             if product_info.purchase_line_warn == 'block':
+                 return {'value': {'product_id': False}, 'warning': warning}
  
          result =  super(purchase_order_line, self).onchange_product_id(cr, uid, ids, pricelist, product, qty, uom,
              partner_id, date_order, fiscal_position_id)
  
          return {'value': result.get('value',{}), 'warning':warning}
  
 -purchase_order_line()
  
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: