[MERGE] forward port of branch 7.0 up to 5035c76
authorDenis Ledoux <dle@odoo.com>
Tue, 4 Nov 2014 11:19:58 +0000 (12:19 +0100)
committerDenis Ledoux <dle@odoo.com>
Tue, 4 Nov 2014 11:19:58 +0000 (12:19 +0100)
1  2 
addons/account/account_invoice.py
addons/base_action_rule/base_action_rule.py
addons/product/product.py
addons/project_issue/project_issue.py
addons/stock/stock.py
addons/web/static/src/js/view_form.js

@@@ -24,10 -24,11 +24,10 @@@ from lxml import etre
  import openerp.addons.decimal_precision as dp
  import openerp.exceptions
  
  from openerp.osv import fields, osv, orm
  from openerp.tools import float_compare
  from openerp.tools.translate import _
 +from openerp import SUPERUSER_ID
  
  class account_invoice(osv.osv):
      def _amount_all(self, cr, uid, ids, name, args, context=None):
  
      def _reconciled(self, cr, uid, ids, name, args, context=None):
          res = {}
          for inv in self.browse(cr, uid, ids, context=context):
              res[inv.id] = self.test_paid(cr, uid, [inv.id])
              if not res[inv.id] and inv.state == 'paid':
 -                wf_service.trg_validate(uid, 'account.invoice', inv.id, 'open_test', cr)
 +                self.signal_open_test(cr, uid, [inv.id])
          return res
  
      def _get_reference_type(self, cr, uid, context=None):
          'type': {
          },
          'state': {
 -            'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'paid' and obj['type'] in ('out_invoice', 'out_refund'),
 -            'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open' and obj['type'] in ('out_invoice', 'out_refund'),
 +            'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'),
 +            'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
          },
      }
      _columns = {
          'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments', groups='base.group_user'),
          'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
          'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, track_visibility='onchange', states={'draft':[('readonly',False)]}),
 -        'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
 +        'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]}),
 +        'commercial_partner_id': fields.related('partner_id', 'commercial_partner_id', string='Commercial Entity', type='many2one',
 +                                                relation='res.partner', store=True, readonly=True,
 +                                                help="The commercial entity that will be used on Journal Entries for this invoice")
      }
      _defaults = {
          'type': _get_type,
              context = {}
  
          if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
 -            partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
 +            partner = self.pool[context['active_model']].read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
              if not view_type:
                  try:
                      view_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'invoice_tree')[1]
              'mark_invoice_as_sent': True,
              })
          return {
 +            'name': _('Compose Email'),
              'type': 'ir.actions.act_window',
              'view_type': 'form',
              'view_mode': 'form',
      def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
          return {'value': {}}
  
 -    def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
 +    def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id, context=None):
          #TODO: add the missing context parameter when forward-porting in trunk so we can remove
          #      this hack!
          context = self.pool['res.users'].context_get(cr, uid)
          obj_journal = self.pool.get('account.journal')
          account_obj = self.pool.get('account.account')
          inv_line_obj = self.pool.get('account.invoice.line')
 +
          if company_id and part_id and type:
              acc_id = False
 -            partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
 +            partner_obj = self.pool.get('res.partner').browse(cr, uid, part_id, context=context)
 +
              if partner_obj.property_account_payable and partner_obj.property_account_receivable:
                  if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
                      property_obj = self.pool.get('ir.property')
                      rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
                      pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
 +
                      if not rec_pro_id:
                          rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
                      if not pay_pro_id:
                          pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
 +
                      rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
                      pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
                      rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
                      pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
 +
                      if not rec_res_id and not pay_res_id:
 -                        raise osv.except_osv(_('Configuration Error!'),
 -                            _('Cannot find a chart of account, you should create one from Settings\Configuration\Accounting menu.'))
 +                        raise self.pool.get('res.config.settings').get_config_warning(cr, _('Cannot find any chart of account: you can create a new one from %(menu:account.menu_account_config)s.'), context=context)
 +
                      if type in ('out_invoice', 'out_refund'):
                          acc_id = rec_res_id
                      else:
                          acc_id = pay_res_id
 +
                      val= {'account_id': acc_id}
              if ids:
                  if company_id:
      # go from canceled state to draft state
      def action_cancel_draft(self, cr, uid, ids, *args):
          self.write(cr, uid, ids, {'state':'draft'})
 -        wf_service = netsvc.LocalService("workflow")
 -        for inv_id in ids:
 -            wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
 -            wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
 +        self.delete_workflow(cr, uid, ids)
 +        self.create_workflow(cr, uid, ids)
          return True
  
 +    # ----------------------------------------
 +    # Mail related methods
 +    # ----------------------------------------
 +
 +    def _get_formview_action(self, cr, uid, id, context=None):
 +        """ Update form view id of action to open the invoice """
 +        action = super(account_invoice, self)._get_formview_action(cr, uid, id, context=context)
 +        obj = self.browse(cr, uid, id, context=context)
 +        if obj.type == 'in_invoice':
 +            model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
 +            action.update({
 +                'views': [(view_id, 'form')],
 +                })
 +        else:
 +            model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
 +            action.update({
 +                'views': [(view_id, 'form')],
 +                })
 +        return action
 +
      # Workflow stuff
      #################
  
              for tax in inv.tax_line:
                  if tax.manual:
                      continue
-                 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id, tax.account_analytic_id.id)
+                 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
                  tax_key.append(key)
                  if not key in compute_taxes:
                      raise osv.except_osv(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
                  'company_id': inv.company_id.id,
              }
              period_id = inv.period_id and inv.period_id.id or False
 -            ctx.update(company_id=inv.company_id.id,
 -                       account_period_prefer_normal=True)
 +            ctx.update(company_id=inv.company_id.id)
              if not period_id:
                  period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
                  period_id = period_ids and period_ids[0] or False
@@@ -1519,7 -1494,6 +1519,7 @@@ class account_invoice_line(osv.osv)
          result = {}
          res = self.pool.get('product.product').browse(cr, uid, product, context=context)
  
 +        result['name'] = res.partner_ref
          if type in ('out_invoice','out_refund'):
              a = res.property_account_income.id
              if not a:
  
          if type in ('out_invoice', 'out_refund'):
              taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
 +            if res.description_sale:
 +                result['name'] += '\n'+res.description_sale
          else:
              taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
 +            if res.description_purchase:
 +                result['name'] += '\n'+res.description_purchase
 +
          tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
  
          if type in ('in_invoice', 'in_refund'):
              result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
          else:
              result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
 -        result['name'] = res.partner_ref
  
          result['uos_id'] = uom_id or res.uom_id.id
 -        if res.description:
 -            result['name'] += '\n'+res.description
  
          domain = {'uos_id':[('category_id','=',res.uom_id.category_id.id)]}
  
                  unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
          return {'value':{'invoice_line_tax_id': unique_tax_ids}}
  
 -account_invoice_line()
  
  class account_invoice_tax(osv.osv):
      _name = "account.invoice.tax"
                      val['account_id'] = tax['account_paid_id'] or line.account_id.id
                      val['account_analytic_id'] = tax['account_analytic_paid_id']
  
-                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
+                 # If the taxes generate moves on the same financial account as the invoice line
+                 # and no default analytic account is defined at the tax level, propagate the
+                 # analytic account from the invoice line to the tax line. This is necessary
+                 # in situations were (part of) the taxes cannot be reclaimed,
+                 # to ensure the tax move is allocated to the proper analytic account.
+                 if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id:
+                     val['account_analytic_id'] = line.account_analytic_id.id
+                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
                  if not key in tax_grouped:
                      tax_grouped[key] = val
                  else:
@@@ -1819,7 -1800,13 +1827,7 @@@ class res_partner(osv.osv)
          '''
          Find the partner for which the accounting entries will be created
          '''
 -        # FIXME: after 7.0, to replace by function field partner.commercial_partner_id
 -
 -        #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
 -        #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
 -        while not partner.is_company and partner.parent_id:
 -            partner = partner.parent_id
 -        return partner
 +        return partner.commercial_partner_id
  
      def copy(self, cr, uid, id, default=None, context=None):
          default = default or {}
@@@ -1835,7 -1822,6 +1843,7 @@@ class mail_compose_message(osv.Model)
          if context.get('default_model') == 'account.invoice' and context.get('default_res_id') and context.get('mark_invoice_as_sent'):
              context = dict(context, mail_post_autofollow=True)
              self.pool.get('account.invoice').write(cr, uid, [context['default_res_id']], {'sent': True}, context=context)
 +            self.pool.get('account.invoice').message_post(cr, uid, [context['default_res_id']], body=_("Invoice sent"), context=context)
          return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -51,7 -51,6 +51,7 @@@ class base_action_rule(osv.osv)
  
      _name = 'base.action.rule'
      _description = 'Action Rules'
 +    _order = 'sequence'
  
      _columns = {
          'name':  fields.char('Rule Name', size=64, required=True),
              help="When unchecked, the rule is hidden and will not be executed."),
          'sequence': fields.integer('Sequence',
              help="Gives the sequence order when displaying a list of rules."),
 +        'kind': fields.selection(
 +            [('on_create', 'On Creation'),
 +             ('on_write', 'On Update'),
 +             ('on_create_or_write', 'On Creation & Update'),
 +             ('on_time', 'Based on Timed Condition')],
 +            string='When to Run'),
          'trg_date_id': fields.many2one('ir.model.fields', string='Trigger Date',
              help="When should the condition be triggered. If present, will be checked by the scheduler. If empty, will be checked at creation and update.",
              domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"),
              "trigger date, like sending a reminder 15 minutes before a meeting."),
          'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'),
                                  ('day', 'Days'), ('month', 'Months')], 'Delay type'),
 +        'trg_date_calendar_id': fields.many2one(
 +            'resource.calendar', 'Use Calendar',
 +            help='When calculating a day-based timed condition, it is possible to use a calendar to compute the date based on working days.',
 +            ondelete='set null',
 +        ),
          'act_user_id': fields.many2one('res.users', 'Set Responsible'),
          'act_followers': fields.many2many("res.partner", string="Add Followers"),
          'server_action_ids': fields.many2many('ir.actions.server', string='Server Actions',
              ondelete='restrict',
              domain="[('model_id', '=', model_id.model)]",
              help="If present, this condition must be satisfied before the update of the record."),
 -        'filter_id': fields.many2one('ir.filters', string='After Update Filter',
 +        'filter_id': fields.many2one('ir.filters', string='Filter',
              ondelete='restrict',
              domain="[('model_id', '=', model_id.model)]",
 -            help="If present, this condition must be satisfied after the update of the record."),
 +            help="If present, this condition must be satisfied before executing the action rule."),
          'last_run': fields.datetime('Last Run', readonly=1),
      }
  
          'trg_date_range_type': 'day',
      }
  
 -    _order = 'sequence'
 +    def onchange_kind(self, cr, uid, ids, kind, context=None):
 +        clear_fields = []
 +        if kind in ['on_create', 'on_create_or_write']:
 +            clear_fields = ['filter_pre_id', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
 +        elif kind in ['on_write', 'on_create_or_write']:
 +            clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type']
 +        elif kind == 'on_time':
 +            clear_fields = ['filter_pre_id']
 +        return {'value': dict.fromkeys(clear_fields, False)}
  
      def _filter(self, cr, uid, action, action_filter, record_ids, context=None):
          """ filter the list record_ids that satisfy the action filter """
          if record_ids and action_filter:
              assert action.model == action_filter.model_id, "Filter model different from action rule model"
 -            model = self.pool.get(action_filter.model_id)
 +            model = self.pool[action_filter.model_id]
              domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
              ctx = dict(context or {})
              ctx.update(eval(action_filter.context))
  
      def _process(self, cr, uid, action, record_ids, context=None):
          """ process the given action on the records """
 -        # execute server actions
 -        model = self.pool.get(action.model_id.model)
 -        if action.server_action_ids:
 -            server_action_ids = map(int, action.server_action_ids)
 -            for record in model.browse(cr, uid, record_ids, context):
 -                action_server_obj = self.pool.get('ir.actions.server')
 -                ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id)
 -                action_server_obj.run(cr, uid, server_action_ids, context=ctx)
 +        model = self.pool[action.model_id.model]
  
          # modify records
          values = {}
              follower_ids = map(int, action.act_followers)
              model.message_subscribe(cr, uid, record_ids, follower_ids, context=context)
  
 +        # execute server actions
 +        if action.server_action_ids:
 +            server_action_ids = map(int, action.server_action_ids)
 +            for record in model.browse(cr, uid, record_ids, context):
 +                action_server_obj = self.pool.get('ir.actions.server')
 +                ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id)
 +                action_server_obj.run(cr, uid, server_action_ids, context=ctx)
 +
          return True
  
      def _wrap_create(self, old_create, model):
          """ Return a wrapper around `old_create` calling both `old_create` and
              `_process`, in that order.
          """
 -        def wrapper(cr, uid, vals, context=None, **kwargs):
 +        def create(cr, uid, vals, context=None, **kwargs):
              # avoid loops or cascading actions
              if context and context.get('action'):
                  return old_create(cr, uid, vals, context=context)
              context = dict(context or {}, action=True)
              new_id = old_create(cr, uid, vals, context=context, **kwargs)
  
 -            # as it is a new record, we do not consider the actions that have a prefilter
 -            action_dom = [('model', '=', model), ('trg_date_id', '=', False), ('filter_pre_id', '=', False)]
 +            # retrieve the action rules to run on creation
 +            action_dom = [('model', '=', model), ('kind', 'in', ['on_create', 'on_create_or_write'])]
              action_ids = self.search(cr, uid, action_dom, context=context)
  
              # check postconditions, and execute actions on the records that satisfy them
                      self._process(cr, uid, action, [new_id], context=context)
              return new_id
  
 -        return wrapper
 +        return create
  
      def _wrap_write(self, old_write, model):
          """ Return a wrapper around `old_write` calling both `old_write` and
              `_process`, in that order.
          """
 -        def wrapper(cr, uid, ids, vals, context=None, **kwargs):
 +        def write(cr, uid, ids, vals, context=None, **kwargs):
              # avoid loops or cascading actions
              if context and context.get('action'):
                  return old_write(cr, uid, ids, vals, context=context, **kwargs)
              context = dict(context or {}, action=True)
              ids = [ids] if isinstance(ids, (int, long, str)) else ids
  
 -            # retrieve the action rules to possibly execute
 -            action_dom = [('model', '=', model), ('trg_date_id', '=', False)]
 +            # retrieve the action rules to run on update
 +            action_dom = [('model', '=', model), ('kind', 'in', ['on_write', 'on_create_or_write'])]
              action_ids = self.search(cr, uid, action_dom, context=context)
              actions = self.browse(cr, uid, action_ids, context=context)
  
                      self._process(cr, uid, action, post_ids, context=context)
              return True
  
 -        return wrapper
 +        return write
  
      def _register_hook(self, cr, ids=None):
          """ Wrap the methods `create` and `write` of the models specified by
              ids = self.search(cr, SUPERUSER_ID, [])
          for action_rule in self.browse(cr, SUPERUSER_ID, ids):
              model = action_rule.model_id.model
-             model_obj = self.pool[model]
-             if not hasattr(model_obj, 'base_action_ruled'):
+             model_obj = self.pool.get(model)
+             if model_obj and not hasattr(model_obj, 'base_action_ruled'):
                  model_obj.create = self._wrap_create(model_obj.create, model)
                  model_obj.write = self._wrap_write(model_obj.write, model)
                  model_obj.base_action_ruled = True
                  updated = True
          return updated
  
 +    def _update_cron(self, cr, uid, context=None):
 +        try:
 +            cron = self.pool['ir.model.data'].get_object(
 +                cr, uid, 'base_action_rule', 'ir_cron_crm_action', context=context)
 +        except ValueError:
 +            return False
 +
 +        return cron.toggle(model=self._name, domain=[('kind', '=', 'on_time')])
 +
      def create(self, cr, uid, vals, context=None):
          res_id = super(base_action_rule, self).create(cr, uid, vals, context=context)
          if self._register_hook(cr, [res_id]):
              openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
 +        self._update_cron(cr, uid, context=context)
          return res_id
  
      def write(self, cr, uid, ids, vals, context=None):
          super(base_action_rule, self).write(cr, uid, ids, vals, context=context)
          if self._register_hook(cr, ids):
              openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
 +        self._update_cron(cr, uid, context=context)
          return True
  
 +    def unlink(self, cr, uid, ids, context=None):
 +        res = super(base_action_rule, self).unlink(cr, uid, ids, context=context)
 +        self._update_cron(cr, uid, context=context)
 +        return res
 +
      def onchange_model_id(self, cr, uid, ids, model_id, context=None):
          data = {'model': False, 'filter_pre_id': False, 'filter_id': False}
          if model_id:
              data.update({'model': model.model})
          return {'value': data}
  
 +    def _check_delay(self, cr, uid, action, record, record_dt, context=None):
 +        if action.trg_date_calendar_id and action.trg_date_range_type == 'day':
 +            start_dt = get_datetime(record_dt)
 +            action_dt = self.pool['resource.calendar'].schedule_days_get_date(
 +                cr, uid, action.trg_date_calendar_id.id, action.trg_date_range,
 +                day_date=start_dt, compute_leaves=True, context=context
 +            )
 +        else:
 +            delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range)
 +            action_dt = get_datetime(record_dt) + delay
 +        return action_dt
 +
      def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
          """ This Function is called by scheduler. """
          context = context or {}
 -        # retrieve all the action rules that have a trg_date_id and no precondition
 -        action_dom = [('trg_date_id', '!=', False), ('filter_pre_id', '=', False)]
 +        # retrieve all the action rules to run based on a timed condition
 +        action_dom = [('kind', '=', 'on_time')]
          action_ids = self.search(cr, uid, action_dom, context=context)
          for action in self.browse(cr, uid, action_ids, context=context):
              now = datetime.now()
                  last_run = datetime.utcfromtimestamp(0)
  
              # retrieve all the records that satisfy the action's condition
 -            model = self.pool.get(action.model_id.model)
 +            model = self.pool[action.model_id.model]
              domain = []
              ctx = dict(context)
              if action.filter_id:
              else:
                  get_record_dt = lambda record: record[date_field]
  
 -            delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range)
 -
              # process action on the records that should be executed
              for record in model.browse(cr, uid, record_ids, context=context):
                  record_dt = get_record_dt(record)
                  if not record_dt:
                      continue
 -                action_dt = get_datetime(record_dt) + delay
 +                action_dt = self._check_delay(cr, uid, action, record, record_dt, context=context)
                  if last_run <= action_dt < now:
                      try:
                          context = dict(context or {}, action=True)
@@@ -24,9 -24,8 +24,9 @@@ import r
  
  from _common import ceiling
  
 -from openerp import tools, SUPERUSER_ID
 -from openerp.osv import osv, fields, expression
 +from openerp import SUPERUSER_ID
 +from openerp import tools
 +from openerp.osv import osv, orm, fields, expression
  from openerp.tools.translate import _
  
  import openerp.addons.decimal_precision as dp
@@@ -34,7 -33,7 +34,7 @@@ from openerp.tools.float_utils import f
  
  def ean_checksum(eancode):
      """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
 -    if len(eancode) <> 13:
 +    if len(eancode) != 13:
          return -1
      oddsum=0
      evensum=0
@@@ -57,7 -56,7 +57,7 @@@ def check_ean(eancode)
      """returns True if eancode is a valid ean13 string, or null"""
      if not eancode:
          return True
 -    if len(eancode) <> 13:
 +    if len(eancode) != 13:
          return False
      try:
          int(eancode)
@@@ -84,8 -83,9 +84,8 @@@ class product_uom_categ(osv.osv)
      _name = 'product.uom.categ'
      _description = 'Product uom categ'
      _columns = {
 -        'name': fields.char('Name', size=64, required=True, translate=True),
 +        'name': fields.char('Name', required=True, translate=True),
      }
 -product_uom_categ()
  
  class product_uom(osv.osv):
      _name = 'product.uom'
  
      def create(self, cr, uid, data, context=None):
          if 'factor_inv' in data:
 -            if data['factor_inv'] <> 1:
 +            if data['factor_inv'] != 1:
                  data['factor'] = self._compute_factor_inv(data['factor_inv'])
              del(data['factor_inv'])
          return super(product_uom, self).create(cr, uid, data, context)
  
      _order = "name"
      _columns = {
 -        'name': fields.char('Unit of Measure', size=64, required=True, translate=True),
 +        'name': fields.char('Unit of Measure', required=True, translate=True),
          'category_id': fields.many2one('product.uom.categ', 'Category', required=True, ondelete='cascade',
              help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
          'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision
                      '1 * (reference unit) = ratio * (this unit)'),
          'factor_inv': fields.function(_factor_inv, digits=0, # force NUMERIC with unlimited precision
              fnct_inv=_factor_inv_write,
 -            string='Ratio',
 +            string='Bigger Ratio',
              help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
                      '1 * (this unit) = ratio * (reference unit)', required=True),
          'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
      def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
          if context is None:
              context = {}
 -        if from_unit.category_id.id <> to_unit.category_id.id:
 +        if from_unit.category_id.id != to_unit.category_id.id:
              if context.get('raise-exception', True):
                  raise osv.except_osv(_('Error!'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
              else:
              from_unit, to_unit = uoms[0], uoms[-1]
          else:
              from_unit, to_unit = uoms[-1], uoms[0]
 -        if from_unit.category_id.id <> to_unit.category_id.id:
 +        if from_unit.category_id.id != to_unit.category_id.id:
              return price
          amount = price * from_unit.factor
          if to_uom_id:
                      raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
          return super(product_uom, self).write(cr, uid, ids, vals, context=context)
  
 -product_uom()
  
  
  class product_ul(osv.osv):
      _name = "product.ul"
      _description = "Shipping Unit"
      _columns = {
 -        'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
 +        'name' : fields.char('Name', select=True, required=True, translate=True),
          'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
      }
 -product_ul()
  
  
  #----------------------------------------------------------
@@@ -249,7 -251,7 +249,7 @@@ class product_category(osv.osv)
      _name = "product.category"
      _description = "Product Category"
      _columns = {
 -        'name': fields.char('Name', size=64, required=True, translate=True, select=True),
 +        'name': fields.char('Name', required=True, translate=True, select=True),
          'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
          'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
          'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
      _parent_order = 'sequence, name'
      _order = 'parent_left'
  
 -    def _check_recursion(self, cr, uid, ids, context=None):
 -        level = 100
 -        while len(ids):
 -            cr.execute('select distinct parent_id from product_category 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 categories.', ['parent_id'])
 +        (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
      ]
      def child_get(self, cr, uid, ids):
          return [ids]
  
 -product_category()
 +
 +class product_public_category(osv.osv):
 +    _name = "product.public.category"
 +    _description = "Public Category"
 +    _order = "sequence, name"
 +
 +    _constraints = [
 +        (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
 +    ]
 +
 +    def name_get(self, cr, uid, ids, context=None):
 +        if not len(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 _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
 +        res = self.name_get(cr, uid, ids, context=context)
 +        return dict(res)
 +
 +    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 = {
 +        'name': fields.char('Name', required=True, translate=True),
 +        'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
 +        'parent_id': fields.many2one('product.public.category','Parent Category', select=True),
 +        'child_id': fields.one2many('product.public.category', 'parent_id', string='Children Categories'),
 +        'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
 +        
 +        # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
 +        # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
 +        # In this case, the default image is set by the js code.
 +        # NOTE2: image: all image fields are base64 encoded and PIL-supported
 +        'image': fields.binary("Image",
 +            help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
 +        'image_medium': fields.function(_get_image, fnct_inv=_set_image,
 +            string="Medium-sized image", type="binary", multi="_get_image",
 +            store={
 +                'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 +            },
 +            help="Medium-sized image of the category. It is automatically "\
 +                 "resized as a 128x128px image, with aspect ratio preserved. "\
 +                 "Use this field in form views or some kanban views."),
 +        'image_small': fields.function(_get_image, fnct_inv=_set_image,
 +            string="Smal-sized image", type="binary", multi="_get_image",
 +            store={
 +                'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 +            },
 +            help="Small-sized image of the category. It is automatically "\
 +                 "resized as a 64x64px image, with aspect ratio preserved. "\
 +                 "Use this field anywhere a small image is required."),
 +    }
  
  
  #----------------------------------------------------------
  #----------------------------------------------------------
  class product_template(osv.osv):
      _name = "product.template"
 +    _inherit = ['mail.thread']
      _description = "Product Template"
  
 +    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, avoid_resize_medium=True)
 +        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 = {
 -        'name': fields.char('Name', size=128, required=True, translate=True, select=True),
 +        'name': fields.char('Name', required=True, translate=True, select=True),
          'product_manager': fields.many2one('res.users','Product Manager'),
 -        'description': fields.text('Description',translate=True),
 -        'description_purchase': fields.text('Purchase Description',translate=True),
 -        'description_sale': fields.text('Sale Description',translate=True),
 +        'description': fields.text('Description',translate=True,
 +            help="A precise description of the Product, used only for internal information purposes."),
 +        'description_purchase': fields.text('Purchase Description',translate=True,
 +            help="A description of the Product that you want to communicate to your suppliers. "
 +                 "This description will be copied to every Purchase Order, Reception and Supplier Invoice/Refund."),
 +        'description_sale': fields.text('Sale Description',translate=True,
 +            help="A description of the Product that you want to communicate to your customers. "
 +                 "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
          'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Consumable are product where you don't manage stock, a service is a non-material product provided by a company or an individual."),
          'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."),
          'rental': fields.boolean('Can be Rent'),
          'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
 +        'public_categ_id': fields.many2one('product.public.category','Public Category', help="Those categories are used to group similar products for public sales (eg.: point of sale, e-commerce)."),
          'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."),
 -        'standard_price': fields.float('Cost', digits_compute=dp.get_precision('Product Price'), help="Cost price of the product used for standard stock valuation in accounting and used as a base price on purchase orders.", groups="base.group_user"),
 +        'standard_price': fields.float('Cost Price', digits_compute=dp.get_precision('Product Price'), help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", groups="base.group_user"),
          'volume': fields.float('Volume', help="The volume in m3."),
          'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
          'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
          'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
          'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
          'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
 -            help='Sepcify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
 +            help='Specify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
          'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
              help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
              ' uos = uom * coeff'),
          'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
 -        'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Supplier'),
 +        'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
          'company_id': fields.many2one('res.company', 'Company', select=1),
 +        # image: all image fields are base64 encoded and PIL-supported
 +        'image': fields.binary("Image",
 +            help="This field holds the image used as image for the product, limited to 1024x1024px."),
 +        'image_medium': fields.function(_get_image, fnct_inv=_set_image,
 +            string="Medium-sized image", type="binary", multi="_get_image",
 +            store={
 +                'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 +            },
 +            help="Medium-sized image of the product. It is automatically "\
 +                 "resized as a 128x128px image, with aspect ratio preserved, "\
 +                 "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
 +        'image_small': fields.function(_get_image, fnct_inv=_set_image,
 +            string="Small-sized image", type="binary", multi="_get_image",
 +            store={
 +                'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 +            },
 +            help="Small-sized image of the product. It is automatically "\
 +                 "resized as a 64x64px image, with aspect ratio preserved. "\
 +                 "Use this field anywhere a small image is required."),
 +        'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Product Variants', required=True),
      }
  
      def _get_uom_id(self, cr, uid, *args):
                      raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may deactivate this product from the 'Procurements' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
          return super(product_template, self).write(cr, uid, ids, vals, context=context)
  
 +    def copy(self, cr, uid, id, default=None, context=None):
 +        if default is None:
 +            default = {}
 +        template = self.browse(cr, uid, id, context=context)
 +        default['name'] = _("%s (copy)") % (template['name'])
 +        return super(product_template, self).copy(cr, uid, id, default=default, context=context)
 +
      _defaults = {
          'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
          'list_price': 1,
  
      def _check_uom(self, cursor, user, ids, context=None):
          for product in self.browse(cursor, user, ids, context=context):
 -            if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
 +            if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
                  return False
          return True
  
              pass
          return super(product_template, self).name_get(cr, user, ids, context)
  
 -product_template()
  
  class product_product(osv.osv):
 +    _name = "product.product"
 +    _description = "Product"
 +    _inherits = {'product.template': 'product_tmpl_id'}
 +    _inherit = ['mail.thread']
 +    _order = 'default_code,name_template'
 +
      def view_header_get(self, cr, uid, view_id, view_type, context=None):
          if context is None:
              context = {}
          return res
  
      def _product_price(self, cr, uid, ids, name, arg, context=None):
 +        plobj = self.pool.get('product.pricelist')
          res = {}
          if context is None:
              context = {}
          if pricelist:
              # Support context pricelists specified as display_name or ID for compatibility
              if isinstance(pricelist, basestring):
 -                pricelist_ids = self.pool.get('product.pricelist').name_search(
 +                pricelist_ids = plobj.name_search(
                      cr, uid, pricelist, operator='=', context=context, limit=1)
                  pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
 -            for id in ids:
 -                try:
 -                    price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
 -                except:
 -                    price = 0.0
 -                res[id] = price
 +
 +            if isinstance(pricelist, (int, long)):
 +                products = self.browse(cr, uid, ids, context=context)
 +                qtys = map(lambda x: (x, quantity, partner), products)
 +                pl = plobj.browse(cr, uid, pricelist, context=context)
 +                price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
 +                for id in ids:
 +                    res[id] = price.get(id, 0.0)
          for id in ids:
              res.setdefault(id, 0.0)
          return res
              res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
          return res
  
 +    def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):
 +        field_value = field_value or 0.0
 +        product = self.browse(cr, uid, product_id, context=context)
 +        list_price = (field_value - product.price_extra) / (product.price_margin or 1.0)
 +        return self.write(cr, uid, [product_id], {'list_price': list_price}, context=context)
 +
 +
      def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
          for supinfo in product.seller_ids:
              if supinfo.name.id == partner_id:
                      (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
          return res
  
 +    def _is_only_child(self, cr, uid, ids, name, arg, context=None):
 +        res = dict.fromkeys(ids, True)
 +        for product in self.browse(cr, uid, ids, context=context):
 +            if product.product_tmpl_id and len(product.product_tmpl_id.product_variant_ids) > 1:
 +                res[product.id] = False
 +        return res
  
      def _get_main_product_supplier(self, cr, uid, product, context=None):
          """Determines the main (best) product supplier for ``product``,
              }
          return result
  
 +    def _get_name_template_ids(self, cr, uid, ids, context=None):
 +        result = set()
 +        template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
 +        for el in template_ids:
 +            result.add(el)
 +        return list(result)
  
 -    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, avoid_resize_medium=True)
 -        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)
 -
 -    _defaults = {
 -        'active': lambda *a: 1,
 -        'price_extra': lambda *a: 0.0,
 -        'price_margin': lambda *a: 1.0,
 -        'color': 0,
 -    }
 -
 -    _name = "product.product"
 -    _description = "Product"
 -    _table = "product_product"
 -    _inherits = {'product.template': 'product_tmpl_id'}
 -    _inherit = ['mail.thread']
 -    _order = 'default_code,name_template'
      _columns = {
          'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
          'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
          'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
          'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
 -        'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
 -        'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
 +        'price': fields.function(_product_price, fnct_inv=_save_product_lst_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
 +        'lst_price' : fields.function(_product_lst_price, fnct_inv=_save_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
          'code': fields.function(_product_code, type='char', string='Internal Reference'),
          'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
 -        'default_code' : fields.char('Internal Reference', size=64, select=True),
 +        'default_code' : fields.char('Internal Reference', select=True),
          'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
 -        'variants': fields.char('Variants', size=64),
 +        'variants': fields.char('Variants', translate=True),
          'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
 +        'is_only_child': fields.function(
 +            _is_only_child, type='boolean', string='Sole child of the parent template'),
          'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
          'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
 -        'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price')),
 -        'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price')),
 +        'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
 +        'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price'), help="Price Margin: Margin in percentage amount on sale price for the variant. eg. 10 price margin, 1000 * 1.1 = 1100."),
          'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
 -        'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', size=128, store=True, select=True),
 +        'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
 +            'product.template': (_get_name_template_ids, ['name'], 10),
 +            'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
 +        }, select=True),
          'color': fields.integer('Color Index'),
 -        # image: all image fields are base64 encoded and PIL-supported
 -        'image': fields.binary("Image",
 -            help="This field holds the image used as image for the product, limited to 1024x1024px."),
 -        'image_medium': fields.function(_get_image, fnct_inv=_set_image,
 -            string="Medium-sized image", type="binary", multi="_get_image",
 -            store={
 -                'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 -            },
 -            help="Medium-sized image of the product. It is automatically "\
 -                 "resized as a 128x128px image, with aspect ratio preserved, "\
 -                 "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
 -        'image_small': fields.function(_get_image, fnct_inv=_set_image,
 -            string="Small-sized image", type="binary", multi="_get_image",
 -            store={
 -                'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
 -            },
 -            help="Small-sized image of the product. It is automatically "\
 -                 "resized as a 64x64px image, with aspect ratio preserved. "\
 -                 "Use this field anywhere a small image is required."),
          'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
          'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
          'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
          'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_info"),
      }
 +
 +    _defaults = {
 +        'active': lambda *a: 1,
 +        'price_extra': lambda *a: 0.0,
 +        'price_margin': lambda *a: 1.0,
 +        'color': 0,
 +        'is_only_child': True,
 +    }
 +
      def unlink(self, cr, uid, ids, context=None):
          unlink_ids = []
          unlink_product_tmpl_ids = []
              res = check_ean(product['ean13'])
          return res
  
 -
      _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
  
      def on_order(self, cr, uid, ids, orderline, quantity):
          else:
              partner_ids = []
  
 +        # all user don't have access to seller and partner
 +        # check access and use superuser
 +        self.check_access_rights(cr, user, "read")
 +        self.check_access_rule(cr, user, ids, "read", context=context)
 +
          result = []
 -        for product in self.browse(cr, user, ids, context=context):
 +        for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
              sellers = partner_ids and filter(lambda x: x.name.id in partner_ids, product.seller_ids) or []
              if sellers:
                  for s in sellers:
                  # on a database with thousands of matching products, due to the huge merge+unique needed for the
                  # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                  # Performing a quick memory merge of ids in Python will give much better performance
 -                ids = set()
 -                ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
 +                ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
                  if not limit or len(ids) < limit:
                      # we may underrun the limit because of dupes in the results, that's fine
 -                    ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit and (limit-len(ids)) or False) , context=context))
 +                    limit2 = (limit - len(ids)) if limit else False
 +                    ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
                  ids = list(ids)
              elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
                  ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
      # Could be overrided for variants matrices prices
      #
      def price_get(self, cr, uid, ids, ptype='list_price', context=None):
 +        products = self.browse(cr, uid, ids, context=context)
 +        return self._price_get(cr, uid, products, ptype=ptype, context=context)
 +
 +    def _price_get(self, cr, uid, products, ptype='list_price', context=None):
          if context is None:
              context = {}
  
              price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
  
          res = {}
 -        product_uom_obj = self.pool.get('product.uom')
+         company_id = self.pool['res.users'].read(cr, uid, uid, ['company_id'], context=context)['company_id'][0]
 -        for product in self.browse(cr, SUPERUSER_ID, ids, context=dict(context, force_company=company_id)):
 -            res[product.id] = product[ptype] or 0.0
 +        # standard_price field can only be seen by users in base.group_user
 +        # Thus, in order to compute the sale price from the cost price for users not in this group
 +        # We fetch the standard price as the superuser
 +        for product in products:
 +            if ptype != 'standard_price':
 +                res[product.id] = product[ptype] or 0.0
 +            else: 
-                 res[product.id] = self.read(cr, SUPERUSER_ID, product.id, [ptype], context=context)[ptype] or 0.0
++                res[product.id] = self.read(cr, SUPERUSER_ID, product.id, [ptype], context=dict(context, force_company=company_id))[ptype] or 0.0
 +
 +        product_uom_obj = self.pool.get('product.uom')
 +        for product in products:
              if ptype == 'list_price':
                  res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
                          product.price_extra
          if context is None:
              context={}
  
 -        if not default:
 -            default = {}
 +        product = self.browse(cr, uid, id, context)
 +        if context.get('variant'):
 +            # if we copy a variant or create one, we keep the same template
 +            default['product_tmpl_id'] = product.product_tmpl_id.id
 +        elif 'name' not in default:
 +            default['name'] = _("%s (copy)") % (product.name,)
  
 -        product = self.read(cr, uid, id, ['name'], context=context)
 -        default = default.copy()
 -        default.update(name=_("%s (copy)") % (product['name']))
 -
 -        if context.get('variant',False):
 -            fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
 -                    'price_margin', 'price_extra']
 -            data = self.read(cr, uid, id, fields=fields, context=context)
 -            for f in fields:
 -                if f in default:
 -                    data[f] = default[f]
 -            data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
 -                    and data['product_tmpl_id'][0]
 -            del data['id']
 -            return self.create(cr, uid, data)
 -        else:
 -            return super(product_product, self).copy(cr, uid, id, default=default,
 -                    context=context)
 +        return super(product_product, self).copy(cr, uid, id, default=default, context=context)
  
      def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
          if context is None:
              context = {}
 -        if context and context.get('search_default_categ_id', False):
 +        if context.get('search_default_categ_id'):
              args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
          return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
  
 +    def open_product_template(self, cr, uid, ids, context=None):
 +        """ Utility method used to add an "Open Template" button in product views """
 +        product = self.browse(cr, uid, ids[0], context=context)
 +        return {'type': 'ir.actions.act_window',
 +                'res_model': 'product.template',
 +                'view_mode': 'form',
 +                'res_id': product.product_tmpl_id.id,
 +                'target': 'new'}
 +
      def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
          '''
          Computes product's invoicing quantity in UoS from quantity in UoM.
              return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
  
  
 -product_product()
  
  class product_packaging(osv.osv):
      _name = "product.packaging"
      _order = 'sequence'
      _columns = {
          'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
 -        'name' : fields.text('Description', size=64),
 +        'name' : fields.text('Description'),
          'qty' : fields.float('Quantity by Package',
              help="The total number of products you can put by pallet or box."),
          'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
          'rows' : fields.integer('Number of Layers', required=True,
              help='The number of layers on a pallet or box'),
          'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
 -        'ean' : fields.char('EAN', size=14,
 -            help="The EAN code of the package unit."),
 -        'code' : fields.char('Code', size=14,
 -            help="The code of the transport unit."),
 +        'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
 +        'code' : fields.char('Code', help="The code of the transport unit."),
          'weight': fields.float('Total Package Weight',
              help='The weight of a full package, pallet or box.'),
          'weight_ul': fields.float('Empty Package Weight'),
          return (10 - (sum % 10)) % 10
      checksum = staticmethod(checksum)
  
 -product_packaging()
  
  
  class product_supplierinfo(osv.osv):
      _description = "Information about a product supplier"
      def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
          result = {}
 -        product_uom_pool = self.pool.get('product.uom')
          for supplier_info in self.browse(cr, uid, ids, context=context):
              for field in fields:
                  result[supplier_info.id] = {field:False}
  
      _columns = {
          'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
 -        'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
 -        'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
 +        'product_name': fields.char('Supplier Product Name', help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
 +        'product_code': fields.char('Supplier Product Code', help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
          'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
 -        'product_uom': fields.related('product_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
 +        'product_uom': fields.related('product_tmpl_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
          'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product Unit of Measure if not empty, in the default unit of measure of the product otherwise."),
          'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Unit of Measure."),
 -        'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
 +        'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
          'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the reception of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
          'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
          'company_id':fields.many2one('res.company','Company',select=1),
          pricelist_pool = self.pool.get('product.pricelist')
          currency_pool = self.pool.get('res.currency')
          currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
 +        # Compute price from standard price of product
 +        product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
 +        product = product_pool.browse(cr, uid, product_id, context=context)
          for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
 -            # Compute price from standard price of product
 -            price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
 -
 +            price = product_price
              # Compute price from Purchase pricelist of supplier
              pricelist_id = supplier.property_product_pricelist_purchase.id
              if pricelist_id:
                  price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
  
              # Compute price from supplier pricelist which are in Supplier Information
 -            supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
 +            supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
              if supplier_info_ids:
                  cr.execute('SELECT * ' \
                      'FROM pricelist_partnerinfo ' \
              res[supplier.id] = price
          return res
      _order = 'sequence'
 -product_supplierinfo()
  
  
  class pricelist_partnerinfo(osv.osv):
      _name = 'pricelist.partnerinfo'
      _columns = {
 -        'name': fields.char('Description', size=64),
 +        'name': fields.char('Description'),
          'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
          'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier Unit of Measure if any or in the default Unit of Measure of the product otherrwise."),
          'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), help="This price will be considered as a price for the supplier Unit of Measure if any or the default Unit of Measure of the product otherwise"),
      }
      _order = 'min_quantity asc'
 -pricelist_partnerinfo()
  
  class res_currency(osv.osv):
      _inherit = 'res.currency'
  #
  ##############################################################################
  
 -from openerp.addons.base_status.base_stage import base_stage
 -from openerp.addons.project.project import _TASK_STATE
 -from openerp.addons.crm import crm
  from datetime import datetime
 -from openerp.osv import fields, osv, orm
 -from openerp.tools.translate import _
 -import binascii
 -import time
 +
 +from openerp import SUPERUSER_ID
  from openerp import tools
 +from openerp.addons.crm import crm
 +from openerp.osv import fields, osv, orm
  from openerp.tools import html2plaintext
 +from openerp.tools.translate import _
  
 -class project_issue_version(osv.osv):
 +class project_issue_version(osv.Model):
      _name = "project.issue.version"
      _order = "name desc"
      _columns = {
      _defaults = {
          'active': 1,
      }
 -project_issue_version()
  
 -class project_issue(base_stage, osv.osv):
 +class project_issue(osv.Model):
      _name = "project.issue"
      _description = "Project Issue"
      _order = "priority, create_date desc"
      _inherit = ['mail.thread', 'ir.needaction_mixin']
  
 +    _mail_post_access = 'read'
      _track = {
 -        'state': {
 -            'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'],
 -            'project_issue.mt_issue_closed': lambda self, cr, uid, obj, ctx=None:  obj['state'] == 'done',
 -            'project_issue.mt_issue_started': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open',
 -        },
          'stage_id': {
 -            'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'done', 'open'],
 +            # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
 +            'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence <= 1,
 +            'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
 +        },
 +        'user_id': {
 +            'project_issue.mt_issue_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
          },
          'kanban_state': {
 -            'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj['kanban_state'] == 'blocked',
 +            'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked',
          },
      }
  
 -    def create(self, cr, uid, vals, context=None):
 -        if context is None:
 -            context = {}
 -        if vals.get('project_id') and not context.get('default_project_id'):
 -            context['default_project_id'] = vals.get('project_id')
 -
 -        # context: no_log, because subtype already handle this
 -        create_context = dict(context, mail_create_nolog=True)
 -        return super(project_issue, self).create(cr, uid, vals, context=create_context)
 -
      def _get_default_partner(self, cr, uid, context=None):
 -        """ Override of base_stage to add project specific behavior """
          project_id = self._get_default_project_id(cr, uid, context)
          if project_id:
              project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
              if project and project.partner_id:
                  return project.partner_id.id
 -        return super(project_issue, self)._get_default_partner(cr, uid, context=context)
 +        return False
  
      def _get_default_project_id(self, cr, uid, context=None):
          """ Gives default project by checking if present in the context """
@@@ -75,7 -88,7 +75,7 @@@
      def _get_default_stage_id(self, cr, uid, context=None):
          """ Gives default stage_id """
          project_id = self._get_default_project_id(cr, uid, context=context)
 -        return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
 +        return self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context)
  
      def _resolve_project_id_from_context(self, cr, uid, context=None):
          """ Returns ID of project based on the value of 'default_project_id'
          'partner_id': fields.many2one('res.partner', 'Contact', select=1),
          'company_id': fields.many2one('res.company', 'Company'),
          'description': fields.text('Private Note'),
 -        'state': fields.related('stage_id', 'state', type="selection", store=True,
 -                selection=_TASK_STATE, string="Status", readonly=True, select=True,
 -                help='The status is set to \'Draft\', when a case is created.\
 -                      If the case is in progress the status is set to \'Open\'.\
 -                      When the case is over, the status is set to \'Done\'.\
 -                      If the case needs to be reviewed then the status is \
 -                      set to \'Pending\'.'),
          'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
                                           track_visibility='onchange',
                                           help="A Issue's kanban state indicates special situations affecting it:\n"
          # Project Issue fields
          'date_closed': fields.datetime('Closed', readonly=True,select=True),
          'date': fields.datetime('Date'),
 +        'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
          'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
          'categ_ids': fields.many2many('project.category', string='Tags'),
          'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
          'version_id': fields.many2one('project.issue.version', 'Version'),
          'stage_id': fields.many2one ('project.task.type', 'Stage',
                          track_visibility='onchange', select=True,
 -                        domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
 +                        domain="[('project_ids', '=', project_id)]"),
          'project_id': fields.many2one('project.project', 'Project', track_visibility='onchange', select=True),
          'duration': fields.float('Duration'),
          'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
          'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
              store = {
                  'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
-                 'project.task': (_get_issue_task, ['progress'], 10),
+                 'project.task': (_get_issue_task, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
                  'project.task.work': (_get_issue_work, ['hours'], 10),
              }),
      }
  
      _defaults = {
          'active': 1,
 -        'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
 -        'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
          'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
          'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
          'priority': crm.AVAILABLE_PRIORITIES[2][0],
          'kanban_state': 'normal',
 +        'date_last_stage_update': fields.datetime.now,
          'user_id': lambda obj, cr, uid, context: uid,
      }
  
      }
  
      def set_priority(self, cr, uid, ids, priority, *args):
 -        """Set lead priority
 -        """
          return self.write(cr, uid, ids, {'priority' : priority})
  
      def set_high_priority(self, cr, uid, ids, *args):
          """
          return self.set_priority(cr, uid, ids, '3')
  
 -    def convert_issue_task(self, cr, uid, ids, context=None):
 -        if context is None:
 -            context = {}
 -
 -        case_obj = self.pool.get('project.issue')
 -        data_obj = self.pool.get('ir.model.data')
 -        task_obj = self.pool.get('project.task')
 -
 -        result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
 -        res = data_obj.read(cr, uid, result, ['res_id'])
 -        id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
 -        id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
 -        if id2:
 -            id2 = data_obj.browse(cr, uid, id2, context=context).res_id
 -        if id3:
 -            id3 = data_obj.browse(cr, uid, id3, context=context).res_id
 -
 -        for bug in case_obj.browse(cr, uid, ids, context=context):
 -            new_task_id = task_obj.create(cr, uid, {
 -                'name': bug.name,
 -                'partner_id': bug.partner_id.id,
 -                'description':bug.description,
 -                'date_deadline': bug.date,
 -                'project_id': bug.project_id.id,
 -                # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
 -                'priority': str(int(bug.priority) - 1),
 -                'user_id': bug.user_id.id,
 -                'planned_hours': 0.0,
 -            })
 -            vals = {
 -                'task_id': new_task_id,
 -                'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
 -            }
 -            message = _("Project issue <b>converted</b> to task.")
 -            self.message_post(cr, uid, [bug.id], body=message, context=context)
 -            case_obj.write(cr, uid, [bug.id], vals, context=context)
 -
 -        return  {
 -            'name': _('Tasks'),
 -            'view_type': 'form',
 -            'view_mode': 'form,tree',
 -            'res_model': 'project.task',
 -            'res_id': int(new_task_id),
 -            'view_id': False,
 -            'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
 -            'type': 'ir.actions.act_window',
 -            'search_view_id': res['res_id'],
 -            'nodestroy': True
 -        }
 -
      def copy(self, cr, uid, id, default=None, context=None):
          issue = self.read(cr, uid, id, ['name'], context=context)
          if not default:
          return super(project_issue, self).copy(cr, uid, id, default=default,
                  context=context)
  
 +    def create(self, cr, uid, vals, context=None):
 +        if context is None:
 +            context = {}
 +        if vals.get('project_id') and not context.get('default_project_id'):
 +            context['default_project_id'] = vals.get('project_id')
 +
 +        # context: no_log, because subtype already handle this
 +        create_context = dict(context, mail_create_nolog=True)
 +        return super(project_issue, self).create(cr, uid, vals, context=create_context)
 +
      def write(self, cr, uid, ids, vals, context=None):
 -    
 -        #Update last action date every time the user changes the stage
 +        # stage change: update date_last_stage_update
          if 'stage_id' in vals:
 -            vals['date_action_last'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
 +            vals['date_last_stage_update'] = fields.datetime.now()
              if 'kanban_state' not in vals:
 -                vals.update(kanban_state='normal')
 -            state = self.pool.get('project.task.type').browse(cr, uid, vals['stage_id'], context=context).state
 -            for issue in self.browse(cr, uid, ids, context=context):
 -                # Change from draft to not draft EXCEPT cancelled: The issue has been opened -> set the opening date
 -                if issue.state == 'draft' and state not in ('draft', 'cancelled'):
 -                    vals['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
 -                # Change from not done to done: The issue has been closed -> set the closing date
 -                if issue.state != 'done' and state == 'done':
 -                    vals['date_closed'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
 +                vals['kanban_state'] = 'normal'
 +        # user_id change: update date_start
 +        if vals.get('user_id'):
 +            vals['date_start'] = fields.datetime.now()
  
          return super(project_issue, self).write(cr, uid, ids, vals, context)
  
          task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
          return {'value': {'user_id': task.user_id.id, }}
  
 -    def case_reset(self, cr, uid, ids, context=None):
 -        """Resets case as draft
 +    def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
 +        """ This function returns value of partner email address based on partner
 +            :param part: Partner's id
          """
 -        res = super(project_issue, self).case_reset(cr, uid, ids, context)
 -        self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
 -        return res
 +        result = {}
 +        if partner_id:
 +            partner = self.pool['res.partner'].browse(cr, uid, partner_id, context)
 +            result['email_from'] = partner.email
 +        return {'value': result}
 +
 +    def get_empty_list_help(self, cr, uid, help, context=None):
 +        context['empty_list_help_model'] = 'project.project'
 +        context['empty_list_help_id'] = context.get('default_project_id')
 +        context['empty_list_help_document_name'] = _("issues")
 +        return super(project_issue, self).get_empty_list_help(cr, uid, help, context=context)
  
      # -------------------------------------------------------
      # Stage management
              return stage_ids[0]
          return False
  
 -    def case_cancel(self, cr, uid, ids, context=None):
 -        """ Cancels case """
 -        self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
 -        return True
 -
 -    def case_escalate(self, cr, uid, ids, context=None):
 -        cases = self.browse(cr, uid, ids)
 -        for case in cases:
 +    def case_escalate(self, cr, uid, ids, context=None):        # FIXME rename this method to issue_escalate
 +        for issue in self.browse(cr, uid, ids, context=context):
              data = {}
 -            if case.project_id.project_escalation_id:
 -                data['project_id'] = case.project_id.project_escalation_id.id
 -                if case.project_id.project_escalation_id.user_id:
 -                    data['user_id'] = case.project_id.project_escalation_id.user_id.id
 -                if case.task_id:
 -                    self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
 -            else:
 +            esc_proj = issue.project_id.project_escalation_id
 +            if not esc_proj:
                  raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
 -            self.case_set(cr, uid, ids, 'draft', data, context=context)
 +
 +            data['project_id'] = esc_proj.id
 +            if esc_proj.user_id:
 +                data['user_id'] = esc_proj.user_id.id
 +            issue.write(data)
 +
 +            if issue.task_id:
 +                issue.task_id.write({'project_id': esc_proj.id, 'user_id': False})
          return True
  
      # -------------------------------------------------------
          if context is None:
              context = {}
          context['state_to'] = 'draft'
 -
 -        desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
 -
          defaults = {
              'name':  msg.get('subject') or _("No Subject"),
 -            'description': desc,
              'email_from': msg.get('from'),
              'email_cc': msg.get('cc'),
              'partner_id': msg.get('author_id', False),
          """
          if context is None:
              context = {}
 -        
          res = super(project_issue, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
 -        
 -        if thread_id:
 -            self.write(cr, uid, thread_id, {'date_action_last': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)    
 -        
 -        return res   
 +        if thread_id and subtype:
 +            self.write(cr, SUPERUSER_ID, thread_id, {'date_action_last': fields.datetime.now()}, context=context)
 +        return res
  
 -class project(osv.osv):
 +
 +class project(osv.Model):
      _inherit = "project.project"
  
      def _get_alias_models(self, cr, uid, context=None):
          return [('project.task', "Tasks"), ("project.issue", "Issues")]
  
      def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
 +        """ :deprecated: this method will be removed with OpenERP v8. Use issue_ids
 +                         fields instead. """
          res = dict.fromkeys(ids, 0)
          issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
          for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
 -            if issue.state not in ('done', 'cancelled'):
 +            if issue.stage_id and not issue.stage_id.fold:
                  res[issue.project_id.id] += 1
          return res
  
      _columns = {
 -        'project_escalation_id' : fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
 -        'issue_count': fields.function(_issue_count, type='integer', string="Unclosed Issues"),
 +        'project_escalation_id': fields.many2one('project.project', 'Project Escalation',
 +            help='If any issue is escalated from the current Project, it will be listed under the project selected here.',
 +            states={'close': [('readonly', True)], 'cancelled': [('readonly', True)]}),
 +        'issue_count': fields.function(_issue_count, type='integer', string="Unclosed Issues",
 +                                       deprecated="This field will be removed in OpenERP v8. Use issue_ids one2many field instead."),
 +        'issue_ids': fields.one2many('project.issue', 'project_id',
 +                                     domain=[('stage_id.fold', '=', False)])
      }
  
      def _check_escalation(self, cr, uid, ids, context=None):
          (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
      ]
  
 -project()
  
 -class account_analytic_account(osv.osv):
 +class account_analytic_account(osv.Model):
      _inherit = 'account.analytic.account'
      _description = 'Analytic Account'
  
      _columns = {
 -        'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
 +        'use_issues': fields.boolean('Issues', help="Check this field if this project manages issues"),
      }
  
      def on_change_template(self, cr, uid, ids, template_id, context=None):
          return res
  
      def _trigger_project_creation(self, cr, uid, vals, context=None):
 -        if context is None: context = {}
 +        if context is None:
 +            context = {}
          res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
          return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
  
 -account_analytic_account()
  
 -class project_project(osv.osv):
 +class project_project(osv.Model):
      _inherit = 'project.project'
 +
      _defaults = {
          'use_issues': True
      }
  
 +    def _check_create_write_values(self, cr, uid, vals, context=None):
 +        """ Perform some check on values given to create or write. """
 +        # Handle use_tasks / use_issues: if only one is checked, alias should take the same model
 +        if vals.get('use_tasks') and not vals.get('use_issues'):
 +            vals['alias_model'] = 'project.task'
 +        elif vals.get('use_issues') and not vals.get('use_tasks'):
 +            vals['alias_model'] = 'project.issue'
 +
 +    def on_change_use_tasks_or_issues(self, cr, uid, ids, use_tasks, use_issues, context=None):
 +        values = {}
 +        if use_tasks and not use_issues:
 +            values['alias_model'] = 'project.task'
 +        elif not use_tasks and use_issues:
 +            values['alias_model'] = 'project.issue'
 +        return {'value': values}
 +
 +    def create(self, cr, uid, vals, context=None):
 +        self._check_create_write_values(cr, uid, vals, context=context)
 +        return super(project_project, self).create(cr, uid, vals, context=context)
 +
 +    def write(self, cr, uid, ids, vals, context=None):
 +        self._check_create_write_values(cr, uid, vals, context=context)
 +        return super(project_project, self).write(cr, uid, ids, vals, context=context)
 +
 +
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
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)
          if ('name' not in default) or (picking_obj.name == '/'):
              seq_obj_name = 'stock.picking.' + picking_obj.type
              default['name'] = self.pool.get('ir.sequence').get(cr, uid, seq_obj_name)
-             default['origin'] = ''
-             default['backorder_id'] = False
+             default.setdefault('origin', False)
+             default.setdefault('backorder_id', False)
          if 'invoice_state' not in default and picking_obj.invoice_state == 'invoiced':
              default['invoice_state'] = '2binvoiced'
          res = super(stock_picking, self).copy(cr, uid, id, default, 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 = [], [], []
                  partial_data = partial_datas.get('move%s'%(move.id), {})
                  product_qty = partial_data.get('product_qty',0.0)
                  move_product_qty[move.id] = product_qty
-                 product_uom = partial_data.get('product_uom',False)
+                 product_uom = partial_data.get('product_uom', move.product_uom.id)
                  product_price = partial_data.get('product_price',0.0)
                  product_currency = partial_data.get('product_currency',False)
                  prodlot_id = partial_data.get('prodlot_id')
  
              # 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 = new_picking
                  self.message_post(cr, uid, new_picking, body=_("Back order <em>%s</em> has been <b>created</b>.") % (pick.name), context=context)
              elif empty_picking:
                  delivered_pack_id = pick.id
              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)
@@@ -1505,6 -1512,7 +1505,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
@@@ -2062,6 -2071,7 +2062,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):
          """
          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
@@@ -2988,6 -3008,7 +2988,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)
@@@ -3016,25 -3037,15 +3016,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"""
@@@ -3099,25 -3110,15 +3099,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"""
@@@ -1,6 -1,4 +1,6 @@@
 -openerp.web.form = function (instance) {
 +(function() {
 +
 +var instance = openerp;
  var _t = instance.web._t,
     _lt = instance.web._lt;
  var QWeb = instance.web.qweb;
@@@ -207,7 -205,7 +207,7 @@@ instance.web.FormView = instance.web.Vi
          this.has_been_loaded.resolve();
  
          // Add bounce effect on button 'Edit' when click on readonly page view.
 -        this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
 +        this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
              if(self.get("actual_mode") == "view") {
                  var $button = self.options.$buttons.find(".oe_form_button_edit");
                  $button.openerpBounce();
              self.on_form_changed();
              self.rendering_engine.init_fields();
              self.is_initialized.resolve();
 -            self.do_update_pager(record.id == null);
 +            self.do_update_pager(record.id === null || record.id === undefined);
              if (self.sidebar) {
                 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
              }
                      this.dataset.index = this.dataset.ids.length - 1;
                      break;
              }
 -            this.reload();
 +            var def = this.reload();
              this.trigger('pager_action_executed');
 +            return def;
          }
 +        return $.when();
      },
      init_pager: function() {
          var self = this;
              this.$el.find('.oe_form_pager').replaceWith(this.$pager);
          }
          this.$pager.on('click','a[data-pager-action]',function() {
 -            var action = $(this).data('pager-action');
 -            self.execute_pager_action(action);
 +            var $el = $(this);
 +            if ($el.attr("disabled"))
 +                return;
 +            var action = $el.data('pager-action');
 +            var def = $.when(self.execute_pager_action(action));
 +            $el.attr("disabled");
 +            def.always(function() {
 +                $el.removeAttr("disabled");
 +            });
          });
          this.do_update_pager();
      },
  
          var method = call[1];
          if (!_.str.trim(call[2])) {
 -            return {method: method, args: []}
 +            return {method: method, args: []};
          }
  
          var argument_replacement = {
              // form field
              if (self.fields[field]) {
                  var value_ = self.fields[field].get_value();
 -                return value_ == null ? false : value_;
 +                return value_ === null || value_ === undefined ? false : value_;
              }
              // parent field
              var splitted = field.split('.');
                  }
                  var p_val = parent_fields[_.str.trim(splitted[1])];
                  if (p_val !== undefined) {
 -                    return p_val == null ? false : p_val;
 +                    return p_val === null || p_val === undefined ? false : p_val;
                  }
              }
              // string literal
      },
      on_processed_onchange: function(result, processed) {
          try {
 +        var fields = this.fields;
 +        _(result.domain).each(function (domain, fieldname) {
 +            var field = fields[fieldname];
 +            if (!field) { return; }
 +            field.node.attrs.domain = domain;
 +        });
 +            
          if (result.value) {
              this._internal_set_values(result.value, processed);
          }
              });
          }
  
 -        var fields = this.fields;
 -        _(result.domain).each(function (domain, fieldname) {
 -            var field = fields[fieldname];
 -            if (!field) { return; }
 -            field.node.attrs.domain = domain;
 -        });
 -
          return $.Deferred().resolve();
          } catch(e) {
              console.error(e);
                      self.save_list.pop();
                      return $.when();
                  });
 -            };
 +            }
              return iterate();
          });
      },
              } else {
                  $.async_when().done(function () {
                      def.reject();
 -                })
 +                });
              }
          });
          return def.promise();
      reload: function() {
          var self = this;
          return this.reload_mutex.exec(function() {
 -            if (self.dataset.index == null) {
 +            if (self.dataset.index === null || self.dataset.index === undefined) {
                  self.trigger("previous_view");
                  return $.Deferred().reject().promise();
              }
 -            if (self.dataset.index == null || self.dataset.index < 0) {
 +            if (self.dataset.index < 0) {
                  return $.when(self.on_button_new());
              } else {
                  var fields = _.keys(self.fields_view.fields);
      open_defaults_dialog: function () {
          var self = this;
          var display = function (field, value) {
 +            if (!value) { return value; }
              if (field instanceof instance.web.form.FieldSelection) {
 -                return _(field.values).find(function (option) {
 +                return _(field.get('values')).find(function (option) {
                      return option[0] === value;
                  })[1];
              } else if (field instanceof instance.web.form.FieldMany2One) {
                  return field.get_displayed();
              }
              return value;
 -        }
 +        };
          var fields = _.chain(this.fields)
              .map(function (field) {
                  var value = field.get_value();
                      string: field.string,
                      value: value,
                      displayed: display(field, value),
 -                }
 +                };
              })
              .compact()
              .sortBy(function (field) { return field.string; })
                      string: field.string,
                      value: value,
                      displayed: display(field, value),
 -                }
 +                };
              })
              .value();
  
@@@ -1454,7 -1442,7 +1454,7 @@@ instance.web.form.FormRenderingEngine 
                  row_cols = cols;
              } else if (tagName==='group') {
                  // When <group> <group/><group/> </group>, we need a spacing between the two groups
 -                $td.addClass('oe_group_right')
 +                $td.addClass('oe_group_right');
              }
              row_cols -= colspan;
  
@@@ -1739,7 -1727,7 +1739,7 @@@ instance.web.form.compute_domain = func
  };
  
  instance.web.form.is_bin_size = function(v) {
 -    return /^\d+(\.\d*)? \w+$/.test(v);
 +    return (/^\d+(\.\d*)? \w+$/).test(v);
  };
  
  /**
@@@ -1877,8 -1865,7 +1877,8 @@@ instance.web.form.FormWidget = instance
                      return QWeb.render(template, {
                          debug: instance.session.debug,
                          widget: widget
 -                })},
 +                    });
 +                },
                  gravity: $.fn.tipsy.autoBounds(50, 'nw'),
                  html: true,
                  opacity: 0.85,
@@@ -2101,7 -2088,7 +2101,7 @@@ instance.web.form.AbstractField = insta
       * @param node
       */
      init: function(field_manager, node) {
 -        var self = this
 +        var self = this;
          this._super(field_manager, node);
          this.name = this.node.attrs.name;
          this.field = this.field_manager.get_field_desc(this.name);
@@@ -2272,7 -2259,7 +2272,7 @@@ instance.web.form.ReinitializeFieldMixi
  /**
      Some hack to make placeholders work in ie9.
  */
 -if ($.browser.msie && $.browser.version === "9.0") {
 +if (!('placeholder' in document.createElement('input'))) {    
      document.addEventListener("DOMNodeInserted",function(event){
          var nodename =  event.target.nodeName.toLowerCase();
          if ( nodename === "input" || nodename == "textarea" ) {
@@@ -2681,7 -2668,7 +2681,7 @@@ instance.web.form.FieldText = instance.
          if (! this.get("effective_readonly")) {
              var show_value = instance.web.format_value(this.get('value'), this, '');
              if (show_value === '') {
 -                this.$textarea.css('height', parseInt(this.default_height)+"px");
 +                this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
              }
              this.$textarea.val(show_value);
              if (! this.auto_sized) {
@@@ -2790,7 -2777,6 +2790,7 @@@ instance.web.form.FieldBoolean = instan
          }, this));
          var check_readonly = function() {
              self.$checkbox.prop('disabled', self.get("effective_readonly"));
 +            self.click_disabled_boolean();
          };
          this.on("change:effective_readonly", this, check_readonly);
          check_readonly.call(this);
      focus: function() {
          var input = this.$checkbox && this.$checkbox[0];
          return input ? input.focus() : false;
 +    },
 +    click_disabled_boolean: function(){
 +        var $disabled = this.$el.find('input[type=checkbox]:disabled');
 +        $disabled.each(function (){
 +            $(this).next('div').remove();
 +            $(this).closest("span").append($('<div class="boolean"></div>'));
 +        });
      }
  });
  
@@@ -2836,37 -2815,10 +2836,37 @@@ instance.web.form.FieldSelection = inst
      init: function(field_manager, node) {
          var self = this;
          this._super(field_manager, node);
 -        this.values = _(this.field.selection).chain()
 -            .reject(function (v) { return v[0] === false && v[1] === ''; })
 -            .unshift([false, ''])
 -            .value();
 +        this.set("value", false);
 +        this.set("values", []);
 +        this.records_orderer = new instance.web.DropMisordered();
 +        this.field_manager.on("view_content_has_changed", this, function() {
 +            var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
 +            if (! _.isEqual(domain, this.get("domain"))) {
 +                this.set("domain", domain);
 +            }
 +        });
 +    },
 +    initialize_field: function() {
 +        instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
 +        this.on("change:domain", this, this.query_values);
 +        this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
 +        this.on("change:values", this, this.render_value);
 +    },
 +    query_values: function() {
 +        var self = this;
 +        var def;
 +        if (this.field.type === "many2one") {
 +            var model = new openerp.Model(openerp.session, this.field.relation);
 +            def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
 +        } else {
 +            var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
 +            def = $.when(values);
 +        }
 +        this.records_orderer.add(def).then(function(values) {
 +            if (! _.isEqual(values, self.get("values"))) {
 +                self.set("values", values);
 +            }
 +        });
      },
      initialize_content: function() {
          // Flag indicating whether we're in an event chain containing a change
      },
      store_dom_value: function () {
          if (!this.get('effective_readonly') && this.$('select').length) {
 -            this.internal_set_value(
 -                this.values[this.$('select')[0].selectedIndex][0]);
 +            var val = JSON.parse(this.$('select').val());
 +            this.internal_set_value(val);
          }
      },
      set_value: function(value_) {
          this._super(value_);
      },
      render_value: function() {
 -        if (!this.get("effective_readonly")) {
 -            var index = 0;
 -            for (var i = 0, ii = this.values.length; i < ii; i++) {
 -                if (this.values[i][0] === this.get('value')) index = i;
 -            }
 -            this.$el.find('select')[0].selectedIndex = index;
 +        var values = this.get("values");
 +        values =  [[false, this.node.attrs.placeholder || '']].concat(values);
 +        var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
 +        if (! found) {
 +            found = [this.get("value"), _t('Unknown')];
 +            values = [found].concat(values);
 +        }
 +        if (! this.get("effective_readonly")) {
 +            this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
 +            this.$("select").val(JSON.stringify(found[0]));
          } else {
 -            var self = this;
 -            var option = _(this.values)
 -                .detect(function (record) { return record[0] === self.get('value'); });
 -            this.$el.text(option ? option[1] : this.values[0][1]);
 +            this.$el.text(found[1]);
          }
      },
      focus: function() {
      }
  });
  
 +instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 +    template: 'FieldRadio',
 +    events: {
 +        'click input': 'click_change_value'
 +    },
 +    init: function(field_manager, node) {
 +        /* Radio button widget: Attributes options:
 +        * - "horizontal" to display in column
 +        * - "no_radiolabel" don't display text values
 +        */
 +        this._super(field_manager, node);
 +        this.selection = _.clone(this.field.selection) || [];
 +        this.domain = false;
 +    },
 +    initialize_content: function () {
 +        this.uniqueId = _.uniqueId("radio");
 +        this.on("change:effective_readonly", this, this.render_value);
 +        this.field_manager.on("view_content_has_changed", this, this.get_selection);
 +        this.get_selection();
 +    },
 +    click_change_value: function (event) {
 +        var val = $(event.target).val();
 +        val = this.field.type == "selection" ? val : +val;
 +        if (val == this.get_value()) {
 +            this.set_value(false);
 +        } else {
 +            this.set_value(val);
 +        }
 +    },
 +    /** Get the selection and render it
 +     *  selection: [[identifier, value_to_display], ...]
 +     *  For selection fields: this is directly given by this.field.selection
 +     *  For many2one fields:  perform a search on the relation of the many2one field
 +     */
 +    get_selection: function() {
 +        var self = this;
 +        var selection = [];
 +        var def = $.Deferred();
 +        if (self.field.type == "many2one") {
 +            var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
 +            if (! _.isEqual(self.domain, domain)) {
 +                self.domain = domain;
 +                var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
 +                ds.call('search', [self.domain])
 +                    .then(function (records) {
 +                        ds.name_get(records).then(function (records) {
 +                            selection = records;
 +                            def.resolve();
 +                        });
 +                    });
 +            } else {
 +                selection = self.selection;
 +                def.resolve();
 +            }
 +        }
 +        else if (self.field.type == "selection") {
 +            selection = self.field.selection || [];
 +            def.resolve();
 +        }
 +        return def.then(function () {
 +            if (! _.isEqual(selection, self.selection)) {
 +                self.selection = _.clone(selection);
 +                self.renderElement();
 +                self.render_value();
 +            }
 +        });
 +    },
 +    set_value: function (value_) {
 +        if (value_) {
 +            if (this.field.type == "selection") {
 +                value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
 +            }
 +            else if (!this.selection.length) {
 +                this.selection = [value_];
 +            }
 +        }
 +        this._super(value_);
 +    },
 +    get_value: function () {
 +        var value = this.get('value');
 +        return value instanceof Array ? value[0] : value;
 +    },
 +    render_value: function () {
 +        var self = this;
 +        this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
 +        this.$("input:checked").prop("checked", false);
 +        if (this.get_value()) {
 +            this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
 +            this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
 +        }
 +    }
 +});
 +
  // jquery autocomplete tweak to allow html and classnames
  (function() {
      var proto = $.ui.autocomplete.prototype,
  })();
  
  /**
 - * A mixin containing some useful methods to handle completion inputs.
 - */
 +    A mixin containing some useful methods to handle completion inputs.
 +    
 +    The widget containing this option can have these arguments in its widget options:
 +    - no_quick_create: if true, it will disable the quick create
 +*/
  instance.web.form.CompletionFieldMixin = {
      init: function() {
          this.limit = 7;
              }
              // quick create
              var raw_result = _(data.result).map(function(x) {return x[1];});
 -            if (search_val.length > 0 && !_.include(raw_result, search_val)) {
 +            if (search_val.length > 0 && !_.include(raw_result, search_val) &&
 +                ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
                  values.push({
                      label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
                          $('<span />').text(search_val).html()),
                  });
              }
              // create...
 -            values.push({
 -                label: _t("Create and Edit..."),
 -                action: function() {
 -                    self._search_create_popup("form", undefined, self._create_context(search_val));
 -                },
 -                classname: 'oe_m2o_dropdown_option'
 -            });
 +            if (!(self.options && self.options.no_create)){
 +                values.push({
 +                    label: _t("Create and Edit..."),
 +                    action: function() {
 +                        self._search_create_popup("form", undefined, self._create_context(search_val));
 +                    },
 +                    classname: 'oe_m2o_dropdown_option'
 +                });
 +            }
  
              return values;
          });
              self.field.relation,
              {
                  title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
 -                initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
 +                initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
                  initial_view: view,
                  disable_multiple_selection: true
              },
@@@ -3233,7 -3085,6 +3233,7 @@@ instance.web.form.FieldMany2One = insta
          instance.web.form.CompletionFieldMixin.init.call(this);
          this.set({'value': false});
          this.display_value = {};
 +        this.display_value_backup = {};
          this.last_search = [];
          this.floating = false;
          this.current_display = null;
              );
              pop.on('write_completed', self, function(){
                  self.display_value = {};
 +                self.display_value_backup = {};
                  self.render_value();
                  self.focus();
                  self.trigger('changed_value');
          this.$input.keydown(input_changed);
          this.$input.change(input_changed);
          this.$drop_down.click(function() {
 +            self.$input.focus();
              if (self.$input.autocomplete("widget").is(":visible")) {
 -                self.$input.autocomplete("close");
 -                self.$input.focus();
 +                self.$input.autocomplete("close");                
              } else {
                  if (self.get("value") && ! self.floating) {
                      self.$input.autocomplete("search", "");
                  if (self.last_search.length > 0) {
                      if (self.last_search[0][0] != self.get("value")) {
                          self.display_value = {};
 +                        self.display_value_backup = {};
                          self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
                          self.reinit_value(self.last_search[0][0]);
                      } else {
                  }
                  self.floating = false;
              }
 -            if (used && self.get("value") === false && ! self.no_ed) {
 +            if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
                  self.ed_def.reject();
                  self.uned_def.reject();
                  self.ed_def = $.Deferred();
                  var item = ui.item;
                  if (item.id) {
                      self.display_value = {};
 +                    self.display_value_backup = {};
                      self.display_value["" + item.id] = item.name;
                      self.reinit_value(item.id);
                  } else if (item.action) {
                  }
                  self.display_value["" + self.get("value")] = data[0][1];
                  self.render_value(true);
 +            }).fail( function (data, event) {
 +                // avoid displaying crash errors as many2One should be name_get compliant
 +                event.preventDefault();
 +                self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
 +                self.render_value(true);
              });
          }
      },
          var self = this;
          if (value_ instanceof Array) {
              this.display_value = {};
 +            this.display_value_backup = {};
              if (! this.options.always_reload) {
                  this.display_value["" + value_[0]] = value_[1];
              }
 +            else {
 +                this.display_value_backup["" + value_[0]] = value_[1];
 +            }
              value_ = value_[0];
          }
          value_ = value_ || false;
      },
      add_id: function(id) {
          this.display_value = {};
 +        this.display_value_backup = {};
          this.reinit_value(id);
      },
      is_false: function() {
@@@ -3629,67 -3467,6 +3629,67 @@@ instance.web.form.Many2OneButton = inst
       },
  });
  
 +/**
 + * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
 + * the big ugly button in the header.
 + *
 + * Requires the implementation of a ``is_readonly`` method (usually a proxy to
 + * the corresponding field's readonly or effective_readonly property) to
 + * decide whether the special row should or should not be inserted.
 + *
 + * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
 + * set on the insertion row.
 + */
 +instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
 +    pad_table_to: function (count) {
 +        if (!this.view.is_action_enabled('create') || this.is_readonly()) {
 +            this._super(count);
 +            return;
 +        }
 +
 +        this._super(count > 0 ? count - 1 : 0);
 +
 +        var self = this;
 +        var columns = _(this.columns).filter(function (column) {
 +            return column.invisible !== '1';
 +        }).length;
 +        if (this.options.selectable) { columns++; }
 +        if (this.options.deletable) { columns++; }
 +
 +        var $cell = $('<td>', {
 +            colspan: columns,
 +            'class': this._add_row_class || ''
 +        }).append(
 +            $('<a>', {href: '#'}).text(_t("Add an item"))
 +                .mousedown(function () {
 +                    // FIXME: needs to be an official API somehow
 +                    if (self.view.editor.is_editing()) {
 +                        self.view.__ignore_blur = true;
 +                    }
 +                })
 +                .click(function (e) {
 +                    e.preventDefault();
 +                    e.stopPropagation();
 +                    // FIXME: there should also be an API for that one
 +                    if (self.view.editor.form.__blur_timeout) {
 +                        clearTimeout(self.view.editor.form.__blur_timeout);
 +                        self.view.editor.form.__blur_timeout = false;
 +                    }
 +                    self.view.ensure_saved().done(function () {
 +                        self.view.do_add_record();
 +                    });
 +                }));
 +
 +        var $padding = this.$current.find('tr:not([data-id]):first');
 +        var $newrow = $('<tr>').append($cell);
 +        if ($padding.length) {
 +            $padding.before($newrow);
 +        } else {
 +            this.$current.append($newrow)
 +        }
 +    }
 +});
 +
  /*
  # Values: (0, 0,  { fields })    create
  #         (1, ID, { fields })    update
@@@ -3867,7 -3644,7 +3867,7 @@@ instance.web.form.FieldOne2Many = insta
                   });
                  controller.on('pager_action_executed',self,self.save_any_view);
              } else if (view_type == "graph") {
 -                self.reload_current_view()
 +                self.reload_current_view();
              }
              def.resolve();
          });
      },
      reload_current_view: function() {
          var self = this;
 -        return self.is_loaded = self.is_loaded.then(function() {
 +        self.is_loaded = self.is_loaded.then(function() {
              var active_view = self.viewmanager.active_view;
              var view = self.viewmanager.views[active_view].controller;
              if(active_view === "list") {
                  return view.do_search(self.build_domain(), self.dataset.get_context(), []);
              }
          }, undefined);
 +        return self.is_loaded;
      },
      set_value: function(value_) {
          value_ = value_ || [];
          var self = this;
          this.dataset.reset_ids([]);
 +        var ids;
          if(value_.length >= 1 && value_[0] instanceof Array) {
 -            var ids = [];
 +            ids = [];
              _.each(value_, function(command) {
                  var obj = {values: command[2]};
                  switch (command[0]) {
              this._super(ids);
              this.dataset.set_ids(ids);
          } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
 -            var ids = [];
 +            ids = [];
              this.dataset.delete_all = true;
              _.each(value_, function(command) {
                  var obj = {values: command};
              this.viewmanager.views[this.viewmanager.active_view].controller) {
              var view = this.viewmanager.views[this.viewmanager.active_view].controller;
              if (this.viewmanager.active_view === "form") {
 -                if (!view.is_initialized.state() === 'resolved') {
 +                if (view.is_initialized.state() !== 'resolved') {
                      return $.when(false);
                  }
                  return $.when(view.save());
                  .invoke('is_valid')
                  .all(_.identity)
                  .value();
 -            break;
          case 'list':
              return view.is_valid();
          }
@@@ -4105,28 -3881,23 +4105,23 @@@ instance.web.form.One2ManyListView = in
          this.o2m.trigger_on_change();
      },
      is_valid: function () {
-         var editor = this.editor;
-         var form = editor.form;
-         // If no edition is pending, the listview can not be invalid (?)
-         if (!editor.record) {
-             return true;
-         }
-         // If the form has not been modified, the view can only be valid
-         // NB: is_dirty will also be set on defaults/onchanges/whatever?
-         // oe_form_dirty seems to only be set on actual user actions
-         if (!form.$el.is('.oe_form_dirty')) {
+         var self = this;
+         if (!this.editable()){
              return true;
          }
          this.o2m._dirty_flag = true;
-         // Otherwise validate internal form
-         return _(form.fields).chain()
-             .invoke(function () {
-                 this._check_css_flags();
-                 return this.is_valid();
-             })
-             .all(_.identity)
-             .value();
+         var r;
+         return _.every(this.records.records, function(record){
+             r = record;
+             _.each(self.editor.form.fields, function(field){
+                 field.set_value(r.attributes[field.name]);
+             });
+             return _.every(self.editor.form.fields, function(field){
+                 field.process_modifiers();
+                 field._check_css_flags();
+                 return field.is_valid();
+             });
+         });
      },
      do_add_record: function () {
          if (this.editable()) {
@@@ -4276,11 -4047,62 +4271,11 @@@ instance.web.form.One2ManyGroups = inst
          }
      }
  });
 -instance.web.form.One2ManyList = instance.web.ListView.List.extend({
 -    pad_table_to: function (count) {
 -        if (!this.view.is_action_enabled('create')) {
 -            this._super(count);
 -        } else {
 -            this._super(count > 0 ? count - 1 : 0);
 -        }
 -
 -        // magical invocation of wtf does that do
 -        if (this.view.o2m.get('effective_readonly')) {
 -            return;
 -        }
 -
 -        var self = this;
 -        var columns = _(this.columns).filter(function (column) {
 -            return column.invisible !== '1';
 -        }).length;
 -        if (this.options.selectable) { columns++; }
 -        if (this.options.deletable) { columns++; }
 -
 -        if (!this.view.is_action_enabled('create')) {
 -            return;
 -        }
 -
 -        var $cell = $('<td>', {
 -            colspan: columns,
 -            'class': 'oe_form_field_one2many_list_row_add'
 -        }).append(
 -            $('<a>', {href: '#'}).text(_t("Add an item"))
 -                .mousedown(function () {
 -                    // FIXME: needs to be an official API somehow
 -                    if (self.view.editor.is_editing()) {
 -                        self.view.__ignore_blur = true;
 -                    }
 -                })
 -                .click(function (e) {
 -                    e.preventDefault();
 -                    e.stopPropagation();
 -                    // FIXME: there should also be an API for that one
 -                    if (self.view.editor.form.__blur_timeout) {
 -                        clearTimeout(self.view.editor.form.__blur_timeout);
 -                        self.view.editor.form.__blur_timeout = false;
 -                    }
 -                    self.view.ensure_saved().done(function () {
 -                        self.view.do_add_record();
 -                    });
 -                }));
 -
 -        var $padding = this.$current.find('tr:not([data-id]):first');
 -        var $newrow = $('<tr>').append($cell);
 -        if ($padding.length) {
 -            $padding.before($newrow);
 -        } else {
 -            this.$current.append($newrow)
 -        }
 -    }
 +instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
 +    _add_row_class: 'oe_form_field_one2many_list_row_add',
 +    is_readonly: function () {
 +        return this.view.o2m.get('effective_readonly');
 +    },
  });
  
  instance.web.form.One2ManyFormView = instance.web.FormView.extend({
@@@ -4310,7 -4132,6 +4305,7 @@@ var lazy_build_o2m_kanban_view = functi
  
  instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
      template: "FieldMany2ManyTags",
 +    tag_template: "FieldMany2ManyTag",
      init: function() {
          this._super.apply(this, arguments);
          instance.web.form.CompletionFieldMixin.init.call(this);
          this._display_orderer = new instance.web.DropMisordered();
          this._drop_shown = false;
      },
 -    initialize_content: function() {
 -        if (this.get("effective_readonly"))
 -            return;
 +    initialize_texttext: function(){
          var self = this;
 -        var ignore_blur = false;
 -        self.$text = this.$("textarea");
 -        self.$text.textext({
 +        return {
              plugins : 'tags arrow autocomplete',
              autocomplete: {
                  render: function(suggestion) {
                          if (data.id) {
                              self.add_id(data.id);
                          } else {
 -                            ignore_blur = true;
 +                            self.ignore_blur = true;
                              data.action();
                          }
                          this.trigger('setSuggestions', {result : []});
                  },
                  core: {
                      onSetInputData: function(e, data) {
 -                        if (data == '') {
 +                        if (data === '') {
                              this._plugins.autocomplete._suggestions = null;
                          }
                          this.input().val(data);
                      },
                  },
              },
 -        }).bind('getSuggestions', function(e, data) {
 +        }
 +    },
 +    initialize_content: function() {
 +        if (this.get("effective_readonly"))
 +            return;
 +        var self = this;
 +        self.ignore_blur = false;
 +        self.$text = this.$("textarea");
 +        self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
              var _this = this;
              var str = !!data ? data.query || '' : '';
              self.get_search_result(str).done(function(result) {
          self.$text
              .focusin(function () {
                  self.trigger('focused');
 -                ignore_blur = false;
 +                self.ignore_blur = false;
              })
              .focusout(function() {
                  self.$text.trigger("setInputData", "");
 -                if (!ignore_blur) {
 +                if (!self.ignore_blur) {
                      self.trigger('blurred');
                  }
              }).keydown(function(e) {
      get_search_blacklist: function() {
          return this.get("value");
      },
 +    map_tag: function(data){
 +        return _.map(data, function(el) {return {name: el[1], id:el[0]};})
 +    },
 +    get_render_data: function(ids){
 +        var self = this;
 +        var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
 +        return dataset.name_get(ids);
 +    },
 +    render_tag: function(data) {
 +        var self = this;
 +        if (! self.get("effective_readonly")) {
 +            self.tags.containerElement().children().remove();
 +            self.$('textarea').css("padding-left", "3px");
 +            self.tags.addTags(self.map_tag(data));
 +        } else {
 +            self.$el.html(QWeb.render(self.tag_template, {elements: data}));
 +        }
 +    },
      render_value: function() {
          var self = this;
          var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
                  indexed[el[0]] = el;
              });
              data = _.map(values, function(el) { return indexed[el]; });
 -            if (! self.get("effective_readonly")) {
 -                self.tags.containerElement().children().remove();
 -                self.$('textarea').css("padding-left", "3px");
 -                self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
 -            } else {
 -                self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
 -            }
 -        };
 +            self.render_tag(data);
 +        }
          if (! values || values.length > 0) {
 -            return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
 +            return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
          } else {
              handle_names([]);
          }
          var input = this.$text && this.$text[0];
          return input ? input.focus() : false;
      },
 +    set_dimensions: function (height, width) {
 +        this._super(height, width);        
 +        this.$("textarea").css({
 +            width: width,
 +            minHeight: height
 +        });
 +    },    
 +    _search_create_popup: function() {
 +        self.ignore_blur = true;
 +        return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
 +    },
  });
  
  /**
@@@ -4513,7 -4307,7 +4508,7 @@@ instance.web.form.FieldMany2Many = inst
          this.$el.addClass('oe_form_field oe_form_field_many2many');
  
          this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
 -                    'addable': this.get("effective_readonly") ? null : _t("Add"),
 +                    'addable': null,
                      'deletable': this.get("effective_readonly") ? false : true,
                      'selectable': this.multi_selection,
                      'sortable': false,
@@@ -4580,11 -4374,6 +4575,11 @@@ instance.web.form.Many2ManyDataSet = in
   * @extends instance.web.ListView
   */
  instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
 +    init: function (parent, dataset, view_id, options) {
 +        this._super(parent, dataset, view_id, _.extend(options || {}, {
 +            ListType: instance.web.form.Many2ManyList,
 +        }));
 +    },
      do_add_record: function () {
          var pop = new instance.web.form.SelectCreatePopup(this);
          pop.select_element(
       },
      is_action_enabled: function () { return true; },
  });
 +instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
 +    _add_row_class: 'oe_form_field_many2many_list_row_add',
 +    is_readonly: function () {
 +        return this.view.m2m_field.get('effective_readonly');
 +    }
 +});
  
  instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
      disable_utility_classes: true,
          if (type !== "form")
              return;
          var self = this;
 +        var pop;
          if (this.dataset.index === null) {
 -            var pop = new instance.web.form.SelectCreatePopup(this);
 +            pop = new instance.web.form.SelectCreatePopup(this);
              pop.select_element(
                  this.field.relation,
                  {
              });
          } else {
              var id = self.dataset.ids[self.dataset.index];
 -            var pop = new instance.web.form.FormOpenPopup(this);
 +            pop = new instance.web.form.FormOpenPopup(this);
              pop.show_element(self.field.relation, id, self.build_context(), {
                  title: _t("Open: ") + self.string,
                  write_function: function(id, data, options) {
@@@ -5080,7 -4862,7 +5075,7 @@@ instance.web.form.SelectCreatePopup = i
                      self.select_elements(self.selected_ids);
                      self.destroy();
                  });
 -                var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
 +                $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
                  $cbutton.click(function() {
                      self.new_object();
                  });
@@@ -5165,8 -4947,8 +5160,8 @@@ instance.web.form.FieldReference = inst
          this.selection.on("change:value", this, this.on_selection_changed);
          this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
          this.selection
 -            .on('focused', null, function () {self.trigger('focused')})
 -            .on('blurred', null, function () {self.trigger('blurred')});
 +            .on('focused', null, function () {self.trigger('focused');})
 +            .on('blurred', null, function () {self.trigger('blurred');});
  
          this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
              name: 'Referenced Document',
          this.m2o.on("change:value", this, this.data_changed);
          this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
          this.m2o
 -            .on('focused', null, function () {self.trigger('focused')})
 -            .on('blurred', null, function () {self.trigger('blurred')});
 +            .on('focused', null, function () {self.trigger('focused');})
 +            .on('blurred', null, function () {self.trigger('blurred');});
      },
      on_selection_changed: function() {
          if (this.reference_ready) {
@@@ -5335,18 -5117,18 +5330,18 @@@ instance.web.form.FieldBinaryFile = ins
          }
      },
      render_value: function() {
 +        var show_value;
          if (!this.get("effective_readonly")) {
              if (this.node.attrs.filename) {
                  show_value = this.view.datarecord[this.node.attrs.filename] || '';
              } else {
 -                show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
 +                show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
              }
              this.$el.find('input').eq(0).val(show_value);
          } else {
              this.$el.find('a').toggle(!!this.get('value'));
              if (this.get('value')) {
 -                var show_value = _t("Download")
 +                show_value = _t("Download");
                  if (this.view)
                      show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
                  this.$el.find('a').text(show_value);
@@@ -5390,13 -5172,6 +5385,13 @@@ instance.web.form.FieldBinaryImage = in
              url = this.placeholder;
          }
          var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
 +        $($img).click(function(e) {
 +            if(self.view.get("actual_mode") == "view") {
 +                var $button = $(".oe_form_button_edit");
 +                $button.openerpBounce();
 +                e.stopPropagation();
 +            }
 +        });
          this.$el.find('> img').remove();
          this.$el.prepend($img);
          $img.load(function() {
  });
  
  /**
 - * Widget for (one2many field) to upload one or more file in same time and display in list.
 + * Widget for (many2many field) to upload one or more file in same time and display in list.
   * The user can delete his files.
   * Options on attribute ; "blockui" {Boolean} block the UI or not
   * during the file is uploading
   */
 -instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
 +instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
      template: "FieldBinaryFileUploader",
      init: function(field_manager, node) {
          this._super(field_manager, node);
          if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
              throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
          }
 +        this.data = {};
 +        this.set_value([]);
          this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
          this.fileupload_id = _.uniqueId('oe_fileupload_temp');
          $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
      start: function() {
          this._super(this);
          this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
 +        this.on("change:effective_readonly", this, function () {
 +            this.render_value();
 +        });
      },
      set_value: function(value_) {
 -        var value_ = value_ || [];
 -        var self = this;
 -        var ids = [];
 -        _.each(value_, function(command) {
 -            if (isNaN(command) && command.id == undefined) {
 -                switch (command[0]) {
 -                    case commands.CREATE:
 -                        ids = ids.concat(command[2]);
 -                        return;
 -                    case commands.REPLACE_WITH:
 -                        ids = ids.concat(command[2]);
 -                        return;
 -                    case commands.UPDATE:
 -                        ids = ids.concat(command[2]);
 -                        return;
 -                    case commands.LINK_TO:
 -                        ids = ids.concat(command[1]);
 -                        return;
 -                    case commands.DELETE:
 -                        ids = _.filter(ids, function (id) { return id != command[1];});
 -                        return;
 -                    case commands.DELETE_ALL:
 -                        ids = [];
 -                        return;
 -                }
 -            } else {
 -                ids.push(command);
 -            }
 -        });
 -        this._super( ids );
 +        value_ = value_ || [];
 +        if (value_.length >= 1 && value_[0] instanceof Array) {
 +            value_ = value_[0][2];
 +        }
 +        this._super(value_);
      },
      get_value: function() {
 -        return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
 +        var tmp = [commands.replace_with(this.get("value"))];
 +        return tmp;
      },
      get_file_url: function (attachment) {
          return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
      },
      read_name_values : function () {
          var self = this;
 -        // select the list of id for a get_name
 -        var values = [];
 -        _.each(this.get('value'), function (val) {
 -            if (typeof val != 'object') {
 -                values.push(val);
 -            }
 -        });
 +        // don't reset know values
 +        var ids = this.get('value');
 +        var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
          // send request for get_name
 -        if (values.length) {
 -            return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
 +        if (_value.length) {
 +            return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
                  _.each(datas, function (data) {
                      data.no_unlink = true;
                      data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
 -
 -                    _.each(self.get('value'), function (val, key) {
 -                        if(val == data.id) {
 -                            self.get('value')[key] = data;
 -                        }
 -                    });
 +                    self.data[data.id] = data;
                  });
 +                return ids;
              });
          } else {
 -            return $.when(this.get('value'));
 +            return $.when(ids);
          }
      },
      render_value: function () {
          var self = this;
 -        this.read_name_values().then(function (datas) {
 -
 -            var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
 +        this.$('.oe_add').css('visibility', this.get('effective_readonly') ? 'hidden': '');
 +        this.read_name_values().then(function (ids) {
 +            var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
              render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
              self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
  
          var self = this;
          var $target = $(event.target);
          if ($target.val() !== '') {
 -
              var filename = $target.val().replace(/.*[\\\/]/,'');
 -
 -            // if the files is currently uploded, don't send again
 -            if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
 +            // don't uplode more of one file in same time
 +            if (self.data[0] && self.data[0].upload ) {
                  return false;
              }
 +            for (var id in this.get('value')) {
 +                // if the files exits, delete the file before upload (if it's a new file)
 +                if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
 +                    self.ds_file.unlink([id]);
 +                }
 +            }
  
              // block UI or not
              if(this.node.attrs.blockui>0) {
                  instance.web.blockUI();
              }
  
 -            // if the files exits for this answer, delete the file before upload
 -            var files = _.filter(this.get('value'), function (file) {
 -                if((file.filename || file.name) == filename) {
 -                    self.ds_file.unlink([file.id]);
 -                    return false;
 -                } else {
 -                    return true;
 -                }
 -            });
 -
              // TODO : unactivate send on wizard and form
  
              // submit file
              this.$('form.oe_form_binary_form').submit();
              this.$(".oe_fileupload").hide();
 -
 -            // add file on result
 -            files.push({
 +            // add file on data result
 +            this.data[0] = {
                  'id': 0,
                  'name': filename,
                  'filename': filename,
                  'url': '',
                  'upload': true
 -            });
 -
 -            this.set({'value': files});
 +            };
          }
      },
      on_file_loaded: function (event, result) {
              instance.web.unblockUI();
          }
  
 -        // TODO : activate send on wizard and form
 -
          if (result.error || !result.id ) {
              this.do_warn( _t('Uploading Error'), result.error);
 -            files = _.filter(files, function (val) { return !val.upload; });
 +            delete this.data[0];
          } else {
 -            for(var i in files){
 -                if(files[i].filename == result.filename && files[i].upload) {
 -                    files[i] = {
 -                        'id': result.id,
 -                        'name': result.name,
 -                        'filename': result.filename,
 -                        'url': this.get_file_url(result)
 -                    };
 -                }
 +            if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
 +                delete this.data[0];
 +                this.data[result.id] = {
 +                    'id': result.id,
 +                    'name': result.name,
 +                    'filename': result.filename,
 +                    'url': this.get_file_url(result)
 +                };
 +            } else {
 +                this.data[result.id] = {
 +                    'id': result.id,
 +                    'name': result.name,
 +                    'filename': result.filename,
 +                    'url': this.get_file_url(result)
 +                };
              }
 +            var values = _.clone(this.get('value'));
 +            values.push(result.id);
 +            this.set({'value': values});
          }
 -
 -        this.set({'value': files});
 -        this.render_value()
 +        this.render_value();
      },
      on_file_delete: function (event) {
          event.stopPropagation();
          var file_id=$(event.target).data("id");
          if (file_id) {
 -            var files=[];
 -            for(var i in this.get('value')){
 -                if(file_id != this.get('value')[i].id){
 -                    files.push(this.get('value')[i]);
 -                }
 -                else if(!this.get('value')[i].no_unlink) {
 -                    this.ds_file.unlink([file_id]);
 -                }
 +            var files = _.filter(this.get('value'), function (id) {return id != file_id;});
 +            if(!this.data[file_id].no_unlink) {
 +                this.ds_file.unlink([file_id]);
              }
              this.set({'value': files});
          }
@@@ -5606,10 -5417,9 +5601,10 @@@ instance.web.form.FieldStatus = instanc
          this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
          this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
          this.set({value: false});
 -        this.selection = [];
 -        this.set("selection", []);
 +        this.selection = {'unfolded': [], 'folded': []};
 +        this.set("selection", {'unfolded': [], 'folded': []});
          this.selection_dm = new instance.web.DropMisordered();
 +        this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
      },
      start: function() {
          this.field_manager.on("view_content_has_changed", this, this.calc_domain);
          });
          this.get_selection();
          if (this.options.clickable) {
 -            this.$el.on('click','li',this.on_click_stage);
 +            this.$el.on('click','li[data-id]',this.on_click_stage);
 +        }
 +        if (this.$el.parent().is('header')) {
 +            this.$el.after('<div class="oe_clear"/>');
          }
          this._super();
      },
      },
      render_value: function() {
          var self = this;
 -        var content = QWeb.render("FieldStatus.content", {widget: self});
 +        var content = QWeb.render("FieldStatus.content", {
 +            'widget': self, 
 +            'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
 +        });
          self.$el.html(content);
 -        var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
 -        var color = colors[self.get('value')];
 -        if (color) {
 -            self.$("oe_active").css("color", color);
 -        }
      },
      calc_domain: function() {
          var d = instance.web.pyeval.eval('domain', this.build_domain());
          var domain = []; //if there is no domain defined, fetch all the records
 -        
 +
          if (d.length) {
              domain = ['|',['id', '=', this.get('value')]].concat(d);
          }
 -        
 +
          if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
              this.set("evaluated_selection_domain", domain);
          }
       */
      get_selection: function() {
          var self = this;
 -        var selection = [];
 +        var selection_unfolded = [];
 +        var selection_folded = [];
 +        var fold_field = this.options.fold_field;
  
          var calculation = _.bind(function() {
              if (this.field.type == "many2one") {
 -                var domain = [];
 -                var ds = new instance.web.DataSetSearch(this, this.field.relation,
 -                    self.build_context(), this.get("evaluated_selection_domain"));
 -                return ds.read_slice(['name'], {}).then(function (records) {
 -                    for(var i = 0; i < records.length; i++) {
 -                        selection.push([records[i].id, records[i].name]);
 -                    }
 -                });
 +                return self.get_distant_fields().then(function (fields) {
 +                    return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
 +                        .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
 +                            var ids = _.pluck(records, 'id');
 +                            return self.dataset.name_get(ids).then(function (records_name) {
 +                                _.each(records, function (record) {
 +                                    var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
 +                                    if (fold_field && record[fold_field] && record.id != self.get('value')) {
 +                                        selection_folded.push([record.id, name]);
 +                                    } else {
 +                                        selection_unfolded.push([record.id, name]);
 +                                    }
 +                                });
 +                            });
 +                        });
 +                    });
              } else {
                  // For field type selection filter values according to
                  // statusbar_visible attribute of the field. For example:
                  for(var i=0; i < select.length; i++) {
                      var key = select[i][0];
                      if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
 -                        selection.push(select[i]);
 +                        selection_unfolded.push(select[i]);
                      }
                  }
                  return $.when();
              }
          }, this);
          this.selection_dm.add(calculation()).then(function () {
 +            var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
              if (! _.isEqual(selection, self.get("selection"))) {
                  self.set("selection", selection);
              }
          });
      },
 +    /*
 +     * :deprecated: this feature will probably be removed with OpenERP v8
 +     */
 +    get_distant_fields: function() {
 +        var self = this;
 +        if (! this.options.fold_field) {
 +            this.distant_fields = {}
 +        }
 +        if (this.distant_fields) {
 +            return $.when(this.distant_fields);
 +        }
 +        return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
 +            self.distant_fields = fields;
 +            return fields;
 +        });
 +    },
      on_click_stage: function (ev) {
          var self = this;
          var $li = $(ev.currentTarget);
 +        var val;
          if (this.field.type == "many2one") {
 -            var val = parseInt($li.data("id"));
 +            val = parseInt($li.data("id"), 10);
          }
          else {
 -            var val = $li.data("id");
 +            val = $li.data("id");
          }
          if (val != self.get('value')) {
              this.view.recursive_save().done(function() {
@@@ -5782,131 -5563,6 +5777,131 @@@ instance.web.form.FieldMonetary = insta
      },
  });
  
 +/*
 +    This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
 +    record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
 +    will be added to the relation.
 +*/
 +instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 +    className: "oe_form_many2many_checkboxes",
 +    init: function() {
 +        this._super.apply(this, arguments);
 +        this.set("value", {});
 +        this.set("records", []);
 +        this.field_manager.on("view_content_has_changed", this, function() {
 +            var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
 +            if (! _.isEqual(domain, this.get("domain"))) {
 +                this.set("domain", domain);
 +            }
 +        });
 +        this.records_orderer = new instance.web.DropMisordered();
 +    },
 +    initialize_field: function() {
 +        instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
 +        this.on("change:domain", this, this.query_records);
 +        this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
 +        this.on("change:records", this, this.render_value);
 +    },
 +    query_records: function() {
 +        var self = this;
 +        var model = new openerp.Model(openerp.session, this.field.relation);
 +        this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
 +            return model.call("name_get", [record_ids] , {"context": self.build_context()});
 +        })).then(function(res) {
 +            self.set("records", res);
 +        });
 +    },
 +    render_value: function() {
 +        this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
 +        var inputs = this.$("input");
 +        inputs.change(_.bind(this.from_dom, this));
 +        if (this.get("effective_readonly"))
 +            inputs.attr("disabled", "true");
 +    },
 +    from_dom: function() {
 +        var new_value = {};
 +        this.$("input").each(function() {
 +            var elem = $(this);
 +            new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
 +        });
 +        if (! _.isEqual(new_value, this.get("value")))
 +            this.internal_set_value(new_value);
 +    },
 +    set_value: function(value) {
 +        value = value || [];
 +        if (value.length >= 1 && value[0] instanceof Array) {
 +            value = value[0][2];
 +        }
 +        var formatted = {};
 +        _.each(value, function(el) {
 +            formatted[JSON.stringify(el)] = true;
 +        });
 +        this._super(formatted);
 +    },
 +    get_value: function() {
 +        var value = _.filter(_.keys(this.get("value")), function(el) {
 +            return this.get("value")[el];
 +        }, this);
 +        value = _.map(value, function(el) {
 +            return JSON.parse(el);
 +        });
 +        return [commands.replace_with(value)];
 +    },
 +});
 +
 +/**
 +    This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
 +    "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
 +    action on the model of the relation and show only the linked records.
 +
 +    Widget options:
 +
 +    * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
 +      to display (or False to take the default one) and the second element is the type of the view. Defaults to
 +      [[false, "tree"], [false, "form"]] .
 +*/
 +instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 +    className: "oe_form_x2many_counter",
 +    init: function() {
 +        this._super.apply(this, arguments);
 +        this.set("value", []);
 +        _.defaults(this.options, {
 +            "views": [[false, "tree"], [false, "form"]],
 +        });
 +    },
 +    render_value: function() {
 +        var text = _.str.sprintf("%d %s", this.val().length, this.string);
 +        this.$().html(QWeb.render("X2ManyCounter", {text: text}));
 +        this.$("a").click(_.bind(this.go_to, this));
 +    },
 +    go_to: function() {
 +        return this.view.recursive_save().then(_.bind(function() {
 +            var val = this.val();
 +            var context = {};
 +            if (this.field.type === "one2many") {
 +                context["default_" + this.field.relation_field] = this.view.datarecord.id;
 +            }
 +            var domain = [["id", "in", val]];
 +            return this.do_action({
 +                type: 'ir.actions.act_window',
 +                name: this.string,
 +                res_model: this.field.relation,
 +                views: this.options.views,
 +                target: 'current',
 +                context: context,
 +                domain: domain,
 +            });
 +        }, this));
 +    },
 +    val: function() {
 +        var value = this.get("value") || [];
 +        if (value.length >= 1 && value[0] instanceof Array) {
 +            value = value[0][2];
 +        }
 +        return value;
 +    }
 +});
 +
  /**
   * Registry of form fields, called by :js:`instance.web.FormView`.
   *
@@@ -5923,7 -5579,6 +5918,7 @@@ instance.web.form.widgets = new instanc
      'date' : 'instance.web.form.FieldDate',
      'datetime' : 'instance.web.form.FieldDatetime',
      'selection' : 'instance.web.form.FieldSelection',
 +    'radio' : 'instance.web.form.FieldRadio',
      'many2one' : 'instance.web.form.FieldMany2One',
      'many2onebutton' : 'instance.web.form.Many2OneButton',
      'many2many' : 'instance.web.form.FieldMany2Many',
      'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
      'statusbar': 'instance.web.form.FieldStatus',
      'monetary': 'instance.web.form.FieldMonetary',
 +    'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
 +    'x2many_counter': 'instance.web.form.X2ManyCounter',
  });
  
  /**
@@@ -5959,6 -5612,6 +5954,6 @@@ instance.web.form.tags = new instance.w
  instance.web.form.custom_widgets = new instance.web.Registry({
  });
  
 -};
 +})();
  
  // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: