[MERGE] forward port of branch 8.0 up to 491372e
authorChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 19:30:40 +0000 (20:30 +0100)
committerChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 19:30:40 +0000 (20:30 +0100)
45 files changed:
1  2 
addons/account/account_invoice.py
addons/account/security/account_security.xml
addons/base_action_rule/base_action_rule.py
addons/crm/crm.py
addons/crm/crm_lead.py
addons/hr/hr.py
addons/mail/mail_mail.py
addons/mail/tests/test_mail_features.py
addons/marketing_campaign/marketing_campaign_view.xml
addons/mass_mailing/models/mass_mailing.py
addons/payment/models/payment_acquirer.py
addons/point_of_sale/point_of_sale.py
addons/point_of_sale/static/src/css/pos.css
addons/point_of_sale/static/src/js/screens.js
addons/product/product.py
addons/project_issue/project_issue.py
addons/purchase/purchase_view.xml
addons/report/static/src/js/qwebactionmanager.js
addons/share/wizard/share_wizard.py
addons/web/static/src/js/view_form.js
addons/web/static/src/js/views.js
addons/web/static/src/xml/base.xml
addons/web_kanban/static/src/js/kanban.js
addons/website/controllers/main.py
addons/website/models/ir_qweb.py
addons/website/models/ir_ui_view.py
addons/website/models/website.py
addons/website/static/src/css/website.css
addons/website/static/src/css/website.sass
addons/website/views/website_templates.xml
addons/website_blog/views/website_blog_templates.xml
addons/website_crm/controllers/main.py
addons/website_forum/controllers/main.py
addons/website_forum/models/forum.py
addons/website_sale/controllers/main.py
doc/conf.py
openerp/addons/base/ir/ir_model.py
openerp/addons/base/ir/ir_qweb.py
openerp/addons/base/ir/ir_ui_view.py
openerp/addons/base/res/res_partner.py
openerp/addons/base/res/res_users.py
openerp/models.py
openerp/service/server.py
openerp/tools/convert.py
openerp/tools/misc.py

@@@ -108,42 -108,41 +108,41 @@@ class account_invoice(models.Model)
          'state', 'currency_id', 'invoice_line.price_subtotal',
          'move_id.line_id.account_id.type',
          'move_id.line_id.amount_residual',
+         # Fixes the fact that move_id.line_id.amount_residual, being not stored and old API, doesn't trigger recomputation
+         'move_id.line_id.reconcile_id',
          'move_id.line_id.amount_residual_currency',
          'move_id.line_id.currency_id',
          'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
      )
+     # An invoice's residual amount is the sum of its unreconciled move lines and,
+     # for partially reconciled move lines, their residual amount divided by the
+     # number of times this reconciliation is used in an invoice (so we split
+     # the residual amount between all invoice)
      def _compute_residual(self):
-         nb_inv_in_partial_rec = max_invoice_id = 0
          self.residual = 0.0
+         # Each partial reconciliation is considered only once for each invoice it appears into,
+         # and its residual amount is divided by this number of invoices
+         partial_reconciliations_done = []
          for line in self.sudo().move_id.line_id:
-             if line.account_id.type in ('receivable', 'payable'):
-                 if line.currency_id == self.currency_id:
-                     self.residual += line.amount_residual_currency
-                 else:
-                     # ahem, shouldn't we use line.currency_id here?
-                     from_currency = line.company_id.currency_id.with_context(date=line.date)
-                     self.residual += from_currency.compute(line.amount_residual, self.currency_id)
-                 # we check if the invoice is partially reconciled and if there
-                 # are other invoices involved in this partial reconciliation
+             if line.account_id.type not in ('receivable', 'payable'):
+                 continue
+             if line.reconcile_partial_id and line.reconcile_partial_id.id in partial_reconciliations_done:
+                 continue
+             # Get the correct line residual amount
+             if line.currency_id == self.currency_id:
+                 line_amount = line.currency_id and line.amount_residual_currency or line.amount_residual
+             else:
+                 from_currency = line.company_id.currency_id.with_context(date=line.date)
+                 line_amount = from_currency.compute(line.amount_residual, self.currency_id)
+             # For partially reconciled lines, split the residual amount
+             if line.reconcile_partial_id:
+                 partial_reconciliation_invoices = set()
                  for pline in line.reconcile_partial_id.line_partial_ids:
                      if pline.invoice and self.type == pline.invoice.type:
-                         nb_inv_in_partial_rec += 1
-                         # store the max invoice id as for this invoice we will
-                         # make a balance instead of a simple division
-                         max_invoice_id = max(max_invoice_id, pline.invoice.id)
-         if nb_inv_in_partial_rec:
-             # if there are several invoices in a partial reconciliation, we
-             # split the residual by the number of invoices to have a sum of
-             # residual amounts that matches the partner balance
-             new_value = self.currency_id.round(self.residual / nb_inv_in_partial_rec)
-             if self.id == max_invoice_id:
-                 # if it's the last the invoice of the bunch of invoices
-                 # partially reconciled together, we make a balance to avoid
-                 # rounding errors
-                 self.residual = self.residual - ((nb_inv_in_partial_rec - 1) * new_value)
-             else:
-                 self.residual = new_value
-         # prevent the residual amount on the invoice to be less than 0
+                         partial_reconciliation_invoices.update([pline.invoice.id])
+                 line_amount = self.currency_id.round(line_amount / len(partial_reconciliation_invoices))
+                 partial_reconciliations_done.append(line.reconcile_partial_id.id)
+             self.residual += line_amount
          self.residual = max(self.residual, 0.0)
  
      @api.one
                      if line.value == 'fixed':
                          total_fixed += line.value_amount
                      if line.value == 'procent':
 -                        total_percent += line.value_amount
 +                        total_percent += (line.value_amount/100.0)
                  total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
                  if (total_fixed + total_percent) > 100:
                      raise except_orm(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
          <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
      </record>
  
 -    <record id="analytic_journal_comp_rule" model="ir.rule">
 -        <field name="name">Analytic journal multi-company</field>
 -        <field name="model_id" ref="model_account_analytic_journal"/>
 -        <field name="global" eval="True"/>
 -        <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
 -    </record>
 -
      <record id="period_comp_rule" model="ir.rule">
          <field name="name">Period multi-company</field>
          <field name="model_id" ref="model_account_period"/>
          <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
      </record>
  
+     <record id="analytic_entry_analysis_comp_rule" model="ir.rule">
+         <field name="name">Analytic Entries Analysis multi-company</field>
+         <field name="model_id" ref="model_analytic_entries_report"/>
+         <field name="global" eval="True"/>
+         <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
+     </record>
      <record id="account_fiscal_position_comp_rule" model="ir.rule">
          <field name="name">Account fiscal Mapping company rule</field>
          <field name="model_id" ref="model_account_fiscal_position"/>
@@@ -88,16 -88,14 +88,16 @@@ class base_action_rule(osv.osv)
          'server_action_ids': fields.many2many('ir.actions.server', string='Server Actions',
              domain="[('model_id', '=', model_id)]",
              help="Examples: email reminders, call object service, etc."),
 -        'filter_pre_id': fields.many2one('ir.filters', string='Before Update Filter',
 -            ondelete='restrict',
 -            domain="[('model_id', '=', model_id.model)]",
 +        'filter_pre_id': fields.many2one(
 +            'ir.filters', string='Before Update Filter',
 +            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='Filter',
 -            ondelete='restrict',
 -            domain="[('model_id', '=', model_id.model)]",
 +        'filter_pre_domain': fields.char(string='Before Update Domain', help="If present, this condition must be satisfied before the update of the record."),
 +        'filter_id': fields.many2one(
 +            'ir.filters', string='Filter',
 +            ondelete='restrict', domain="[('model_id', '=', model_id.model)]",
              help="If present, this condition must be satisfied before executing the action rule."),
 +        'filter_domain': fields.char(string='Domain', help="If present, this condition must be satisfied before executing the action rule."),
          'last_run': fields.datetime('Last Run', readonly=1, copy=False),
      }
  
              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[action_filter.model_id]
 -            domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
 -            ctx = dict(context or {})
 -            ctx.update(eval(action_filter.context))
 -            record_ids = model.search(cr, uid, domain, context=ctx)
 +    def onchange_filter_pre_id(self, cr, uid, ids, filter_pre_id, context=None):
 +        ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_pre_id, context=context)
 +        return {'value': {'filter_pre_domain': ir_filter.domain}}
 +
 +    def onchange_filter_id(self, cr, uid, ids, filter_id, context=None):
 +        ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
 +        return {'value': {'filter_domain': ir_filter.domain}}
 +
 +    def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None):
 +        """ Filter the list record_ids that satisfy the domain or the action filter. """
 +        if record_ids and (domain is not False or action_filter):
 +            if domain is not False:
 +                new_domain = [('id', 'in', record_ids)] + eval(domain)
 +                ctx = context
 +            elif action_filter:
 +                assert action.model == action_filter.model_id, "Filter model different from action rule model"
 +                new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain)
 +                ctx = dict(context or {})
 +                ctx.update(eval(action_filter.context))
 +            record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx)
          return record_ids
  
      def _process(self, cr, uid, action, record_ids, context=None):
          """ process the given action on the records """
          model = self.pool[action.model_id.model]
 -
          # modify records
          values = {}
-         if 'date_action_last' in model._all_columns:
+         if 'date_action_last' in model._fields:
              values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
-         if action.act_user_id and 'user_id' in model._all_columns:
+         if action.act_user_id and 'user_id' in model._fields:
              values['user_id'] = action.act_user_id.id
          if values:
              model.write(cr, uid, record_ids, values, context=context)
  
                  # check postconditions, and execute actions on the records that satisfy them
                  for action in action_model.browse(cr, uid, action_ids, context=context):
 -                    if action_model._filter(cr, uid, action, action.filter_id, [new_id], context=context):
 +                    if action_model._filter(cr, uid, action, action.filter_id, [new_id], domain=action.filter_domain, context=context):
                          action_model._process(cr, uid, action, [new_id], context=context)
                  return new_id
  
                  # check preconditions
                  pre_ids = {}
                  for action in actions:
 -                    pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, context=context)
 +                    pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, domain=action.filter_pre_domain, context=context)
  
                  # call original method
                  write.origin(self, cr, uid, ids, vals, context=context, **kwargs)
  
                  # check postconditions, and execute actions on the records that satisfy them
                  for action in actions:
 -                    post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], context=context)
 +                    post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], domain=action.filter_domain, context=context)
                      if post_ids:
                          action_model._process(cr, uid, action, post_ids, context=context)
                  return True
              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'):
                  # monkey-patch methods create and write
                  model_obj._patch_method('create', make_create())
                  model_obj._patch_method('write', make_write())
                  last_run = get_datetime(action.last_run)
              else:
                  last_run = datetime.utcfromtimestamp(0)
 -
              # retrieve all the records that satisfy the action's condition
              model = self.pool[action.model_id.model]
              domain = []
              ctx = dict(context)
 -            if action.filter_id:
 +            if action.filter_domain is not False:
 +                domain = eval(action.filter_domain)
 +            elif action.filter_id:
                  domain = eval(action.filter_id.domain)
                  ctx.update(eval(action.filter_id.context))
                  if 'lang' not in ctx:
  
              # determine when action should occur for the records
              date_field = action.trg_date_id.name
-             if date_field == 'date_action_last' and 'create_date' in model._all_columns:
+             if date_field == 'date_action_last' and 'create_date' in model._fields:
                  get_record_dt = lambda record: record[date_field] or record.create_date
              else:
                  get_record_dt = lambda record: record[date_field]
diff --combined addons/crm/crm.py
@@@ -23,10 -23,11 +23,10 @@@ from openerp.osv import osv, field
  from openerp.http import request
  
  AVAILABLE_PRIORITIES = [
 -    ('0', 'Very Low'),
 +    ('0', 'Normal'),
      ('1', 'Low'),
 -    ('2', 'Normal'),
 -    ('3', 'High'),
 -    ('4', 'Very High'),
 +    ('2', 'High'),
 +    ('3', 'Very High'),
  ]
  
  
@@@ -51,7 -52,7 +51,7 @@@ class crm_tracking_campaign(osv.Model)
      _rec_name = "name"
      _columns = {
          'name': fields.char('Campaign Name', required=True, translate=True),
 -        'section_id': fields.many2one('crm.case.section', 'Sales Team'),
 +        'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'),
      }
  
  
@@@ -69,7 -70,7 +69,7 @@@ class crm_tracking_mixin(osv.AbstractMo
      _name = 'crm.tracking.mixin'
  
      _columns = {
 -        'campaign_id': fields.many2one('crm.tracking.campaign', 'Campaign',  # old domain ="['|',('section_id','=',section_id),('section_id','=',False)]"
 +        'campaign_id': fields.many2one('crm.tracking.campaign', 'Campaign',  # old domain ="['|',('team_id','=',team_id),('team_id','=',False)]"
                                         help="This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special"),
          'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Ex: Search Engine, another domain, or name of email list"),
          'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad"),
          return [('utm_campaign', 'campaign_id'), ('utm_source', 'source_id'), ('utm_medium', 'medium_id')]
  
      def tracking_get_values(self, cr, uid, vals, context=None):
-         for key, field in self.tracking_fields():
-             column = self._all_columns[field].column
-             value = vals.get(field) or (request and request.httprequest.cookies.get(key))  # params.get should be always in session by the dispatch from ir_http
-             if column._type in ['many2one'] and isinstance(value, basestring):  # if we receive a string for a many2one, we search / create the id
+         for key, fname in self.tracking_fields():
+             field = self._fields[fname]
+             value = vals.get(fname) or (request and request.httprequest.cookies.get(key))  # params.get should be always in session by the dispatch from ir_http
+             if field.type == 'many2one' and isinstance(value, basestring):
+                 # if we receive a string for a many2one, we search/create the id
                  if value:
-                     Model = self.pool[column._obj]
+                     Model = self.pool[field.comodel_name]
                      rel_id = Model.name_search(cr, uid, value, context=context)
                      if rel_id:
                          rel_id = rel_id[0][0]
                      else:
                          rel_id = Model.create(cr, uid, {'name': value}, context=context)
-                 vals[field] = rel_id
-             # Here the code for others cases that many2one
+                 vals[fname] = rel_id
              else:
-                 vals[field] = value
+                 # Here the code for others cases that many2one
+                 vals[fname] = value
          return vals
  
      def _get_default_track(self, cr, uid, field, context=None):
      }
  
  
 -class crm_case_stage(osv.osv):
 +class crm_stage(osv.Model):
      """ Model for case stages. This models the main stages of a document
          management flow. Main CRM objects (leads, opportunities, project
          issues, ...) will now use only stages, instead of state and stages.
          Stages are for example used to display the kanban view of records.
      """
 -    _name = "crm.case.stage"
 +    _name = "crm.stage"
      _description = "Stage of case"
      _rec_name = 'name'
      _order = "sequence"
          'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"),
          'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
          'requirements': fields.text('Requirements'),
 -        'section_ids': fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
 -                                        help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
 +        'team_ids': fields.many2many('crm.team', 'crm_team_stage_rel', 'stage_id', 'team_id', string='Teams',
 +                        help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
          'case_default': fields.boolean('Default to New Sales Team',
 -                                       help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
 +                        help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
          'fold': fields.boolean('Folded in Kanban View',
                                 help='This stage is folded in the kanban view when'
                                 'there are no records in that stage to display.'),
          'case_default': True,
      }
  
 -
 -class crm_case_categ(osv.osv):
 -    """ Category of Case """
 -    _name = "crm.case.categ"
 -    _description = "Category of Case"
 -    _columns = {
 -        'name': fields.char('Name', required=True, translate=True),
 -        'section_id': fields.many2one('crm.case.section', 'Sales Team'),
 -        'object_id': fields.many2one('ir.model', 'Object Name'),
 -    }
 -
 -    def _find_object_id(self, cr, uid, context=None):
 -        """Finds id for case object"""
 -        context = context or {}
 -        object_id = context.get('object_id', False)
 -        ids = self.pool.get('ir.model').search(cr, uid, ['|', ('id', '=', object_id), ('model', '=', context.get('object_name', False))])
 -        return ids and ids[0] or False
 -    _defaults = {
 -        'object_id': _find_object_id
 -    }
 -
 -
 -class crm_payment_mode(osv.osv):
 -    """ Payment Mode for Fund """
 -    _name = "crm.payment.mode"
 -    _description = "CRM Payment Mode"
 -    _columns = {
 -        'name': fields.char('Name', required=True),
 -        'section_id': fields.many2one('crm.case.section', 'Sales Team'),
 -    }
 -
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --combined addons/crm/crm_lead.py
@@@ -37,7 -37,7 +37,7 @@@ CRM_LEAD_FIELDS_TO_MERGE = ['name'
      'campaign_id',
      'company_id',
      'country_id',
 -    'section_id',
 +    'team_id',
      'state_id',
      'stage_id',
      'medium_id',
@@@ -86,34 -86,36 +86,34 @@@ class crm_lead(format_address, osv.osv)
      def get_empty_list_help(self, cr, uid, help, context=None):
          context = dict(context or {})
          if context.get('default_type') == 'lead':
 -            context['empty_list_help_model'] = 'crm.case.section'
 -            context['empty_list_help_id'] = context.get('default_section_id')
 +            context['empty_list_help_model'] = 'crm.team'
 +            context['empty_list_help_id'] = context.get('default_team_id')
          context['empty_list_help_document_name'] = _("leads")
          return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
  
 -    def _get_default_section_id(self, cr, uid, user_id=False, context=None):
 -        """ Gives default section by checking if present in the context """
 -        section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
 -        if not section_id:
 -            section_id = self.pool.get('res.users').browse(cr, uid, user_id or uid, context).default_section_id.id or False
 -        return section_id
 +    def _get_default_team_id(self, cr, uid, user_id=False, context=None):
 +        """ Gives default team by checking if present in the context """
 +        team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False
 +        return team_id
  
      def _get_default_stage_id(self, cr, uid, context=None):
          """ Gives default stage_id """
 -        section_id = self._get_default_section_id(cr, uid, context=context)
 -        return self.stage_find(cr, uid, [], section_id, [('fold', '=', False)], context=context)
 +        team_id = self._get_default_team_id(cr, uid, context=context)
 +        return self.stage_find(cr, uid, [], team_id, [('fold', '=', False)], context=context)
  
 -    def _resolve_section_id_from_context(self, cr, uid, context=None):
 -        """ Returns ID of section based on the value of 'section_id'
 +    def _resolve_team_id_from_context(self, cr, uid, context=None):
 +        """ Returns ID of team based on the value of 'team_id'
              context key, or None if it cannot be resolved to a single
              Sales Team.
          """
          if context is None:
              context = {}
 -        if type(context.get('default_section_id')) in (int, long):
 -            return context.get('default_section_id')
 -        if isinstance(context.get('default_section_id'), basestring):
 -            section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
 -            if len(section_ids) == 1:
 -                return int(section_ids[0][0])
 +        if type(context.get('default_team_id')) in (int, long):
 +            return context.get('default_team_id')
 +        if isinstance(context.get('default_team_id'), basestring):
 +            team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context)
 +            if len(team_ids) == 1:
 +                return int(team_ids[0][0])
          return None
  
      def _resolve_type_from_context(self, cr, uid, context=None):
  
      def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
          access_rights_uid = access_rights_uid or uid
 -        stage_obj = self.pool.get('crm.case.stage')
 +        stage_obj = self.pool.get('crm.stage')
          order = stage_obj._order
          # lame hack to allow reverting search, should just work in the trivial case
          if read_group_order == 'stage_id desc':
              order = "%s desc" % order
 -        # retrieve section_id from the context and write the domain
 +        # retrieve team_id from the context and write the domain
          # - ('id', 'in', 'ids'): add columns that should be present
          # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
 -        # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
 +        # - OR ('team_ids', '=', team_id), ('fold', '=', False) if team_id: add team columns that are not folded
          search_domain = []
 -        section_id = self._resolve_section_id_from_context(cr, uid, context=context)
 -        if section_id:
 -            search_domain += ['|', ('section_ids', '=', section_id)]
 +        team_id = self._resolve_team_id_from_context(cr, uid, context=context)
 +        if team_id:
 +            search_domain += ['|', ('team_ids', '=', team_id)]
              search_domain += [('id', 'in', ids)]
          else:
              search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
              select=True, help="Linked partner (optional). Usually created when converting the lead."),
  
          'id': fields.integer('ID', readonly=True),
 -        'name': fields.char('Subject', required=True, select=1),
 +        'name': fields.char('Opportunity', required=True, select=1),
          'active': fields.boolean('Active', required=False),
          'date_action_last': fields.datetime('Last Action', readonly=1),
          'date_action_next': fields.datetime('Next Action', readonly=1),
          'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
 -        'section_id': fields.many2one('crm.case.section', 'Sales Team',
 +        'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id',
                          select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
          'create_date': fields.datetime('Creation Date', readonly=True),
          'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
          'description': fields.text('Notes'),
          'write_date': fields.datetime('Update Date', readonly=True),
 -        'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Tags', \
 -            domain="['|', ('section_id', '=', section_id), ('section_id', '=', False), ('object_id.model', '=', 'crm.lead')]", help="Classify and analyze your lead/opportunity categories like: Training, Service"),
 +        'tag_ids': fields.many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', 'Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service"),
          'contact_name': fields.char('Contact Name', size=64),
          'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
          'opt_out': fields.boolean('Opt-Out', oldname='optout',
          'type': fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', select=True, help="Type is used to separate Leads and Opportunities"),
          'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
          'date_closed': fields.datetime('Closed', readonly=True, copy=False),
 -        'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange', select=True,
 -                        domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
 +        'stage_id': fields.many2one('crm.stage', 'Stage', track_visibility='onchange', select=True,
 +                        domain="['&', ('team_ids', '=', team_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
          'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
          'referred': fields.char('Referred By'),
          'date_open': fields.datetime('Assigned', readonly=True),
          'function': fields.char('Function'),
          'title': fields.many2one('res.partner.title', 'Title'),
          'company_id': fields.many2one('res.company', 'Company', select=1),
 -        'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
 -                            domain="[('section_id','=',section_id)]"),
          'planned_cost': fields.float('Planned Costs'),
          'meeting_count': fields.function(_meeting_count, string='# Meetings', type='integer'),
      }
          'type': 'lead',
          'user_id': lambda s, cr, uid, c: uid,
          'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
 -        'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, context=c),
 +        'team_id': lambda s, cr, uid, c: s._get_default_team_id(cr, uid, context=c),
          'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
 -        'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
 +        'priority': lambda *a: crm.AVAILABLE_PRIORITIES[0][0],
          'color': 0,
          'date_last_stage_update': fields.datetime.now,
      }
      def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
          if not stage_id:
              return {'value': {}}
 -        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
 +        stage = self.pool.get('crm.stage').browse(cr, uid, stage_id, context=context)
          if not stage.on_change:
              return {'value': {}}
          vals = {'probability': stage.probability}
          return {'value': values}
  
      def on_change_user(self, cr, uid, ids, user_id, context=None):
 -        """ When changing the user, also set a section_id or restrict section id
 +        """ When changing the user, also set a team_id or restrict team id
              to the ones user_id is member of. """
 -        section_id = self._get_default_section_id(cr, uid, user_id=user_id, context=context) or False
 -        if user_id and not section_id:
 -            section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
 -            if section_ids:
 -                section_id = section_ids[0]
 -        return {'value': {'section_id': section_id}}
 -
 -    def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
 +        team_id = self._get_default_team_id(cr, uid, context=context)
 +        if user_id and not team_id:
 +            team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
 +            if team_ids:
 +                team_id = team_ids[0]
 +        return {'value': {'team_id': team_id}}
 +
 +    def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None):
          """ Override of the base.stage method
              Parameter of the stage search taken from the lead:
              - type: stage type must be the same or 'both'
 -            - section_id: if set, stages must belong to this section or
 +            - team_id: if set, stages must belong to this team or
                be a default stage; if not set, stages must be default
                stages
          """
              context = {}
          # check whether we should try to add a condition on type
          avoid_add_type_term = any([term for term in domain if len(term) == 3 if term[0] == 'type'])
 -        # collect all section_ids
 -        section_ids = set()
 +        # collect all team_ids
 +        team_ids = set()
          types = ['both']
          if not cases and context.get('default_type'):
              ctx_type = context.get('default_type')
              types += [ctx_type]
 -        if section_id:
 -            section_ids.add(section_id)
 +        if team_id:
 +            team_ids.add(team_id)
          for lead in cases:
 -            if lead.section_id:
 -                section_ids.add(lead.section_id.id)
 +            if lead.team_id:
 +                team_ids.add(lead.team_id.id)
              if lead.type not in types:
                  types.append(lead.type)
 -        # OR all section_ids and OR with case_default
 +        # OR all team_ids and OR with case_default
          search_domain = []
 -        if section_ids:
 -            search_domain += [('|')] * len(section_ids)
 -            for section_id in section_ids:
 -                search_domain.append(('section_ids', '=', section_id))
 +        if team_ids:
 +            search_domain += [('|')] * len(team_ids)
 +            for team_id in team_ids:
 +                search_domain.append(('team_ids', '=', team_id))
          search_domain.append(('case_default', '=', True))
          # AND with cases types
          if not avoid_add_type_term:
          # AND with the domain in parameter
          search_domain += list(domain)
          # perform search, return the first found
 -        stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
 +        stage_ids = self.pool.get('crm.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
          if stage_ids:
              return stage_ids[0]
          return False
          """
          stages_leads = {}
          for lead in self.browse(cr, uid, ids, context=context):
 -            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
 +            stage_id = self.stage_find(cr, uid, [lead], lead.team_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
              if stage_id:
                  if stages_leads.get(stage_id):
                      stages_leads[stage_id].append(lead.id)
          """
          stages_leads = {}
          for lead in self.browse(cr, uid, ids, context=context):
 -            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
 +            stage_id = self.stage_find(cr, uid, [lead], lead.team_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
              if stage_id:
                  if stages_leads.get(stage_id):
                      stages_leads[stage_id].append(lead.id)
          """ Escalates case to parent level """
          for case in self.browse(cr, uid, ids, context=context):
              data = {'active': True}
 -            if case.section_id.parent_id:
 -                data['section_id'] = case.section_id.parent_id.id
 -                if case.section_id.parent_id.change_responsible:
 -                    if case.section_id.parent_id.user_id:
 -                        data['user_id'] = case.section_id.parent_id.user_id.id
 +            if case.team_id.parent_id:
 +                data['team_id'] = case.team_id.parent_id.id
 +                if case.team_id.parent_id.change_responsible:
 +                    if case.team_id.parent_id.user_id:
 +                        data['user_id'] = case.team_id.parent_id.user_id.id
              else:
                  raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
              self.write(cr, uid, [case.id], data, context=context)
          # Process the fields' values
          data = {}
          for field_name in fields:
-             field_info = self._all_columns.get(field_name)
-             if field_info is None:
+             field = self._fields.get(field_name)
+             if field is None:
                  continue
-             field = field_info.column
-             if field._type in ('many2many', 'one2many'):
+             if field.type in ('many2many', 'one2many'):
                  continue
-             elif field._type == 'many2one':
+             elif field.type == 'many2one':
                  data[field_name] = _get_first_not_null_id(field_name)  # !!
-             elif field._type == 'text':
+             elif field.type == 'text':
                  data[field_name] = _concat_all(field_name)  #not lost
              else:
                  data[field_name] = _get_first_not_null(field_name)  #not lost
              body.append("%s\n" % (title))
  
          for field_name in fields:
-             field_info = self._all_columns.get(field_name)
-             if field_info is None:
+             field = self._fields.get(field_name)
+             if field is None:
                  continue
-             field = field_info.column
              value = ''
  
-             if field._type == 'selection':
-                 if hasattr(field.selection, '__call__'):
+             if field.type == 'selection':
+                 if callable(field.selection):
                      key = field.selection(self, cr, uid, context=context)
                  else:
                      key = field.selection
                  value = dict(key).get(lead[field_name], lead[field_name])
-             elif field._type == 'many2one':
+             elif field.type == 'many2one':
                  if lead[field_name]:
                      value = lead[field_name].name_get()[0][1]
-             elif field._type == 'many2many':
+             elif field.type == 'many2many':
                  if lead[field_name]:
                      for val in lead[field_name]:
                          field_value = val.name_get()[0][1]
          self._merge_opportunity_attachments(cr, uid, highest, opportunities, context=context)
          self._merge_opportunity_phonecalls(cr, uid, highest, opportunities, context=context)
  
 -    def merge_opportunity(self, cr, uid, ids, user_id=False, section_id=False, context=None):
 +    def merge_opportunity(self, cr, uid, ids, user_id=False, team_id=False, context=None):
          """
          Different cases of merge:
          - merge leads together = 1 new lead
  
          if user_id:
              merged_data['user_id'] = user_id
 -        if section_id:
 -            merged_data['section_id'] = section_id
 +        if team_id:
 +            merged_data['team_id'] = team_id
  
          # Merge notifications about loss of information
          opportunities = [highest]
          self.merge_dependences(cr, uid, highest.id, tail_opportunities, context=context)
  
          # Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
 -        if merged_data.get('section_id'):
 -            section_stage_ids = self.pool.get('crm.case.stage').search(cr, uid, [('section_ids', 'in', merged_data['section_id']), ('type', '=', merged_data.get('type'))], order='sequence', context=context)
 -            if merged_data.get('stage_id') not in section_stage_ids:
 -                merged_data['stage_id'] = section_stage_ids and section_stage_ids[0] or False
 +        if merged_data.get('team_id'):
 +            team_stage_ids = self.pool.get('crm.stage').search(cr, uid, [('team_ids', 'in', merged_data['team_id']), ('type', '=', merged_data.get('type'))], order='sequence', context=context)
 +            if merged_data.get('stage_id') not in team_stage_ids:
 +                merged_data['stage_id'] = team_stage_ids and team_stage_ids[0] or False
          # Write merged data into first opportunity
          self.write(cr, uid, [highest.id], merged_data, context=context)
          # Delete tail opportunities 
  
          return highest.id
  
 -    def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
 -        crm_stage = self.pool.get('crm.case.stage')
 +    def _convert_opportunity_data(self, cr, uid, lead, customer, team_id=False, context=None):
 +        crm_stage = self.pool.get('crm.stage')
          contact_id = False
          if customer:
              contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
 -        if not section_id:
 -            section_id = lead.section_id and lead.section_id.id or False
 +        if not team_id:
 +            team_id = lead.team_id and lead.team_id.id or False
          val = {
              'planned_revenue': lead.planned_revenue,
              'probability': lead.probability,
              'phone': customer and customer.phone or lead.phone,
          }
          if not lead.stage_id or lead.stage_id.type=='lead':
 -            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('type', 'in', ('opportunity', 'both'))], context=context)
 +            val['stage_id'] = self.stage_find(cr, uid, [lead], team_id, [('type', 'in', ('opportunity', 'both'))], context=context)
          return val
  
 -    def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
 +    def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, team_id=False, context=None):
          customer = False
          if partner_id:
              partner = self.pool.get('res.partner')
              # TDE: was if lead.state in ('done', 'cancel'):
              if lead.probability == 100 or (lead.probability == 0 and lead.stage_id.fold):
                  continue
 -            vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
 +            vals = self._convert_opportunity_data(cr, uid, lead, customer, team_id, context=context)
              self.write(cr, uid, [lead.id], vals, context=context)
  
 -        if user_ids or section_id:
 -            self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
 +        if user_ids or team_id:
 +            self.allocate_salesman(cr, uid, ids, user_ids, team_id, context=context)
  
          return True
  
          vals = {'name': name,
              'user_id': lead.user_id.id,
              'comment': lead.description,
 -            'section_id': lead.section_id.id or False,
 +            'team_id': lead.team_id.id or False,
              'parent_id': parent_id,
              'phone': lead.phone,
              'mobile': lead.mobile,
                  continue
              if not partner_id and action == 'create':
                  partner_id = self._create_lead_partner(cr, uid, lead, context)
 -                self.pool['res.partner'].write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
 +                self.pool['res.partner'].write(cr, uid, partner_id, {'team_id': lead.team_id and lead.team_id.id or False})
              if partner_id:
                  lead.write({'partner_id': partner_id}, context=context)
              partner_ids[lead.id] = partner_id
          for lead_id in ids:
              value = {}
              if team_id:
 -                value['section_id'] = team_id
 +                value['team_id'] = team_id
              if user_ids:
                  value['user_id'] = user_ids[index]
                  # Cycle through user_ids
                  self.write(cr, uid, [lead_id], value, context=context)
          return True
  
 -    def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
 +    def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, team_id=False, categ_id=False, action='schedule', context=None):
          """
          :param string action: ('schedule','Schedule a call'), ('log','Log a call')
          """
              except ValueError:
                  pass
          for lead in self.browse(cr, uid, ids, context=context):
 -            if not section_id:
 -                section_id = lead.section_id and lead.section_id.id or False
 +            if not team_id:
 +                team_id = lead.team_id and lead.team_id.id or False
              if not user_id:
                  user_id = lead.user_id and lead.user_id.id or False
              vals = {
                  'categ_id': categ_id or False,
                  'description': desc or '',
                  'date': schedule_time,
 -                'section_id': section_id or False,
 +                'team_id': team_id or False,
                  'partner_id': lead.partner_id and lead.partner_id.id or False,
                  'partner_phone': phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
                  'partner_mobile': lead.partner_id and lead.partner_id.mobile or False,
              'default_opportunity_id': lead.type == 'opportunity' and lead.id or False,
              'default_partner_id': lead.partner_id and lead.partner_id.id or False,
              'default_partner_ids': partner_ids,
 -            'default_section_id': lead.section_id and lead.section_id.id or False,
 +            'default_team_id': lead.team_id and lead.team_id.id or False,
              'default_name': lead.name,
          }
          return res
          context = dict(context or {})
          if vals.get('type') and not context.get('default_type'):
              context['default_type'] = vals.get('type')
 -        if vals.get('section_id') and not context.get('default_section_id'):
 -            context['default_section_id'] = vals.get('section_id')
 +        if vals.get('team_id') and not context.get('default_team_id'):
 +            context['default_team_id'] = vals.get('team_id')
          if vals.get('user_id'):
              vals['date_open'] = fields.datetime.now()
  
          lead = self.browse(cr, uid, id, context=context)
          local_context = dict(context)
          local_context.setdefault('default_type', lead.type)
 -        local_context.setdefault('default_section_id', lead.section_id.id)
 +        local_context.setdefault('default_team_id', lead.team_id.id)
          if lead.type == 'opportunity':
              default['date_open'] = fields.datetime.now()
          else:
  
      def get_empty_list_help(self, cr, uid, help, context=None):
          context = dict(context or {})
 -        context['empty_list_help_model'] = 'crm.case.section'
 -        context['empty_list_help_id'] = context.get('default_section_id', None)
 +        context['empty_list_help_model'] = 'crm.team'
 +        context['empty_list_help_id'] = context.get('default_team_id', None)
          context['empty_list_help_document_name'] = _("opportunity")
          if context.get('default_type') == 'lead':
              context['empty_list_help_document_name'] = _("lead")
      def message_get_reply_to(self, cr, uid, ids, context=None):
          """ Override to get the reply_to of the parent project. """
          leads = self.browse(cr, SUPERUSER_ID, ids, context=context)
 -        section_ids = set([lead.section_id.id for lead in leads if lead.section_id])
 -        aliases = self.pool['crm.case.section'].message_get_reply_to(cr, uid, list(section_ids), context=context)
 -        return dict((lead.id, aliases.get(lead.section_id and lead.section_id.id or 0, False)) for lead in leads)
 +        team_ids = set([lead.team_id.id for lead in leads if lead.team_id])
 +        aliases = self.pool['crm.team'].message_get_reply_to(cr, uid, list(team_ids), context=context)
 +        return dict((lead.id, aliases.get(lead.team_id and lead.team_id.id or 0, False)) for lead in leads)
  
      def get_formview_id(self, cr, uid, id, context=None):
          obj = self.browse(cr, uid, id, context=context)
                      break
          return res
  
 +class crm_lead_tag(osv.Model):
 +    _name = "crm.lead.tag"
 +    _description = "Category of lead"
 +    _columns = {
 +        'name': fields.char('Name', required=True, translate=True),
 +        'team_id': fields.many2one('crm.team', 'Sales Team'),
 +    }
 +
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --combined addons/hr/hr.py
@@@ -2,7 -2,7 +2,7 @@@
  ##############################################################################
  #
  #    OpenERP, Open Source Management Solution
 -#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
 +#    Copyright (C) 2004-Today OpenERP S.A. (<http://openerp.com>).
  #
  #    This program is free software: you can redistribute it and/or modify
  #    it under the terms of the GNU Affero General Public License as
@@@ -72,6 -72,7 +72,6 @@@ class hr_employee_category(osv.Model)
          (_check_recursion, 'Error! You cannot create recursive Categories.', ['parent_id'])
      ]
  
 -
  class hr_job(osv.Model):
  
      def _get_nbr_employees(self, cr, uid, ids, name, args, context=None):
@@@ -93,7 -94,7 +93,7 @@@
  
      _name = "hr.job"
      _description = "Job Position"
 -    _inherit = ['mail.thread', 'ir.needaction_mixin']
 +    _inherit = ['mail.thread']
      _columns = {
          'name': fields.char('Job Name', required=True, select=True),
          'expected_employees': fields.function(_get_nbr_employees, string='Total Forecasted Employees',
          'requirements': fields.text('Requirements'),
          'department_id': fields.many2one('hr.department', 'Department'),
          'company_id': fields.many2one('res.company', 'Company'),
 -        'state': fields.selection([('open', 'Recruitment Closed'), ('recruit', 'Recruitment in Progress')],
 +        'state': fields.selection([('recruit', 'Recruitment in Progress'), ('open', 'Recruitment Closed')],
                                    string='Status', readonly=True, required=True,
                                    track_visibility='always', copy=False,
 -                                  help="By default 'Closed', set it to 'In Recruitment' if recruitment process is going on for this job position."),
 +                                  help="Set whether the recruitment process is open or closed for this job position."),
          'write_date': fields.datetime('Update Date', readonly=True),
      }
  
      _defaults = {
          'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.job', context=ctx),
 -        'state': 'open',
 +        'state': 'recruit',
 +        'no_of_recruitment' : 1,
      }
  
      _sql_constraints = [
          ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
 -        ('hired_employee_check', "CHECK ( no_of_hired_employee <= no_of_recruitment )", "Number of hired employee must be less than expected number of employee in recruitment."),
 +        
      ]
  
      def set_recruit(self, cr, uid, ids, context=None):
@@@ -281,11 -281,6 +281,11 @@@ class hr_employee(osv.osv)
          if context.get("mail_broadcast"):
              context['mail_create_nolog'] = True
  
 +        if data.get('user_id', False) == SUPERUSER_ID and data.get('name',False) == 'Administrator':
 +            user_name = self.pool.get('res.users').browse(cr, uid, data.get('user_id'), context=context).name
 +            if data['name'] != user_name:
 +                data['name'] = user_name
 +
          employee_id = super(hr_employee, self).create(cr, uid, data, context=context)
  
          if context.get("mail_broadcast"):
          if auto_follow_fields is None:
              auto_follow_fields = ['user_id']
          user_field_lst = []
-         for name, column_info in self._all_columns.items():
-             if name in auto_follow_fields and name in updated_fields and column_info.column._obj == 'res.users':
+         for name, field in self._fields.items():
+             if name in auto_follow_fields and name in updated_fields and field.comodel_name == 'res.users':
                  user_field_lst.append(name)
          return user_field_lst
  
  
  
  class hr_department(osv.osv):
 +    _name = "hr.department"
 +    _description = "HR Department"
 +    _inherit = ['mail.thread', 'ir.needaction_mixin']
  
      def _dept_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
          res = self.name_get(cr, uid, ids, context=context)
          return dict(res)
  
 -    _name = "hr.department"
      _columns = {
          'name': fields.char('Department Name', required=True),
          'complete_name': fields.function(_dept_name_get_fnc, type="char", string='Name'),
          'company_id': fields.many2one('res.company', 'Company', select=True, required=False),
          'parent_id': fields.many2one('hr.department', 'Parent Department', select=True),
          'child_ids': fields.one2many('hr.department', 'parent_id', 'Child Departments'),
 -        'manager_id': fields.many2one('hr.employee', 'Manager'),
 +        'manager_id': fields.many2one('hr.employee', 'Manager', track_visibility='onchange'),
          'member_ids': fields.one2many('hr.employee', 'department_id', 'Members', readonly=True),
          'jobs_ids': fields.one2many('hr.job', 'department_id', 'Jobs'),
          'note': fields.text('Note'),
              res.append((record['id'], name))
          return res
  
 +    def create(self, cr, uid, vals, context=None):
 +        # TDE note: auto-subscription of manager done by hand, because currently
 +        # the tracking allows to track+subscribe fields linked to a res.user record
 +        # An update of the limited behavior should come, but not currently done.
 +        manager_id = vals.get("manager_id")
 +        new_id = super(hr_department, self).create(cr, uid, vals, context=context)
 +        if manager_id:
 +            employee = self.pool.get('hr.employee').browse(cr, uid, manager_id, context=context)
 +            if employee.user_id:
 +                self.message_subscribe_users(cr, uid, [new_id], user_ids=[employee.user_id.id], context=context)
 +        return new_id
 +
 +    def write(self, cr, uid, ids, vals, context=None):
 +        # TDE note: auto-subscription of manager done by hand, because currently
 +        # the tracking allows to track+subscribe fields linked to a res.user record
 +        # An update of the limited behavior should come, but not currently done.
 +        if isinstance(ids, (int, long)):
 +            ids = [ids]
 +        manager_id = vals.get("manager_id")
 +        if manager_id:
 +            employee = self.pool.get('hr.employee').browse(cr, uid, manager_id, context=context)
 +            if employee.user_id:
 +                self.message_subscribe_users(cr, uid, ids, user_ids=[employee.user_id.id], context=context)
 +        return super(hr_department, self).write(cr, uid, ids, vals, context=context)
 +
  
  class res_users(osv.osv):
      _name = 'res.users'
      _inherit = 'res.users'
 -    _columns = {
 -        'employee_ids': fields.one2many('hr.employee', 'user_id', 'Related employees'),
 -    }
  
 +    def write(self, cr, uid, ids, vals, context=None):
 +        if isinstance(ids, (int, long)):
 +            ids = [ids]
 +        result = super(res_users, self).write(cr, uid, ids, vals, context=context)
 +        employee_obj = self.pool.get('hr.employee')
 +        if vals.get('name'):
 +            for user_id in ids:
 +                if user_id == SUPERUSER_ID:
 +                    employee_ids = employee_obj.search(cr, uid, [('user_id', '=', user_id)])
 +                    employee_obj.write(cr, uid, employee_ids, {'name': vals['name']}, context=context)
 +        return result
  
 -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --combined addons/mail/mail_mail.py
@@@ -30,7 -30,6 +30,7 @@@ from openerp.addons.base.ir.ir_mail_ser
  from openerp.osv import fields, osv
  from openerp.tools.safe_eval import safe_eval as eval
  from openerp.tools.translate import _
 +import openerp.tools as tools
  
  _logger = logging.getLogger(__name__)
  
@@@ -61,7 -60,6 +61,7 @@@ class mail_mail(osv.Model)
          'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
          'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
          'headers': fields.text('Headers', copy=False),
 +        'failure_reason': fields.text('Failure Reason', help="Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues.", readonly=1),
          # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
          # and during unlink() we will not cascade delete the parent and its attachments
          'notification': fields.boolean('Is Notification',
@@@ -75,7 -73,7 +75,7 @@@
      def default_get(self, cr, uid, fields, context=None):
          # protection for `default_type` values leaking from menu action context (e.g. for invoices)
          # To remove when automatic context propagation is removed in web client
-         if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
+         if context and context.get('default_type') and context.get('default_type') not in self._fields['type'].selection:
              context = dict(context, default_type=None)
          return super(mail_mail, self).default_get(cr, uid, fields, context=context)
  
                  # Writing on the mail object may fail (e.g. lock on user) which
                  # would trigger a rollback *after* actually sending the email.
                  # To avoid sending twice the same email, provoke the failure earlier
 -                mail.write({'state': 'exception'})
 +                mail.write({
 +                    'state': 'exception', 
 +                    'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
 +                })
                  mail_sent = False
  
                  # build an RFC2822 email.message.Message object and send it without queuing
                          subtype='html',
                          subtype_alternative='plain',
                          headers=headers)
-                     res = ir_mail_server.send_email(cr, uid, msg,
+                     try:
+                         res = ir_mail_server.send_email(cr, uid, msg,
                                                      mail_server_id=mail.mail_server_id.id,
                                                      context=context)
+                     except AssertionError as error:
+                         if error.message == ir_mail_server.NO_VALID_RECIPIENT:
+                             # No valid recipient found for this particular
+                             # mail item -> ignore error to avoid blocking
+                             # delivery to next recipients, if any. If this is
+                             # the only recipient, the mail will show as failed.
+                             _logger.warning("Ignoring invalid recipients for mail.mail %s: %s",
+                                             mail.message_id, email.get('email_to'))
+                         else:
+                             raise
                  if res:
 -                    mail.write({'state': 'sent', 'message_id': res})
 +                    mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
                      mail_sent = True
  
                  # /!\ can't use mail.state here, as mail.refresh() will cause an error
                                    mail.id, mail.message_id)
                  raise
              except Exception as e:
 -                _logger.exception('failed sending mail.mail %s', mail.id)
 -                mail.write({'state': 'exception'})
 +                failure_reason = tools.ustr(e)
 +                _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
 +                mail.write({'state': 'exception', 'failure_reason': failure_reason})
                  self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
                  if raise_exception:
                      if isinstance(e, AssertionError):
@@@ -621,8 -621,7 +621,8 @@@ class test_mail(TestMail)
          mail_compose.send_mail(cr, user_raoul.id, [compose_id], {'mail_post_autofollow': True, 'mail_create_nosubscribe': True})
          group_pigs.refresh()
          message = group_pigs.message_ids[0]
 -
 +        # Test: mail_mail: notifications have been deleted
 +        self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', message.id)]),'message_send: mail.mail message should have been auto-deleted!')
          # Test: mail.group: followers (c and d added by auto follow key; raoul not added by nosubscribe key)
          pigs_pids = [p.id for p in group_pigs.message_follower_ids]
          test_pids = [self.partner_admin_id, p_b_id, p_c_id, p_d_id]
  
          # Test: Pigs and Bird did receive their message
          test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
 +        mail_ids = self.mail_mail.search(cr, uid, [('mail_message_id', '=', message2.id)])
 +        mail_record_id = self.mail_mail.browse(cr, uid, mail_ids)[0]
 +        self.assertTrue(mail_record_id, "'message_send: mail.mail message should have in processing mail queue'" )
 +        #check mass mail state...
 +        test_mail_ids = self.mail_mail.search(cr, uid, [('state', '=', 'exception')])
 +        self.assertNotIn(mail_ids, test_mail_ids, 'compose wizard: Mail sending Failed!!')
          self.assertIn(message1.id, test_msg_ids, 'compose wizard: Pigs did not receive its mass mailing message')
          self.assertIn(message2.id, test_msg_ids, 'compose wizard: Bird did not receive its mass mailing message')
  
          self.ir_model_data.create(cr, uid, {'name': 'mt_group_public', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public_id})
  
          # Data: alter mail_group model for testing purposes (test on classic, selection and many2one fields)
-         self.mail_group._track = {
+         cls = type(self.mail_group)
+         self.assertNotIn('_track', cls.__dict__)
+         cls._track = {
              'public': {
                  'mail.mt_private': lambda self, cr, uid, obj, ctx=None: obj.public == 'private',
              },
                  'mail.mt_group_public': lambda self, cr, uid, obj, ctx=None: True,
              },
          }
-         public_col = self.mail_group._columns.get('public')
-         name_col = self.mail_group._columns.get('name')
-         group_public_col = self.mail_group._columns.get('group_public_id')
-         public_col.track_visibility = 'onchange'
-         name_col.track_visibility = 'always'
-         group_public_col.track_visibility = 'onchange'
+         visibility = {'public': 'onchange', 'name': 'always', 'group_public_id': 'onchange'}
+         for key in visibility:
+             self.assertFalse(hasattr(getattr(cls, key), 'track_visibility'))
+             getattr(cls, key).track_visibility = visibility[key]
+         @self.addCleanup
+         def cleanup():
+             delattr(cls, '_track')
+             for key in visibility:
+                 del getattr(cls, key).track_visibility
  
          # Test: change name -> always tracked, not related to a subtype
          self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public'})
          # Test: first produced message: no subtype, name change tracked
          last_msg = self.group_pigs.message_ids[-1]
          self.assertFalse(last_msg.subtype_id, 'tracked: message should not have been linked to a subtype')
 -        self.assertIn(u'SelectedGroupOnly\u2192Public', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
 +        self.assertIn(u"Selectedgroupofusers\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
          self.assertIn('Pigs', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
  
          # Test: change name as supername, public as private -> 2 subtypes
          last_msg = self.group_pigs.message_ids[-3]
          self.assertEqual(last_msg.subtype_id.id, mt_name_supername_id, 'tracked: message should be linked to mt_name_supername subtype')
          self.assertIn('Supername name', last_msg.body, 'tracked: message body does not hold the subtype description')
 -        self.assertIn(u'Public\u2192Private', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
 +        self.assertIn(u"Everyone\u2192Invitedpeopleonly", _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
          self.assertIn(u'Pigs\u2192supername', _strip_string_spaces(last_msg.body), 'tracked feature: message body does not hold always tracked field')
  
          # Test: change public as public, group_public_id -> 2 subtypes, name always tracked
          last_msg = self.group_pigs.message_ids[-4]
          self.assertEqual(last_msg.subtype_id.id, mt_group_public_set_id, 'tracked: message should be linked to mt_group_public_set_id')
          self.assertIn('Group set', last_msg.body, 'tracked: message body does not hold the subtype description')
 -        self.assertIn(u'Private\u2192Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
 +        self.assertIn(u"Invitedpeopleonly\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
          self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
          # Test: second produced message: mt_group_public_id, with name always tracked, public tracked on change
          last_msg = self.group_pigs.message_ids[-5]
          self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should be linked to mt_group_public_id')
          self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description')
 -        self.assertIn(u'Private\u2192Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
 +        self.assertIn(u"Invitedpeopleonly\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
          self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
  
          # Test: change group_public_id to False -> 1 subtype, name always tracked
          self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'description': 'Dummy'})
          self.group_pigs.refresh()
          self.assertEqual(len(self.group_pigs.message_ids), 6, 'tracked: No message should have been produced')
-         # Data: removed changes
-         public_col.track_visibility = None
-         name_col.track_visibility = None
-         group_public_col.track_visibility = None
-         self.mail_group._track = {}
          </field>
      </record>
  
 -    <menuitem name="Campaigns" id="menu_marketing_campaign" parent="base.marketing_menu" groups="marketing.group_marketing_user,marketing.group_marketing_manager"/>
 +    <menuitem name="Lead Automation" id="menu_marketing_campaign" parent="base.marketing_menu" groups="marketing.group_marketing_user,marketing.group_marketing_manager" 
 +    sequence="3"/>
      <menuitem id="menu_marketing_campaign_form" parent="menu_marketing_campaign" action="action_marketing_campaign_form" sequence="30"/>
  
      <!-- Marketing Segments -->
                  <separator string="Previous Activities"/>
                  <field name="from_ids" mode="tree" context="{'default_activity_to_id': active_id}">
                      <tree string="Incoming Transitions" editable="bottom">
-                         <field name="activity_from_id" domain="[('campaign_id', '=', parent.campaign_id)]"/>
+                         <field name="activity_from_id" domain="[('campaign_id', '=', parent.campaign_id)]" options="{'no_create': True}" />
                          <field name='trigger'/>
                          <field name="interval_nbr"/>
                          <field name="interval_type"/>
                      </tree>
                      <form string="Incoming Transitions">
                          <group col="4">
-                             <field name="activity_from_id" domain="[('campaign_id', '=', parent.campaign_id)]"/>
+                             <field name="activity_from_id" domain="[('campaign_id', '=', parent.campaign_id)]" options="{'no_create': True}" />
                              <field name='trigger'/>
                              <field name="interval_nbr"/>
                              <field name="interval_type"/>
                  <separator string="Next Activities"/>
                  <field name="to_ids" mode="tree" context="{'default_activity_from_id': active_id}">
                      <tree string="Outgoing Transitions" editable="bottom">
-                         <field name="activity_to_id" domain="[('campaign_id', '=', parent.campaign_id)]"/>
+                         <field name="activity_to_id" domain="[('campaign_id', '=', parent.campaign_id)]" options="{'no_create': True}" />
                          <field name='trigger'/>
                          <field name="interval_nbr"/>
                          <field name="interval_type"/>
                      </tree>
                      <form string="Outgoing Transitions">
                          <group col="4">
-                             <field name="activity_to_id" domain="[('campaign_id', '=', parent.campaign_id)]"/>
+                             <field name="activity_to_id" domain="[('campaign_id', '=', parent.campaign_id)]" options="{'no_create': True}" />
                              <field name='trigger'/>
                              <field name="interval_nbr"/>
                              <field name="interval_type"/>
      </record>
  
      <record model="ir.actions.act_window" id="action_marketing_campaign_workitem">
 -        <field name="name">Campaign Follow-up</field>
 +        <field name="name">Follow-up</field>
          <field name="type">ir.actions.act_window</field>
          <field name="res_model">marketing.campaign.workitem</field>
          <field name="view_type">form</field>
@@@ -44,8 -44,6 +44,8 @@@ class MassMailingContact(osv.Model)
              ondelete='cascade', required=True,
          ),
          'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
 +        'unsubscription_date': fields.datetime('Unsubscription Date'),
 +        'message_bounce': fields.integer('Bounce', help='Counter of the number of bounced emails for this contact.'),
      }
  
      def _get_latest_list(self, cr, uid, context={}):
          'list_id': _get_latest_list
      }
  
 +    def on_change_opt_out(self, cr, uid, id, opt_out, context=None):
 +        return {'value': {
 +            'unsubscription_date': opt_out and fields.datetime.now() or False,
 +        }}
 +
 +    def create(self, cr, uid, vals, context=None):
 +        if 'opt_out' in vals:
 +            vals['unsubscription_date'] = vals['opt_out'] and fields.datetime.now() or False
 +        return super(MassMailingContact, self).create(cr, uid, vals, context=context)
 +
 +    def write(self, cr, uid, ids, vals, context=None):
 +        if 'opt_out' in vals:
 +            vals['unsubscription_date'] = vals['opt_out'] and fields.datetime.now() or False
 +        return super(MassMailingContact, self).write(cr, uid, ids, vals, context=context)
 +
      def get_name_email(self, name, context):
          name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
          if name and not email:
              res[record.id] = {'partner_ids': [], 'email_to': record.email, 'email_cc': False}
          return res
  
 +    def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
 +        """Called by ``message_process`` when a bounce email (such as Undelivered
 +        Mail Returned to Sender) is received for an existing thread. As contacts
 +        do not inherit form mail.thread, we have to define this method to be able
 +        to track bounces (see mail.thread for more details). """
 +        for obj in self.browse(cr, uid, ids, context=context):
 +            self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
 +
  
  class MassMailingList(osv.Model):
      """Model of a contact list. """
@@@ -301,8 -276,8 +301,8 @@@ class MassMailing(osv.Model)
                             'tooltip': ustr((date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y')),
                             } for i in range(0, self._period_number)]
          group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
-         field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
-         pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
+         field = obj._fields.get(groupby_field.split(':')[0])
+         pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field.type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
          for group in group_obj:
              group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
              timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
              'ir.attachment', 'mass_mailing_ir_attachments_rel',
              'mass_mailing_id', 'attachment_id', 'Attachments'
          ),
 +        'keep_archives': fields.boolean('Keep Archives'),
          'mass_mailing_campaign_id': fields.many2one(
              'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
              ondelete='set null',
@@@ -75,11 -75,6 +75,11 @@@ class PaymentAcquirer(osv.Model)
          'website_published': fields.boolean(
              'Visible in Portal / Website', copy=False,
              help="Make this payment acquirer available (Customer invoices, etc.)"),
 +        'auto_confirm': fields.selection(
 +            [('none', 'No automatic confirmation'),
 +             ('at_pay_confirm', 'At payment confirmation'),
 +             ('at_pay_now', 'At payment')],
 +            string='Order Confirmation', required=True),
          # Fees
          'fees_active': fields.boolean('Compute fees'),
          'fees_dom_fixed': fields.float('Fixed domestic fees'),
          'environment': 'test',
          'validation': 'automatic',
          'website_published': True,
 +        'auto_confirm': 'at_pay_confirm',
      }
  
      def _check_required_if_provider(self, cr, uid, ids, context=None):
          """ If the field has 'required_if_provider="<provider>"' attribute, then it
          required if record.provider is <provider>. """
          for acquirer in self.browse(cr, uid, ids, context=context):
-             if any(c for c, f in self._all_columns.items() if getattr(f.column, 'required_if_provider', None) == acquirer.provider and not acquirer[c]):
+             if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()):
                  return False
          return True
  
@@@ -25,7 -25,6 +25,7 @@@ import tim
  from openerp import tools
  from openerp.osv import fields, osv
  from openerp.tools.translate import _
 +from openerp.exceptions import Warning
  
  import openerp.addons.decimal_precision as dp
  import openerp.addons.product.product
@@@ -73,7 -72,6 +73,7 @@@ class pos_config(osv.osv)
          'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"),
          'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'),
          'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'),
 +        'iface_fullscreen':     fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'),
          'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"),
          'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"),
          'proxy_ip':       fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45),
@@@ -270,7 -268,7 +270,7 @@@ class pos_session(osv.osv)
                                      readonly=True,
                                      states={'opening_control' : [('readonly', False)]}
                                     ),
 -        'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currnecy"),
 +        'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"),
          'start_at' : fields.datetime('Opening Date', readonly=True), 
          'stop_at' : fields.datetime('Closing Date', readonly=True),
  
@@@ -602,7 -600,11 +602,7 @@@ class pos_order(osv.osv)
              if order['amount_return']:
                  cash_journal = session.cash_journal_id
                  if not cash_journal:
 -                    cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
 -                    if not len(cash_journal_ids):
 -                        raise osv.except_osv( _('error!'),
 -                            _("No cash statement found for this session. Unable to record returned cash."))
 -                    cash_journal = cash_journal_ids[0].journal_id
 +                    raise Warning(_('No cash statement found with cash control enabled for this session. Unable to record returned cash.'))
                  self.add_payment(cr, uid, order_id, {
                      'amount': -order['amount_return'],
                      'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
          'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Account'), multi='all'),
          'amount_total': fields.function(_amount_all, string='Total', multi='all'),
          'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Account'), multi='all'),
 -        'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Account'), multi='all'),
 +        'amount_return': fields.function(_amount_all, string='Returned', digits_compute=dp.get_precision('Account'), multi='all'),
          'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True),
          'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
          'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
      def create_account_move(self, cr, uid, ids, context=None):
          return self._create_account_move_line(cr, uid, ids, None, None, context=context)
  
+     def _prepare_analytic_account(self, cr, uid, line, context=None):
+         '''This method is designed to be inherited in a custom module'''
+         return False
      def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
          # Tricky, via the workflow, we only have one id in the ids variable
          """Create a account move line of order grouped by products or not."""
                  })
  
                  if data_type == 'product':
 -                    key = ('product', values['partner_id'], values['product_id'], values['debit'] > 0)
 +                    key = ('product', values['partner_id'], (values['product_id'], values['name']), values['debit'] > 0)
                  elif data_type == 'tax':
                      key = ('tax', values['partner_id'], values['tax_code_id'], values['debit'] > 0)
                  elif data_type == 'counter_part':
                      if tax_code_id:
                          break
  
 +                name = line.product_id.name
 +                if line.notice:
 +                    # add discount reason in move
 +                    name = name + ' (' + line.notice + ')'
 +
                  # Create a move for the line
                  insert_data('product', {
 -                    'name': line.product_id.name,
 +                    'name': name,
                      'quantity': line.qty,
                      'product_id': line.product_id.id,
                      'account_id': income_account,
+                     'analytic_account_id': self._prepare_analytic_account(cr, uid, line, context=context),
                      'credit': ((amount>0) and amount) or 0.0,
                      'debit': ((amount<0) and -amount) or 0.0,
                      'tax_code_id': tax_code_id,
@@@ -1247,7 -1249,7 +1252,7 @@@ class account_bank_statement(osv.osv)
  class account_bank_statement_line(osv.osv):
      _inherit = 'account.bank.statement.line'
      _columns= {
 -        'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
 +        'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'),
      }
  
  
  .ui-dialog .ui-icon-closethick{
      float: right;
  }
 +div.modal.in {
 +    position: absolute;
 +    background: white;
 +    padding: 20px;
 +    box-shadow: 0px 10px 20px black;
 +    border-radius: 3px;
 +    max-width: 600px;
 +    max-height: 400px;
 +    margin-top: -200px;
 +    margin-left: -300px;
 +    top: 50%;
 +    left: 50%;
 +}
  /* --- Generic Restyling and Resets --- */
  
  html {
@@@ -506,11 -493,10 +506,11 @@@ td 
      margin-right: 4px;
  }
  
 -.pos .control-button.highlight{
 -    background: #6EC89B;
 -    border: solid 1px #6EC89B;
 -    color: white;
 +.pos .control-button.highlight,
 +.pos .button.highlight {
 +    background: #6EC89B !important;
 +    border: solid 1px #6EC89B !important;
 +    color: white !important;
  }
  .pos .control-button:active {
      background: #7F82AC;
      color: inherit;
  }
  
 -/*  ********* The paypad contains the payment buttons ********* */
 +/*  ********* The actionpad (payment, set customer) ********* */
  
 -.pos .paypad {
 -    padding: 8px 4px 8px 8px;
 +.pos .actionpad{
 +    padding: 8px 3px 8px 19px;
      display: inline-block;
      text-align: center;
      vertical-align: top;
 -    width: 205px;
 +    width: 183px;
      max-height: 350px;
      overflow-y: auto;
      overflow-x: hidden;
  }
 -.pos .paypad button {
 -    height: 39px;
 +.pos .actionpad .button {
 +    height: 50px;
      display: block;
      width: 100%;
      margin: 0px -6px 4px -2px;
      color: #555555;
      font-size: 14px;
  }
 -.pos .paypad button:active, 
 -.pos .numpad button:active, 
 -.pos .numpad .selected-mode, 
 -.pos .popup  button:active{
 -    border: none;
 +.pos .actionpad .button.pay {
 +    height: 158px;
 +}
 +.pos .actionpad .button.pay .fa {
 +    display: block;
 +    font-size: 32px;
 +    line-height: 54px;
 +    padding-top: 6px;
 +    background: rgb(86, 86, 86);
      color: white;
 -    background: #7f82ac;
 +    width: 60px;
 +    margin: auto;
 +    border-radius: 30px;
 +    margin-bottom: 10px;
  }
  
  /*  ********* The Numpad ********* */
  .pos .numpad button {
      height: 50px;
      width: 50px;
 -    margin: 0px 0px 4px 0px;
 +    margin: 0px 3px 4px 0px;
      font-weight: bold;
      vertical-align: middle;
      color: #555555;
      overflow-y: auto;
      border-right: dashed 1px rgb(215,215,215);
  }
 +.screen .left-content.pc40{
 +    right: 66%;
 +}
  .screen .right-content{
      position: absolute;
      right:0px; top: 64px; bottom: 0px;
      overflow-x: hidden;
      overflow-y: auto;
  }
 +.screen .right-content.pc60{
 +    left:34%
 +}
  .screen .centered-content{
      position: absolute;
      right:25%; top: 64px; bottom: 0px;
      position:relative;
  }
  
 -/* b) The payment screen */
  
 +/* b) The payment screen */
  
 -.pos .pos-payment-container {
 +.pos .payment-numpad {
      display: inline-block;
 -    font-size: 16px;
 -    text-align: left;
 -    width: 360px;
 -}
 -.pos .payment-due-total {
 +    width: 50%;
 +    box-sizing: border-box;
 +    padding: 8px;
      text-align: center;
 -    font-weight: bold;
 -    font-size: 48px;
 -    margin: 27px;
 -    text-shadow: 0px 2px rgb(202, 202, 202);
 +    float: left;
  }
 -.pos .paymentline{
 -    position: relative;
 +.pos .payment-numpad .numpad button {
 +    width: 66px;
 +    height: 66px;
 +}
 +
 +.pos .paymentlines-container {
 +    padding: 16px;
 +    padding-top: 0;
 +    border-bottom: dashed 1px gainsboro;
 +    min-height: 154px;
 +}
 +
 +.pos .paymentlines {
 +    width: 100%;
 +}
 +.pos .paymentlines .controls {
 +    width: 40px;
 +}
 +.pos .paymentlines .label > * {
 +    font-size: 16px;
      padding: 8px;
 -    border-box: 3px;
 -    box-sizing: border-box;
 -    -moz-box-sizing: border-box;
 +}
 +.pos .paymentlines tbody{
 +    background: white;
      border-radius: 3px;
  }
 -.pos .paymentline-name{
 -    margin-bottom: 8px;
 +.pos .paymentline{
 +    font-size: 22px;
  }
 -.pos .paymentline-input{
 -    font-size: 20px;
 -    font-family: Lato;
 -    display: block;
 -    width: 100%;
 -    box-sizing: border-box;
 -    -moz-box-sizing: border-box;
 -    outline: none;
 -    border: none;
 -    padding: 6px 8px;
 +.pos .paymentline.selected {
 +    font-size: 22px;
 +    background: #6EC89B;
 +    color: white;
 +}
 +.pos .paymentline.selected .edit {
      background: white;
 -    color: #484848;
 -    text-align: right;
 +    color: #6EC89B;
 +    outline: 3px blue;
 +    box-shadow: 0px 0px 0px 3px #6EC89B;
 +    position: relative;
      border-radius: 3px;
 -    box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset;
  }
  
 -.pos .paymentline-input:focus{
 -    color: rgb(130, 124, 255);
 -    box-shadow: 0px 2px rgba(219, 219, 219, 0.3) inset;
 -    -webkit-animation: all 250ms linear;
 +.pos .paymentline > *{
 +    padding: 8px 12px;
  }
 -
 -.paymentline-delete {
 -    width: 32px;
 -    height: 32px;
 -    padding: 5px;
 -    text-align: center;
 -    box-sizing: border-box;
 -    -moz-box-sizing: border-box;
 -    position: absolute;
 -    bottom: 10px;
 -    left: 8px;
 +.pos .paymentline .col-due,
 +.pos .paymentline .col-tendered,
 +.pos .paymentline .col-change {
 +    min-width: 90px;
 +}
 +.pos .paymentline .col-change.highlight {
 +    background: rgb(184, 152, 204);
 +}
 +.pos .paymentline .col-name {
 +    font-size: 16px;
 +}
 +.pos .paymentline .delete-button{
      cursor: pointer;
 +    text-align: center;
  }
 -
 -.pos .pos-payment-container .left-block{
 +.pos .payment-buttons {
      display: inline-block;
 -    width:49%;
 -    margin:0;
 -    padding:0;
 -    text-align:left;
 +    width: 50%;
 +    box-sizing: border-box;
 +    padding: 16px;
 +    float: right;
  }
 -.pos .pos-payment-container .infoline{
 -    margin-top:5px;
 -    margin-bottom:5px;
 +
 +.payment-screen   .payment-buttons .button {
 +    background: rgb(221, 221, 221);
 +    line-height: 48px;
 +    margin-bottom: 4px;
 +    border-radius: 3px;
 +    font-size: 16px;
      padding: 0px 8px;
 -    opacity: 0.5;
 -}
 -.pos .pos-payment-container .infoline.bigger{
 -    opacity: 1;
 -    font-size: 20px;
 +    border: solid 1px rgb(202, 202, 202);
 +    cursor: pointer;
 +    text-align: center;
  }
 -.pos .pos-payment-container .right-block{
 -    display: inline-block;
 -    width:49%;
 -    margin:0;
 -    padding:0;
 -    text-align:right;
 +.payment-screen  .paymentlines-empty .total {
 +    text-align: center;
 +    padding: 24px 0px 18px;
 +    font-size: 64px;
 +    color: #43996E;
 +    text-shadow: 0px 2px white, 0px 2px 2px rgba(0, 0, 0, 0.27);
  }
 -.pos .paymentline.selected{
 -    background: rgb(220,220,220);
 +.payment-screen  .paymentlines-empty .message {
 +    text-align: center;
  }
  
  /* c) The receipt screen */
  
 +.pos .receipt-screen .centered-content .button {
 +    line-height: 40px;
 +    padding: 3px 13px;
 +    font-size: 20px;
 +    text-align: center;
 +    background: rgb(230, 230, 230);
 +    margin: 16px;
 +    margin-bottom: 0px;
 +    border-radius: 3px;
 +    border: solid 1px rgb(209, 209, 209);
 +    cursor: pointer;
 +}
  .pos .pos-receipt-container {
      font-size: 0.75em;
 +    text-align: center;
  }
  
  .pos .pos-sale-ticket {
  }
  
  @media print {
 +    body {
 +        margin: 0;
 +    }
      .oe_leftbar,
      .pos .pos-topheader, 
      .pos .pos-leftpane, 
 -    .pos .keyboard_frame {
 +    .pos .keyboard_frame, 
 +    .pos .receipt-screen header,
 +    .pos .receipt-screen .top-content,
 +    .pos .receipt-screen .centered-content .button {
          display: none !important;
      }
      .pos,
          left: 0px !important;
          background-color: white;
      }
      .pos .receipt-screen {
          text-align: left;
      }
 +    .pos .receipt-screen .centered-content{
 +        position: static;
 +        border: none;
 +        margin: none;
 +    }
 +    .pos .pos-receipt-container {
 +        text-align: left;
 +    }
      .pos-actionbar {
          display: none !important;
      }
      font-size: 24px;
      vertical-align: top;
  }
 -.splitbill-screen .paymentmethods {
 +.splitbill-screen .paymentmethods,
 +.payment-screen   .paymentmethods {
      margin: 16px;
  }
 -.splitbill-screen .paymentmethod {
 +.splitbill-screen .paymentmethod,
 +.payment-screen   .paymentmethod {
      background: rgb(221, 221, 221);
      line-height: 40px;
      margin-bottom: 4px;
      cursor: pointer;
      text-align: center;
  }
 +.splitbill-screen .paymentmethod.active,
 +.payment-screen   .paymentmethod.active {
 +    background: #6EC89B;
 +    color: white;
 +    border-color: #6EC89B;
 +}
 +
 +
  
  /*  ********* The ActionBarWidget  ********* */
  
  .pos .popup .comment{
      font-weight: normal;
      font-size: 18px;
 -    margin: 0px 16px;
 +    margin: 0px 16px 16px 16px;
 +}
 +.pos .popup .comment.traceback {
 +    height: 264px;
 +    overflow: auto;
 +    font-size: 14px;
 +    text-align: left;
 +    font-family: 'Inconsolata';
 +    -webkit-user-select: text;
 +       -moz-user-select: text;
 +            user-select: text;
  }
  .pos .popup .comment.traceback {
      height: 264px;
      overflow: auto;
-     font-size: 14px;
+     font-size: 12px;
      text-align: left;
+     white-space: pre-wrap;
      font-family: 'Inconsolata';
      -webkit-user-select: text;
         -moz-user-select: text;
@@@ -211,6 -211,26 +211,6 @@@ function openerp_pos_screens(instance, 
          barcode_error_action: function(code){
              this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
          },
 -        // shows an action bar on the screen. The actionbar is automatically shown when you add a button
 -        // with add_action_button()
 -        show_action_bar: function(){
 -            this.pos_widget.action_bar.show();
 -        },
 -
 -        // hides the action bar. The actionbar is automatically hidden when it is empty
 -        hide_action_bar: function(){
 -            this.pos_widget.action_bar.hide();
 -        },
 -
 -        // adds a new button to the action bar. The button definition takes three parameters, all optional :
 -        // - label: the text below the button
 -        // - icon:  a small icon that will be shown
 -        // - click: a callback that will be executed when the button is clicked.
 -        // the method returns a reference to the button widget, and automatically show the actionbar.
 -        add_action_button: function(button_def){
 -            this.show_action_bar();
 -            return this.pos_widget.action_bar.add_new_button(button_def);
 -        },
  
          // this method shows the screen and sets up all the widget related to this screen. Extend this method
          // if you want to alter the behavior of the screen.
                  this.$el.removeClass('oe_hidden');
              }
  
 -            if(this.pos_widget.action_bar.get_button_count() > 0){
 -                this.show_action_bar();
 -            }else{
 -                this.hide_action_bar();
 -            }
 -            
              var self = this;
  
              this.pos_widget.set_numpad_visible(this.show_numpad);
              if(this.pos.barcode_reader){
                  this.pos.barcode_reader.reset_action_callbacks();
              }
 -            this.pos_widget.action_bar.destroy_buttons();
          },
  
          // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
          },
      });
  
 +    module.FullscreenPopup = module.PopUpWidget.extend({
 +        template:'FullscreenPopupWidget',
 +        show: function(){
 +            var self = this;
 +            this._super();
 +            this.renderElement();
 +            this.$('.button.fullscreen').off('click').click(function(){
 +                window.document.body.webkitRequestFullscreen();
 +                self.pos_widget.screen_selector.close_popup();
 +            });
 +            this.$('.button.cancel').off('click').click(function(){
 +                self.pos_widget.screen_selector.close_popup();
 +            });
 +        },
 +        ismobile: function(){
 +            return typeof window.orientation !== 'undefined'; 
 +        }
 +    });
 +
 +
      module.ErrorPopupWidget = module.PopUpWidget.extend({
          template:'ErrorPopupWidget',
          show: function(options){
          },
      });
  
 -    module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
 -        template: 'ErrorNoClientPopupWidget',
 -    });
 -
      module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
          template: 'ErrorInvoiceTransferPopupWidget',
      });
  
          init: function(parent, options){
              this._super(parent, options);
 +            this.partner_cache = new module.DomCache();
          },
  
          show_leftpane: false,
              contents.innerHTML = "";
              for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
                  var partner    = partners[i];
 -                var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
 -                var clientline = document.createElement('tbody');
 -                clientline.innerHTML = clientline_html;
 -                clientline = clientline.childNodes[1];
 -
 +                var clientline = this.partner_cache.get_node(partner.id);
 +                if(!clientline){
 +                    var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
 +                    var clientline = document.createElement('tbody');
 +                    clientline.innerHTML = clientline_html;
 +                    clientline = clientline.childNodes[1];
 +                    this.partner_cache.cache_node(partner.id,clientline);
 +                }
                  if( partners === this.new_client ){
                      clientline.classList.add('highlight');
                  }else{
                      clientline.classList.remove('highlight');
                  }
 -
                  contents.appendChild(clientline);
              }
          },
  
      module.ReceiptScreenWidget = module.ScreenWidget.extend({
          template: 'ReceiptScreenWidget',
 -
          show_numpad:     false,
          show_leftpane:   false,
  
              this._super();
              var self = this;
  
 -            var print_button = this.add_action_button({
 -                    label: _t('Print'),
 -                    icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
 -                    click: function(){ self.print(); },
 -                });
 -
 -            var finish_button = this.add_action_button({
 -                    label: _t('Next Order'),
 -                    icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
 -                    click: function() { self.finishOrder(); },
 -                });
 -
              this.refresh();
-             this.print();
+             if (!this.pos.get('selectedOrder')._printed) {
+                 this.print();
+             }
  
 -            //
              // The problem is that in chrome the print() is asynchronous and doesn't
              // execute until all rpc are finished. So it conflicts with the rpc used
              // to send the orders to the backend, and the user is able to go to the next 
              // 2 seconds is the same as the default timeout for sending orders and so the dialog
              // should have appeared before the timeout... so yeah that's not ultra reliable. 
  
 -            finish_button.set_disabled(true);   
 +            this.lock_screen(true);  
              setTimeout(function(){
 -                finish_button.set_disabled(false);
 +                self.lock_screen(false);  
              }, 2000);
          },
 +        lock_screen: function(locked) {
 +            this._locked = locked;
 +            if (locked) {
 +                this.$('.next').removeClass('highlight');
 +            } else {
 +                this.$('.next').addClass('highlight');
 +            }
 +        },
          print: function() {
+             this.pos.get('selectedOrder')._printed = true;
              window.print();
          },
 -        finishOrder: function() {
 -            this.pos.get('selectedOrder').destroy();
 +        finish_order: function() {
 +            if (!this._locked) {
 +                this.pos.get_order().finalize();
 +            }
 +        },
 +        renderElement: function() {
 +            var self = this;
 +            this._super();
 +            this.$('.next').click(function(){
 +                self.finish_order();
 +            });
 +            this.$('.button.print').click(function(){
 +                self.print();
 +            });
          },
          refresh: function() {
 -            var order = this.pos.get('selectedOrder');
 -            $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
 +            var order = this.pos.get_order();
 +            this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
                      widget:this,
                      order: order,
                      orderlines: order.get('orderLines').models,
                      paymentlines: order.get('paymentLines').models,
                  }));
          },
 -        close: function(){
 -            this._super();
 -        }
      });
  
 -
      module.PaymentScreenWidget = module.ScreenWidget.extend({
 -        template: 'PaymentScreenWidget',
 -        back_screen: 'products',
 -        next_screen: 'receipt',
 +        template:      'PaymentScreenWidget',
 +        back_screen:   'product',
 +        next_screen:   'receipt',
 +        show_leftpane: false,
 +        show_numpad:   false,
          init: function(parent, options) {
              var self = this;
 -            this._super(parent,options);
 +            this._super(parent, options);
  
              this.pos.bind('change:selectedOrder',function(){
 -                    this.bind_events();
                      this.renderElement();
 +                    this.watch_order_changes();
                  },this);
 +            this.watch_order_changes();
  
 -            this.bind_events();
 -
 -            this.line_delete_handler = function(event){
 -                var node = this;
 -                while(node && !node.classList.contains('paymentline')){
 -                    node = node.parentNode;
 -                }
 -                if(node){
 -                    self.pos.get('selectedOrder').removePaymentline(node.line)   
 +            this.inputbuffer = "";
 +            this.firstinput  = true;
 +            this.keyboard_handler = function(event){
 +                var key = '';
 +                if ( event.keyCode === 13 ) {         // Enter
 +                    self.validate_order();
 +                } else if ( event.keyCode === 190 ) { // Dot
 +                    key = '.';
 +                } else if ( event.keyCode === 46 ) {  // Delete
 +                    key = 'CLEAR';
 +                } else if ( event.keyCode === 8 ) {   // Backspace 
 +                    key = 'BACKSPACE';
 +                    event.preventDefault(); // Prevents history back nav
 +                } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){       // Numbers
 +                    key = '' + (event.keyCode - 48);
 +                } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){      // Numpad Numbers
 +                    key = '' + (event.keyCode - 96);
 +                } else if ( event.keyCode === 189 || event.keyCode === 109 ) {  // Minus
 +                    key = '-';
 +                } else if ( event.keyCode === 107 ) { // Plus
 +                    key = '+';
                  }
 -                event.stopPropagation();
 -            };
  
 -            this.line_change_handler = function(event){
 -                var node = this;
 -                while(node && !node.classList.contains('paymentline')){
 -                    node = node.parentNode;
 -                }
 -                if(node){
 -                    node.line.set_amount(this.value);
 -                }
 -            };
 +                self.payment_input(key);
  
 -            this.line_click_handler = function(event){
 -                var node = this;
 -                while(node && !node.classList.contains('paymentline')){
 -                    node = node.parentNode;
 -                }
 -                if(node){
 -                    self.pos.get('selectedOrder').selectPaymentline(node.line);
 -                }
              };
 -
 -            this.hotkey_handler = function(event){
 -                if(event.which === 13){
 -                    self.validate_order();
 -                }else if(event.which === 27){
 -                    self.back();
 -                }
 -            };
 -
          },
 -        show: function(){
 -            this._super();
 -            var self = this;
 -            
 -            this.enable_numpad();
 -            this.focus_selected_line();
 -            
 -            document.body.addEventListener('keyup', this.hotkey_handler);
 -
 -            this.add_action_button({
 -                    label: _t('Back'),
 -                    icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
 -                    click: function(){  
 -                        self.back();
 -                    },
 -                });
 -
 -            this.add_action_button({
 -                    label: _t('Validate'),
 -                    name: 'validation',
 -                    icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
 -                    click: function(){
 -                        self.validate_order();
 -                    },
 -                });
 -           
 -            if( this.pos.config.iface_invoicing ){
 -                this.add_action_button({
 -                        label: 'Invoice',
 -                        name: 'invoice',
 -                        icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
 -                        click: function(){
 -                            self.validate_order({invoice: true});
 -                        },
 -                    });
 +        // resets the current input buffer
 +        reset_input: function(){
 +            var line = this.pos.get_order().selected_paymentline;
 +            this.firstinput  = true;
 +            if (line) {
 +                this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
 +            } else {
 +                this.inputbuffer = "";
              }
 -
 -            if( this.pos.config.iface_cashdrawer ){
 -                this.add_action_button({
 -                        label: _t('Cash'),
 -                        name: 'cashbox',
 -                        icon: '/point_of_sale/static/src/img/open-cashbox.png',
 -                        click: function(){
 -                            self.pos.proxy.open_cashbox();
 -                        },
 -                    });
 +        },
 +        // handle both keyboard and numpad input. Accepts
 +        // a string that represents the key pressed.
 +        payment_input: function(input) {
 +            var oldbuf = this.inputbuffer.slice(0);
 +
 +            if (input === '.') {
 +                if (this.firstinput) {
 +                    this.inputbuffer = "0.";
 +                }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
 +                    this.inputbuffer += "0.";
 +                } else if (this.inputbuffer.indexOf('.') < 0){
 +                    this.inputbuffer = this.inputbuffer + '.';
 +                }
 +            } else if (input === 'CLEAR') {
 +                this.inputbuffer = ""; 
 +            } else if (input === 'BACKSPACE') { 
 +                this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
 +            } else if (input === '+') {
 +                if ( this.inputbuffer[0] === '-' ) {
 +                    this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
 +                }
 +            } else if (input === '-') {
 +                if ( this.inputbuffer[0] === '-' ) {
 +                    this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
 +                } else {
 +                    this.inputbuffer = '-' + this.inputbuffer;
 +                }
 +            } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
 +                this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
 +            } else if (!isNaN(parseInt(input))) {
 +                if (this.firstinput) {
 +                    this.inputbuffer = '' + input;
 +                } else {
 +                    this.inputbuffer += input;
 +                }
              }
  
 -            this.update_payment_summary();
 +            this.firstinput = false;
  
 -        },
 -        close: function(){
 -            this._super();
 -            this.disable_numpad();
 -            document.body.removeEventListener('keyup',this.hotkey_handler);
 -        },
 -        remove_empty_lines: function(){
 -            var order = this.pos.get('selectedOrder');
 -            var lines = order.get('paymentLines').models.slice(0);
 -            for(var i = 0; i < lines.length; i++){ 
 -                var line = lines[i];
 -                if(line.get_amount() === 0){
 -                    order.removePaymentline(line);
 +            if (this.inputbuffer !== oldbuf) {
 +                var order = this.pos.get_order();
 +                if (order.selected_paymentline) {
 +                    order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
 +                    this.order_changes();
 +                    this.render_paymentlines();
 +                    this.$('.paymentline.selected .edit').text(this.inputbuffer);
                  }
              }
          },
 -        back: function() {
 -            this.remove_empty_lines();
 -            this.pos_widget.screen_selector.set_current_screen(this.back_screen);
 +        click_numpad: function(button) {
 +            this.payment_input(button.data('action'));
          },
 -        bind_events: function() {
 -            if(this.old_order){
 -                this.old_order.unbind(null,null,this);
 -            }
 -            var order = this.pos.get('selectedOrder');
 -                order.bind('change:selected_paymentline',this.focus_selected_line,this);
 -
 -            this.old_order = order;
 -
 -            if(this.old_paymentlines){
 -                this.old_paymentlines.unbind(null,null,this);
 -            }
 -            var paymentlines = order.get('paymentLines');
 -                paymentlines.bind('add', this.add_paymentline, this);
 -                paymentlines.bind('change:selected', this.rerender_paymentline, this);
 -                paymentlines.bind('change:amount', function(line){
 -                        if(!line.selected && line.node){
 -                            line.node.value = line.amount.toFixed(2);
 -                        }
 -                        this.update_payment_summary();
 -                    },this);
 -                paymentlines.bind('remove', this.remove_paymentline, this);
 -                paymentlines.bind('all', this.update_payment_summary, this);
 -
 -            this.old_paymentlines = paymentlines;
 -
 -            if(this.old_orderlines){
 -                this.old_orderlines.unbind(null,null,this);
 +        render_numpad: function() {
 +            var self = this;
 +            var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
 +            numpad.on('click','button',function(){
 +                self.click_numpad($(this));
 +            });
 +            return numpad;
 +        },
 +        click_delete_paymentline: function(cid){
 +            var lines = this.pos.get_order().get('paymentLines').models;
 +            for ( var i = 0; i < lines.length; i++ ) {
 +                if (lines[i].cid === cid) {
 +                    this.pos.get_order().removePaymentline(lines[i]);
 +                    this.reset_input();
 +                    this.render_paymentlines();
 +                    return;
 +                }
              }
 -            var orderlines = order.get('orderLines');
 -                orderlines.bind('all', this.update_payment_summary, this);
 -
 -            this.old_orderlines = orderlines;
          },
 -        focus_selected_line: function(){
 -            var line = this.pos.get('selectedOrder').selected_paymentline;
 -            if(line){
 -                var input = line.node.querySelector('input');
 -                if(!input){
 +        click_paymentline: function(cid){
 +            var lines = this.pos.get_order().get('paymentLines').models;
 +            for ( var i = 0; i < lines.length; i++ ) {
 +                if (lines[i].cid === cid) {
 +                    this.pos.get_order().selectPaymentline(lines[i]);
 +                    this.reset_input();
 +                    this.render_paymentlines();
                      return;
                  }
 -                var value = input.value;
 -                input.focus();
 +            }
 +        },
 +        render_paymentlines: function() {
 +            var self  = this;
 +            var order = this.pos.get_order();
 +            var lines = order.get('paymentLines').models;
  
 -                if(this.numpad_state){
 -                    this.numpad_state.reset();
 -                }
 +            this.$('.paymentlines-container').empty();
 +            var lines = $(QWeb.render('PaymentScreen-Paymentlines', { 
 +                widget: this, 
 +                order: order,
 +                paymentlines: lines,
 +            }));
  
 -                if(Number(value) === 0){
 -                    input.value = '';
 -                }else{
 -                    input.value = value;
 -                    input.select();
 +            lines.on('click','.delete-button',function(){
 +                self.click_delete_paymentline($(this).data('cid'));
 +            });
 +
 +            lines.on('click','.paymentline',function(){
 +                self.click_paymentline($(this).data('cid'));
 +            });
 +                
 +            lines.appendTo(this.$('.paymentlines-container'));
 +        },
 +        click_paymentmethods: function(id) {
 +            var cashregister = null;
 +            for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
 +                if ( this.pos.cashregisters[i].journal_id[0] === id ){
 +                    cashregister = this.pos.cashregisters[i];
 +                    break;
                  }
              }
 +            this.pos.get_order().addPaymentline( cashregister );
 +            this.reset_input();
 +            this.render_paymentlines();
          },
 -        add_paymentline: function(line) {
 -            var list_container = this.el.querySelector('.payment-lines');
 -                list_container.appendChild(this.render_paymentline(line));
 -            
 -            if(this.numpad_state){
 -                this.numpad_state.reset();
 +        render_paymentmethods: function() {
 +            var self = this;
 +            var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
 +                methods.on('click','.paymentmethod',function(){
 +                    self.click_paymentmethods($(this).data('id'));
 +                });
 +            return methods;
 +        },
 +        click_invoice: function(){
 +            var order = this.pos.get_order();
 +            order.set_to_invoice(!order.is_to_invoice());
 +            if (order.is_to_invoice()) {
 +                this.$('.js_invoice').addClass('highlight');
 +            } else {
 +                this.$('.js_invoice').removeClass('highlight');
              }
          },
 -        render_paymentline: function(line){
 -            var el_html  = openerp.qweb.render('Paymentline',{widget: this, line: line});
 -                el_html  = _.str.trim(el_html);
 +        renderElement: function() {
 +            var self = this;
 +            this._super();
  
 -            var el_node  = document.createElement('tbody');
 -                el_node.innerHTML = el_html;
 -                el_node = el_node.childNodes[0];
 -                el_node.line = line;
 -                el_node.querySelector('.paymentline-delete')
 -                    .addEventListener('click', this.line_delete_handler);
 -                el_node.addEventListener('click', this.line_click_handler);
 -                el_node.querySelector('input')
 -                    .addEventListener('keyup', this.line_change_handler);
 +            var numpad = this.render_numpad();
 +            numpad.appendTo(this.$('.payment-numpad'));
  
 -            line.node = el_node;
 +            var methods = this.render_paymentmethods();
 +            methods.appendTo(this.$('.paymentmethods-container'));
 +
 +            this.render_paymentlines();
 +
 +            this.$('.back').click(function(){
 +                self.pos_widget.screen_selector.back();
 +            });
 +
 +            this.$('.next').click(function(){
 +                self.validate_order();
 +            });
 +
 +            this.$('.js_invoice').click(function(){
 +                self.click_invoice();
 +            });
  
 -            return el_node;
 -        },
 -        rerender_paymentline: function(line){
 -            var old_node = line.node;
 -            var new_node = this.render_paymentline(line);
 -            
 -            old_node.parentNode.replaceChild(new_node,old_node);
          },
 -        remove_paymentline: function(line){
 -            line.node.parentNode.removeChild(line.node);
 -            line.node = undefined;
 +        show: function(){
 +            this.pos.get_order().clean_empty_paymentlines();
 +            this.reset_input();
 +            this.render_paymentlines();
 +            this.order_changes();
 +            window.document.body.addEventListener('keydown',this.keyboard_handler);
 +            this._super();
          },
 -        renderElement: function(){
 +        hide: function(){
 +            window.document.body.removeEventListener('keydown',this.keyboard_handler);
              this._super();
 -
 -            var paymentlines   = this.pos.get('selectedOrder').get('paymentLines').models;
 -            var list_container = this.el.querySelector('.payment-lines');
 -
 -            for(var i = 0; i < paymentlines.length; i++){
 -                list_container.appendChild(this.render_paymentline(paymentlines[i]));
 -            }
 -            
 -            this.update_payment_summary();
 -        },
 -        update_payment_summary: function() {
 -            var currentOrder = this.pos.get('selectedOrder');
 -            var paidTotal = currentOrder.getPaidTotal();
 -            var dueTotal = currentOrder.getTotalTaxIncluded();
 -            var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
 -            var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
 -
 -            this.$('.payment-due-total').html(this.format_currency(dueTotal));
 -            this.$('.payment-paid-total').html(this.format_currency(paidTotal));
 -            this.$('.payment-remaining').html(this.format_currency(remaining));
 -            this.$('.payment-change').html(this.format_currency(change));
 -            if(currentOrder.selected_orderline === undefined){
 -                remaining = 1;  // What is this ? 
 -            }
 -                
 -            if(this.pos_widget.action_bar){
 -                this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
 -                this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
 +        },
 +        // sets up listeners to watch for order changes
 +        watch_order_changes: function() {
 +            var self = this;
 +            var order = this.pos.get_order();
 +            if(this.old_order){
 +                this.old_order.unbind(null,null,this);
              }
 +            order.bind('all',function(){
 +                self.order_changes();
 +            });
 +            this.old_order = order;
          },
 -        is_paid: function(){
 -            var currentOrder = this.pos.get('selectedOrder');
 -            return (currentOrder.getTotalTaxIncluded() < 0.000001 
 -                   || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
 -
 +        // called when the order is changed, used to show if
 +        // the order is paid or not
 +        order_changes: function(){
 +            var self = this;
 +            var order = this.pos.get_order();
 +            if (order.isPaid()) {
 +                self.$('.next').addClass('highlight');
 +            }else{
 +                self.$('.next').removeClass('highlight');
 +            }
          },
 -        validate_order: function(options) {
 +        // Check if the order is paid, then sends it to the backend,
 +        // and complete the sale process
 +        validate_order: function() {
              var self = this;
 -            options = options || {};
  
 -            var currentOrder = this.pos.get('selectedOrder');
 +            var order = this.pos.get_order();
  
 -            if(currentOrder.get('orderLines').models.length === 0){
 +            if(order.get('orderLines').models.length === 0){
                  this.pos_widget.screen_selector.show_popup('error',{
                      'message': _t('Empty Order'),
                      'comment': _t('There must be at least one product in your order before it can be validated'),
                  return;
              }
  
 -            if(!this.is_paid()){
 +            if (!order.isPaid() || this.invoicing) {
                  return;
              }
  
              // The exact amount must be paid if there is no cash payment method defined.
 -            if (Math.abs(currentOrder.getTotalTaxIncluded() - currentOrder.getPaidTotal()) > 0.00001) {
 +            if (Math.abs(order.getTotalTaxIncluded() - order.getPaidTotal()) > 0.00001) {
                  var cash = false;
                  for (var i = 0; i < this.pos.cashregisters.length; i++) {
                      cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
                  }
              }
  
 -            if (this.pos.config.iface_cashdrawer) {
 +            if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) { 
 +            
                      this.pos.proxy.open_cashbox();
              }
  
 -            if(options.invoice){
 -                // deactivate the validation button while we try to send the order
 -                this.pos_widget.action_bar.set_button_disabled('validation',true);
 -                this.pos_widget.action_bar.set_button_disabled('invoice',true);
 -
 -                var invoiced = this.pos.push_and_invoice_order(currentOrder);
 +            if (order.is_to_invoice()) {
 +                var invoiced = this.pos.push_and_invoice_order(order);
 +                this.invoicing = true;
  
                  invoiced.fail(function(error){
 -                    if(error === 'error-no-client'){
 -                        self.pos_widget.screen_selector.show_popup('error',{
 -                            message: _t('An anonymous order cannot be invoiced'),
 -                            comment: _t('Please select a client for this order. This can be done by clicking the order tab'),
 +                    self.invoicing = false;
 +                    if (error === 'error-no-client') {
 +                        self.pos_widget.screen_selector.show_popup('confirm',{
 +                            message: _t('Please select the Customer'),
 +                            comment: _t('You need to select the customer before you can invoice an order.'),
 +                            confirm: function(){
 +                                self.pos_widget.screen_selector.set_current_screen('clientlist');
 +                            },
                          });
 -                    }else{
 +                    } else {
                          self.pos_widget.screen_selector.show_popup('error',{
                              message: _t('The order could not be sent'),
                              comment: _t('Check your internet connection and try again.'),
                          });
                      }
                  });
  
                  invoiced.done(function(){
 -                    self.pos_widget.action_bar.set_button_disabled('validation',false);
 -                    self.pos_widget.action_bar.set_button_disabled('invoice',false);
 -                    self.pos.get('selectedOrder').destroy();
 +                    self.invoicing = false;
 +                    order.finalize();
                  });
 -
 -            }else{
 -                this.pos.push_order(currentOrder) 
 -                if(this.pos.config.iface_print_via_proxy){
 +            } else {
 +                this.pos.push_order(order) 
 +                if (this.pos.config.iface_print_via_proxy) {
                      var receipt = currentOrder.export_for_printing();
                      this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
                          receipt: receipt, widget: self,
                      }));
 -                    this.pos.get('selectedOrder').destroy();    //finish order and go back to scan screen
 -                }else{
 +                    order.finalize();    //finish order and go back to scan screen
 +                } else {
                      this.pos_widget.screen_selector.set_current_screen(this.next_screen);
                  }
              }
 -
 -            // hide onscreen (iOS) keyboard 
 -            setTimeout(function(){
 -                document.activeElement.blur();
 -                $("input").blur();
 -            },250);
 -        },
 -        enable_numpad: function(){
 -            this.disable_numpad();  //ensure we don't register the callbacks twice
 -            this.numpad_state = this.pos_widget.numpad.state;
 -            if(this.numpad_state){
 -                this.numpad_state.reset();
 -                this.numpad_state.changeMode('payment');
 -                this.numpad_state.bind('set_value',   this.set_value, this);
 -                this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
 -            }
 -                    
 -        },
 -        disable_numpad: function(){
 -            if(this.numpad_state){
 -                this.numpad_state.unbind('set_value',  this.set_value);
 -                this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
 -            }
 -        },
 -      set_mode_back_to_payment: function() {
 -              this.numpad_state.set({mode: 'payment'});
 -      },
 -        set_value: function(val) {
 -            var selected_line =this.pos.get('selectedOrder').selected_paymentline;
 -            if(selected_line){
 -                selected_line.set_amount(val);
 -                selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);
 -            }
          },
      });
 +
  }
@@@ -511,7 -511,8 +511,7 @@@ class product_template(osv.osv)
          'warranty': fields.float('Warranty'),
          'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
          'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
 -        'state': fields.selection([('',''),
 -            ('draft', 'In Development'),
 +        'state': fields.selection([('draft', 'In Development'),
              ('sellable','Normal'),
              ('end','End of Lifecycle'),
              ('obsolete','Obsolete')], 'Status'),
              if ptype != 'standard_price':
                  res[product.id] = product[ptype] or 0.0
              else:
-                 res[product.id] = product.sudo()[ptype]
+                 company_id = product.env.user.company_id.id
+                 product = product.with_context(force_company=company_id)
+                 res[product.id] = res[product.id] = product.sudo()[ptype]
              if ptype == 'list_price':
                  res[product.id] += product._name == "product.product" and product.price_extra or 0.0
              if 'uom' in context:
          ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
          if isinstance(ids, (int, long)):
              ids = [ids]
 -        if 'uom_po_id' in vals:
 -            new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
 -            for product in self.browse(cr, uid, ids, context=context):
 -                old_uom = product.uom_po_id
 -                if old_uom.category_id.id != new_uom.category_id.id:
 -                    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,))
          if 'standard_price' in vals:
              for prod_template_id in ids:
                  self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
@@@ -1032,8 -1041,6 +1034,8 @@@ class product_product(osv.osv)
          return result
  
      def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
 +        if context is None:
 +            context = {}
          if not args:
              args = []
          if name:
                  res = ptrn.search(name)
                  if res:
                      ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
 +            # still no results, partner in context: search on supplier info as last hope to find something
 +            if not ids and context.get('partner_id'):
 +                supplier_ids = self.pool['product.supplierinfo'].search(
 +                    cr, user, [
 +                        ('name', '=', context.get('partner_id')),
 +                        '|',
 +                        ('product_code', operator, name),
 +                        ('product_name', operator, name)
 +                    ], context=context)
 +                if supplier_ids:
 +                    ids = self.search(cr, user, [('product_tmpl_id.seller_ids', 'in', supplier_ids)], limit=limit, context=context)
          else:
              ids = self.search(cr, user, args, limit=limit, context=context)
          result = self.name_get(cr, user, ids, context=context)
@@@ -1237,7 -1233,7 +1239,7 @@@ class product_supplierinfo(osv.osv)
          '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 receipt 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', copy=True),
 -        'company_id':fields.many2one('res.company','Company',select=1),
 +        'company_id':fields.many2one('res.company', string='Company',select=1),
      }
      _defaults = {
          'min_qty': 0.0,
  #
  ##############################################################################
  
 -from datetime import datetime
 -
 +import calendar
 +from datetime import datetime,date
 +from dateutil import relativedelta
 +import json
 +import time
  from openerp import api
  from openerp import SUPERUSER_ID
  from openerp import tools
@@@ -106,7 -103,7 +106,7 @@@ class project_issue(osv.Model)
          # lame hack to allow reverting search, should just work in the trivial case
          if read_group_order == 'stage_id desc':
              order = "%s desc" % order
 -        # retrieve section_id from the context and write the domain
 +        # retrieve team_id from the context and write the domain
          # - ('id', 'in', 'ids'): add columns that should be present
          # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
          # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
              res[issue.id] = {'progress' : progress}
          return res
  
 +    def _can_escalate(self, cr, uid, ids, field_name, arg, context=None):
 +        res = {}
 +        for issue in self.browse(cr, uid, ids, context=context):
 +            if issue.project_id.parent_id.type == 'contract':
 +                res[issue.id] = True
 +        return res
 +
      def on_change_project(self, cr, uid, ids, project_id, context=None):
          if project_id:
              project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
          'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
                                                 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
          'date_deadline': fields.date('Deadline'),
 -        'section_id': fields.many2one('crm.case.section', 'Sales Team', \
 +        'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id',\
                          select=True, help='Sales team to which Case belongs to.\
                               Define Responsible user and Email account for mail gateway.'),
          'partner_id': fields.many2one('res.partner', 'Contact', select=1),
          'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
          'date_action_last': fields.datetime('Last Action', readonly=1),
          'date_action_next': fields.datetime('Next Action', readonly=1),
 +        'can_escalate': fields.function(_can_escalate, type='boolean', string='Can Escalate'),
          '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),
              }),
      }
              return {'value': {'date_closed': fields.datetime.now()}}
          return {'value': {'date_closed': False}}
  
 -    def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
 +    def stage_find(self, cr, uid, cases, team_id, domain=[], order='sequence', context=None):
          """ Override of the base.stage method
              Parameter of the stage search taken from the issue:
              - type: stage type must be the same or 'both'
 -            - section_id: if set, stages must belong to this section or
 +            - team_id: if set, stages must belong to this team or
                be a default case
          """
          if isinstance(cases, (int, long)):
              cases = self.browse(cr, uid, cases, context=context)
 -        # collect all section_ids
 -        section_ids = []
 -        if section_id:
 -            section_ids.append(section_id)
 +        # collect all team_ids
 +        team_ids = []
 +        if team_id:
 +            team_ids.append(team_id)
          for task in cases:
              if task.project_id:
 -                section_ids.append(task.project_id.id)
 -        # OR all section_ids and OR with case_default
 +                team_ids.append(task.project_id.id)
 +        # OR all team_ids and OR with case_default
          search_domain = []
 -        if section_ids:
 -            search_domain += [('|')] * (len(section_ids)-1)
 -            for section_id in section_ids:
 -                search_domain.append(('project_ids', '=', section_id))
 +        if team_ids:
 +            search_domain += [('|')] * (len(team_ids)-1)
 +            for team_id in team_ids:
 +                search_domain.append(('project_ids', '=', team_id))
          search_domain += list(domain)
          # perform search, return the first found
          stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
@@@ -483,25 -472,13 +483,25 @@@ class project(osv.Model)
              project_id: Issue.search_count(cr,uid, [('project_id', '=', project_id), ('stage_id.fold', '=', False)], context=context)
              for project_id in ids
          }
 +    def _get_project_issue_data(self, cr, uid, ids, field_name, arg, context=None):
 +        obj = self.pool['project.issue']
 +        month_begin = date.today().replace(day=1)
 +        date_begin = (month_begin - relativedelta.relativedelta(months=self._period_number - 1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
 +        date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
 +        res = {}
 +        for id in ids:
 +            created_domain = [('project_id', '=', id), ('date_closed', '>=', date_begin ), ('date_closed', '<=', date_end )]
 +            res[id] = json.dumps(self.__get_bar_values(cr, uid, obj, created_domain, [ 'date_closed'], 'date_closed_count', 'date_closed', context=context))
 +        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="Issues",),
 -        'issue_ids': fields.one2many('project.issue', 'project_id',
 -                                     domain=[('stage_id.fold', '=', False)])
 +        'issue_ids': fields.one2many('project.issue', 'project_id', string="Issues",
 +                                     domain=[('date_closed', '!=', False)]),
 +        'monthly_issues': fields.function(_get_project_issue_data, type='char', readonly=True,
 +                                             string='Project Issue By Month')
      }
  
      def _check_escalation(self, cr, uid, ids, context=None):
@@@ -537,13 -514,6 +537,13 @@@ class account_analytic_account(osv.Mode
          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)
  
 +    def unlink(self, cr, uid, ids, context=None):
 +        proj_ids = self.pool['project.project'].search(cr, uid, [('analytic_account_id', 'in', ids)])
 +        has_issues = self.pool['project.issue'].search(cr, uid, [('project_id', 'in', proj_ids)], count=True, context=context)
 +        if has_issues:
 +            raise osv.except_osv(_('Warning!'), _('Please remove existing issues in the project linked to the accounts you want to delete.'))
 +        return super(account_analytic_account, self).unlink(cr, uid, ids, context=context)
 +
  
  class project_project(osv.Model):
      _inherit = 'project.project'
          if use_tasks and not use_issues:
              values['alias_model'] = 'project.task'
          elif not use_tasks and use_issues:
-             values['alias_model'] = 'project.issues'
+             values['alias_model'] = 'project.issue'
          return {'value': values}
  
      def create(self, cr, uid, vals, context=None):
@@@ -47,7 -47,7 +47,7 @@@
          <menuitem
              action="purchase_pricelist_version_action" id="menu_purchase_pricelist_version_action"
              parent="menu_purchase_config_pricelist" sequence="2" groups="product.group_purchase_pricelist"/>
 -        
 +
          <menuitem
              action="product.product_price_type_action" id="menu_product_pricelist_action2_purchase_type"
              parent="menu_purchase_config_pricelist" sequence="60" />
  
          <menuitem
               action="product.product_uom_categ_form_action" id="menu_purchase_uom_categ_form_action"
 -             parent="purchase.menu_product_in_config_purchase" sequence="5" />
 +             parent="purchase.menu_product_in_config_purchase" sequence="10" />
  
          <menuitem
                action="product.product_uom_form_action" id="menu_purchase_uom_form_action"
 -              parent="purchase.menu_product_in_config_purchase" sequence="10"/>
 -
 -        <menuitem
 -            id="menu_purchase_partner_cat" name="Address Book"
 -            parent="menu_purchase_config_purchase"/>
 -
 -        <menuitem
 -            action="base.action_partner_category_form" id="menu_partner_categories_in_form" name="Partner Tags"
 -            parent="purchase.menu_purchase_partner_cat" groups="base.group_no_one"/>
 +              parent="purchase.menu_product_in_config_purchase" sequence="5"/>
  
      <!--Supplier menu-->
      <menuitem id="base.menu_procurement_management_supplier_name" name="Suppliers"
            parent="base.menu_purchase_root" sequence="8"/>
  
        <menuitem name="Products by Category" id="menu_product_by_category_purchase_form" action="product.product_category_action"
 -           parent="menu_procurement_management_product" sequence="10"/>
 +           parent="menu_procurement_management_product" sequence="2"/>
  
        <menuitem name="Products" id="menu_procurement_partner_contact_form" action="product_normal_action_puchased"
 -          parent="menu_procurement_management_product"/>
 +          parent="menu_procurement_management_product" sequence="1"/>
  
          <record model="ir.ui.view" id="purchase_order_calendar">
              <field name="name">purchase.order.calendar</field>
                          <page string="Products">
                              <field name="order_line">
                                  <tree string="Purchase Order Lines" editable="bottom">
 -                                    <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
 +                                    <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)" context="{'partner_id': parent.partner_id}"/>
                                      <field name="name"/>
                                      <field name="date_planned"/>
                                      <field name="company_id" groups="base.group_multi_company" widget="selection"/>
 -                                    <field name="account_analytic_id" groups="purchase.group_analytic_accounting" domain="[('type','not in',('view','template'))]"/>
 +                                    <field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="purchase.group_analytic_accounting" domain="[('type','not in',('view','template'))]"/>
-                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
-                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
+                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
+                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
                                      <field name="price_unit"/>
                                      <field name="taxes_id" widget="many2many_tags" domain="[('parent_id','=',False),('type_tax_use','!=','sale')]"/>
                                      <field name="price_subtotal"/>
                      <sheet>
                          <group>
                              <group>
 -                                <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)"/>
 +                                <field name="product_id"
-                                     on_change="onchange_product_id(parent.pricelist_id,product_id,0,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context)" context="{'partner_id': parent.partner_id}"/>
++                                    on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,context)" context="{'partner_id': parent.partner_id}"/>
                                  <label for="product_qty"/>
                                  <div>
-                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
-                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
+                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
+                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
                                  </div>
                                  <field name="price_unit"/>
                              </group>
              <field name="model">product.template</field>
              <field name="inherit_id" ref="product.product_template_search_view"/>
              <field name="arch" type="xml">
-                 <filter name="filter_to_sell" position="before">
-                     <filter name="filter_to_purchase" string="To Purchase" icon="terp-accessories-archiver+" domain="[('purchase_ok', '=', 1)]"/>
-                 </filter>
                  <filter name="filter_to_sell" position="after">
                     <filter name="filter_to_purchase" string="Can be Purchased" icon="terp-accessories-archiver+" domain="[('purchase_ok', '=', 1)]"/>
                  </filter>
@@@ -9,22 -9,6 +9,22 @@@ openerp.report = function(instance) 
              error: c.rpc_error.bind(c)
          });
      }
 +    var show_pdf = function(session, response, c, options, self) {
 +        response.push("pdf_viewer")
 +        session.show_pdf({
 +            url: '/report/download',
 +            data: {data: JSON.stringify(response)},
 +            complete: function(){
 +                openerp.web.unblockUI();
 +                if (!self.dialog) {
 +                    options.on_close();
 +                }
 +                self.dialog_stop();
 +                window.scrollTo(0,0);
 +            },
 +            error: c.rpc_error.bind(c)
 +        });
 +    }
  
      instance.web.ActionManager = instance.web.ActionManager.extend({
          ir_actions_report_xml: function(action, options) {
@@@ -68,7 -52,7 +68,7 @@@
                  var c = openerp.webclient.crashmanager;
  
                  if (action.report_type == 'qweb-html') {
-                     window.open(report_url, '_blank', 'height=900,width=1280');
+                     window.open(report_url, '_blank', 'scrollbars=1,height=900,width=1280');
                      instance.web.unblockUI();
                  } else if (action.report_type === 'qweb-pdf') {
                      // Trigger the download of the pdf/controller report
@@@ -95,12 -79,7 +95,12 @@@ workers to print a pdf version of the r
   support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \
   target="_blank">wkhtmltopdf.org</a>'), true);
                          }
 -                        return trigger_download(self.session, response, c);
 +                        if(action.hasOwnProperty('pdf_viewer')){
 +                            return show_pdf(self.session, response, c, options, self);
 +                        }
 +                        else {
 +                            return trigger_download(self.session, response, c );
 +                        }
                      });
                  } else if (action.report_type === 'controller') {
                      return trigger_download(self.session, response, c);
@@@ -70,7 -70,7 +70,7 @@@ class share_wizard(osv.TransientModel)
          return group_id in self.pool.get('res.users').read(cr, uid, [uid], ['groups_id'], context=context)[0]['groups_id']
  
      def has_share(self, cr, uid, unused_param, context=None):
 -        return self.has_group(cr, uid, module='share', group_xml_id='group_share_user', context=context)
 +        return self.has_group(cr, uid, module='base', group_xml_id='group_no_one', context=context)
  
      def _user_type_selection(self, cr, uid, context=None):
          """Selection values may be easily overridden/extended via inheritance"""
          models = [x[1].model for x in relation_fields]
          model_obj = self.pool.get('ir.model')
          model_osv = self.pool[model.model]
-         for colinfo in model_osv._all_columns.itervalues():
-             coldef = colinfo.column
-             coltype = coldef._type
+         for field in model_osv._fields.itervalues():
+             ftype = field.type
              relation_field = None
-             if coltype in ttypes and colinfo.column._obj not in models:
-                 relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',coldef._obj)])[0]
+             if ftype in ttypes and field.comodel_name not in models:
+                 relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',field.comodel_name)])[0]
                  relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
-                 relation_osv = self.pool[coldef._obj]
+                 relation_osv = self.pool[field.comodel_name]
                  #skip virtual one2many fields (related, ...) as there is no reverse relationship
-                 if coltype == 'one2many' and hasattr(coldef, '_fields_id'):
+                 if ftype == 'one2many' and field.inverse_name:
                      # don't record reverse path if it's not a real m2o (that happens, but rarely)
-                     dest_model_ci = relation_osv._all_columns
-                     reverse_rel = coldef._fields_id
-                     if reverse_rel in dest_model_ci and dest_model_ci[reverse_rel].column._type == 'many2one':
+                     dest_fields = relation_osv._fields
+                     reverse_rel = field.inverse_name
+                     if reverse_rel in dest_fields and dest_fields[reverse_rel].type == 'many2one':
                          relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
                  local_rel_fields.append((relation_field, relation_model_browse))
                  for parent in relation_osv._inherits:
                      if parent not in models:
                          parent_model = self.pool[parent]
-                         parent_colinfos = parent_model._all_columns
+                         parent_fields = parent_model._fields
                          parent_model_browse = model_obj.browse(cr, UID_ROOT,
                                                                 model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
-                         if relation_field and coldef._fields_id in parent_colinfos:
+                         if relation_field and field.inverse_name in parent_fields:
                              # inverse relationship is available in the parent
                              local_rel_fields.append((relation_field, parent_model_browse))
                          else:
                              # TODO: can we setup a proper rule to restrict inherited models
                              # in case the parent does not contain the reverse m2o?
                              local_rel_fields.append((None, parent_model_browse))
-                 if relation_model_id != model.id and coltype in ['one2many', 'many2many']:
+                 if relation_model_id != model.id and ftype in ['one2many', 'many2many']:
                      local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
-                         [coltype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
+                         [ftype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
          return local_rel_fields
  
      def _get_relationship_classes(self, cr, uid, model, context=None):
                       _('Action and Access Mode are required to create a shared access.'),
                       context=context)
          self._assert(self.has_share(cr, uid, wizard_data, context=context),
 -                     _('You must be a member of the Share/User group to use the share wizard.'),
 +                     _('You must be a member of the Technical group to use the share wizard.'),
                       context=context)
          if wizard_data.user_type == 'emails':
              self._assert((wizard_data.new_users or wizard_data.email_1 or wizard_data.email_2 or wizard_data.email_3),
@@@ -85,6 -85,8 +85,6 @@@ instance.web.FormView = instance.web.Vi
       * @param {instance.web.Session} session the current openerp session
       * @param {instance.web.DataSet} dataset the dataset this view will work with
       * @param {String} view_id the identifier of the OpenERP view object
 -     * @param {Object} options
 -     *                  - resize_textareas : [true|false|max_height]
       *
       * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
       */
                  opacity: '1',
                  filter: 'alpha(opacity = 100)'
              });
 +            instance.web.bus.trigger('form_view_shown', self);
          });
      },
      do_hide: function () {
              this.$pager.remove();
          if (this.get("actual_mode") === "create")
              return;
 -        this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
 +        this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
          if (this.options.$pager) {
              this.$pager.appendTo(this.options.$pager);
          } else {
                  if (menu) {
                      menu.do_reload_needaction();
                  }
 +                instance.web.bus.trigger('form_view_saved', self);
              });
          }).always(function(){
              $(e.target).attr("disabled", false);
@@@ -2614,10 -2614,10 +2614,10 @@@ instance.web.form.FieldCharDomain = ins
  
  instance.web.DateTimeWidget = instance.web.Widget.extend({
      template: "web.datepicker",
 -    jqueryui_object: 'datetimepicker',
      type_of_date: "datetime",
      events: {
 -        'change .oe_datepicker_master': 'change_datetime',
 +        'dp.change .oe_datepicker_main': 'change_datetime',
 +        'dp.show .oe_datepicker_main': 'set_datetime_default',
          'keypress .oe_datepicker_master': 'change_datetime',
      },
      init: function(parent) {
      },
      start: function() {
          var self = this;
 +        var l10n = _t.database.parameters;
 +        var options = {
 +            pickTime: true,
 +            useSeconds: true,
 +            startDate: moment({ y: 1900 }),
 +            endDate: moment().add(200, "y"),
 +            calendarWeeks: true,
 +            icons : {
 +                time: 'fa fa-clock-o',
 +                date: 'fa fa-calendar',
 +                up: 'fa fa-chevron-up',
 +                down: 'fa fa-chevron-down'
 +               },
 +            language : moment.locale(),
 +            format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
 +        };
          this.$input = this.$el.find('input.oe_datepicker_master');
 -        this.$input_picker = this.$el.find('input.oe_datepicker_container');
 -
 -        $.datepicker.setDefaults({
 -            clearText: _t('Clear'),
 -            clearStatus: _t('Erase the current date'),
 -            closeText: _t('Done'),
 -            closeStatus: _t('Close without change'),
 -            prevText: _t('<Prev'),
 -            prevStatus: _t('Show the previous month'),
 -            nextText: _t('Next>'),
 -            nextStatus: _t('Show the next month'),
 -            currentText: _t('Today'),
 -            currentStatus: _t('Show the current month'),
 -            monthNames: Date.CultureInfo.monthNames,
 -            monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
 -            monthStatus: _t('Show a different month'),
 -            yearStatus: _t('Show a different year'),
 -            weekHeader: _t('Wk'),
 -            weekStatus: _t('Week of the year'),
 -            dayNames: Date.CultureInfo.dayNames,
 -            dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
 -            dayNamesMin: Date.CultureInfo.shortestDayNames,
 -            dayStatus: _t('Set DD as first week day'),
 -            dateStatus: _t('Select D, M d'),
 -            firstDay: Date.CultureInfo.firstDayOfWeek,
 -            initStatus: _t('Select a date'),
 -            isRTL: false
 -        });
 -        $.timepicker.setDefaults({
 -            timeOnlyTitle: _t('Choose Time'),
 -            timeText: _t('Time'),
 -            hourText: _t('Hour'),
 -            minuteText: _t('Minute'),
 -            secondText: _t('Second'),
 -            currentText: _t('Now'),
 -            closeText: _t('Done')
 -        });
 -
 -        this.picker({
 -            onClose: this.on_picker_select,
 -            onSelect: this.on_picker_select,
 -            changeMonth: true,
 -            changeYear: true,
 -            showWeek: true,
 -            showButtonPanel: true,
 -            firstDay: Date.CultureInfo.firstDayOfWeek
 -        });
 -        // Some clicks in the datepicker dialog are not stopped by the
 -        // datepicker and "bubble through", unexpectedly triggering the bus's
 -        // click event. Prevent that.
 -        this.picker('widget').click(function (e) { e.stopPropagation(); });
 -
 -        this.$el.find('img.oe_datepicker_trigger').click(function() {
 -            if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
 -                self.$input.focus();
 -                return;
 -            }
 -            self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
 -            self.$input_picker.show();
 -            self.picker('show');
 -            self.$input_picker.hide();
 -        });
 +        if (this.type_of_date === 'date') {
 +            options['pickTime'] = false;
 +            options['useSeconds'] = false;
 +            options['format'] = instance.web.normalize_format(l10n.date_format);
 +        }
 +        this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
          this.set_readonly(false);
          this.set({'value': false});
      },
 -    picker: function() {
 -        return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
 -    },
 -    on_picker_select: function(text, instance_) {
 -        var date = this.picker('getDate');
 -        this.$input
 -            .val(date ? this.format_client(date) : '')
 -            .change()
 -            .focus();
 -    },
      set_value: function(value_) {
          this.set({'value': value_});
          this.$input.val(value_ ? this.format_client(value_) : '');
      },
      set_value_from_ui_: function() {
          var value_ = this.$input.val() || false;
 -        this.set({'value': this.parse_client(value_)});
 +        this.set_value(this.parse_client(value_));
      },
      set_readonly: function(readonly) {
          this.readonly = readonly;
          this.$input.prop('readonly', this.readonly);
 -        this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
      },
      is_valid_: function() {
          var value_ = this.$input.val();
      format_client: function(v) {
          return instance.web.format_value(v, {"widget": this.type_of_date});
      },
 +    set_datetime_default: function(){
 +        //when opening datetimepicker the date and time by default should be the one from
 +        //the input field if any or the current day otherwise
 +        if (this.type_of_date === 'datetime') {
 +            value = moment().second(0);
 +            if (this.$input.val().length !== 0 && this.is_valid_()){
 +                var value = this.$input.val();
 +            }
 +            this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
 +        }
 +    },
      change_datetime: function(e) {
          if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
              this.set_value_from_ui_();
  });
  
  instance.web.DateWidget = instance.web.DateTimeWidget.extend({
 -    jqueryui_object: 'datepicker',
      type_of_date: "date"
  });
  
@@@ -3157,9 -3198,9 +3157,9 @@@ instance.web.form.FieldRadio = instance
          this._super(field_manager, node);
          this.selection = _.clone(this.field.selection) || [];
          this.domain = false;
 +        this.uniqueId = _.uniqueId("radio");
      },
      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();
@@@ -3971,6 -4012,7 +3971,6 @@@ instance.web.form.FieldOne2Many = insta
      disable_utility_classes: true,
      init: function(field_manager, node) {
          this._super(field_manager, node);
 -        lazy_build_o2m_kanban_view();
          this.is_loaded = $.Deferred();
          this.initial_is_loaded = this.is_loaded;
          this.form_last_update = $.Deferred();
          /**
           * Returns the current active view if any.
           */
 -        if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
 -            this.viewmanager.views[this.viewmanager.active_view] &&
 -            this.viewmanager.views[this.viewmanager.active_view].controller) {
 -            return {
 -                type: this.viewmanager.active_view,
 -                controller: this.viewmanager.views[this.viewmanager.active_view].controller
 -            };
 -        }
 +        return (this.viewmanager && this.viewmanager.active_view);
      },
      set_value: function(value_) {
          value_ = value_ || [];
      save_any_view: function() {
          var view = this.get_active_view();
          if (view) {
 -            if (this.viewmanager.active_view === "form") {
 +            if (this.viewmanager.active_view.type === "form") {
                  if (view.controller.is_initialized.state() !== 'resolved') {
                      return $.when(false);
                  }
                  return $.when(view.controller.save());
 -            } else if (this.viewmanager.active_view === "list") {
 +            } else if (this.viewmanager.active_view.type === "list") {
                  return $.when(view.controller.ensure_saved());
              }
          }
          if (!view){
              return true;
          }
 -        switch (this.viewmanager.active_view) {
 +        switch (this.viewmanager.active_view.type) {
          case 'form':
              return _(view.controller.fields).chain()
                  .invoke('is_valid')
@@@ -4265,9 -4314,10 +4265,9 @@@ instance.web.form.One2ManyViewManager 
      template: 'One2Many.viewmanager',
      init: function(parent, dataset, views, flags) {
          this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
 -        this.registry = this.registry.extend({
 +        this.registry = instance.web.views.extend({
              list: 'instance.web.form.One2ManyListView',
              form: 'instance.web.form.One2ManyFormView',
 -            kanban: 'instance.web.form.One2ManyKanbanView',
          });
          this.__ignore_blur = false;
      },
@@@ -4339,28 -4389,25 +4339,25 @@@ 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.fields_view || !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._inhibit_on_change_flag = true;
+                 field.set_value(r.attributes[field.name]);
+                 field._inhibit_on_change_flag = false;
+             });
+             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()) {
              window.confirm = confirm;
          }
      },
 -    reload_record: function (record) {
 -        // Evict record.id from cache to ensure it will be reloaded correctly
 -        this.dataset.evict_record(record.get('id'));
 +    reload_record: function (record, options) {
 +        if (!options || !options['do_not_evict']) {
 +            // Evict record.id from cache to ensure it will be reloaded correctly
 +            this.dataset.evict_record(record.get('id'));
 +        }
  
          return this._super(record);
      }
@@@ -4537,6 -4582,13 +4534,6 @@@ instance.web.form.One2ManyFormView = in
      }
  });
  
 -var lazy_build_o2m_kanban_view = function() {
 -    if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
 -        return;
 -    instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
 -    });
 -};
 -
  instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
      template: "FieldMany2ManyTags",
      tag_template: "FieldMany2ManyTag",
@@@ -5129,7 -5181,8 +5126,7 @@@ instance.web.form.AbstractFormPopup = i
          this.domain = domain || [];
          this.context = context || {};
          this.options = options;
 -        _.defaults(this.options, {
 -        });
 +        _.defaults(this.options, {});
      },
      init_dataset: function() {
          var self = this;
          if (this.options.alternative_form_view) {
              this.view_form.set_embedded_view(this.options.alternative_form_view);
          }
 -        this.view_form.appendTo(this.$el.find(".oe_popup_form"));
 +        this.view_form.appendTo(this.$(".oe_popup_form").show());
          this.view_form.on("form_view_loaded", self, function() {
              var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
              self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
@@@ -5274,20 -5327,22 +5271,20 @@@ instance.web.form.SelectCreatePopup = i
          this.display_popup();
      },
      start: function() {
 -        var self = this;
          this.init_dataset();
          if (this.options.initial_view == "search") {
 -            instance.web.pyeval.eval_domains_and_contexts({
 +            var context = instance.web.pyeval.sync_eval_domains_and_contexts({
                  domains: [],
                  contexts: [this.context]
 -            }).done(function (results) {
 -                var search_defaults = {};
 -                _.each(results.context, function (value_, key) {
 -                    var match = /^search_default_(.*)$/.exec(key);
 -                    if (match) {
 -                        search_defaults[match[1]] = value_;
 -                    }
 -                });
 -                self.setup_search_view(search_defaults);
 +            }).context;
 +            var search_defaults = {};
 +            _.each(context, function (value_, key) {
 +                var match = /^search_default_(.*)$/.exec(key);
 +                if (match) {
 +                    search_defaults[match[1]] = value_;
 +                }
              });
 +            this.setup_search_view(search_defaults);
          } else { // "form"
              this.new_object();
          }
          if (this.searchview) {
              this.searchview.destroy();
          }
 -        if (this.searchview_drawer) {
 -            this.searchview_drawer.destroy();
 -        }
 +        var $buttons = this.$('.o-search-options');
          this.searchview = new instance.web.SearchView(this,
 -                this.dataset, false,  search_defaults);
 -        this.searchview_drawer = new instance.web.SearchViewDrawer(this, this.searchview);
 +                this.dataset, false,  search_defaults, {$buttons: $buttons});
          this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
              if (self.initial_ids) {
                  self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
                  self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
              }
          });
 -        this.searchview.on("search_view_loaded", self, function() {
 +        this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
 +            self.searchview.toggle_visibility(true);
              self.view_list = new instance.web.form.SelectCreateListView(self,
                      self.dataset, false,
                      _.extend({'deletable': false,
                  e.cancel = true;
              });
              self.view_list.popup = self;
 -            self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
 +            self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
                  self.view_list.do_show();
              }).then(function() {
                  self.searchview.do_search();
                      self.new_object();
                  });
              });
 -        });
 -        this.searchview.appendTo(this.$(".oe_popup_search"));
 +        });        
      },
      do_search: function(domains, contexts, groupbys) {
          var self = this;
      },
      new_object: function() {
          if (this.searchview) {
 -            this.searchview.hide();
 +            this.searchview.do_hide();
          }
          if (this.view_list) {
              this.view_list.do_hide();
@@@ -3,7 -3,6 +3,7 @@@
   *---------------------------------------------------------*/
  
  (function() {
 +"use strict";
  
  var instance = openerp;
  openerp.web.views = {};
@@@ -11,16 -10,20 +11,16 @@@ var QWeb = instance.web.qweb
      _t = instance.web._t;
  
  instance.web.ActionManager = instance.web.Widget.extend({
 +    template: "ActionManager",
      init: function(parent) {
          this._super(parent);
          this.inner_action = null;
          this.inner_widget = null;
 +        this.webclient = parent;
          this.dialog = null;
          this.dialog_widget = null;
 -        this.breadcrumbs = [];
 -        this.on('history_back', this, function() {
 -            return this.history_back();
 -        });
 -    },
 -    start: function() {
 -        this._super.apply(this, arguments);
 -        this.$el.on('click', 'a.oe_breadcrumb_item', this.on_breadcrumb_clicked);
 +        this.widgets = [];
 +        this.on('history_back', this, this.proxy('history_back'));
      },
      dialog_stop: function (reason) {
          if (this.dialog) {
          this.dialog = null;
      },
      /**
 -     * Add a new item to the breadcrumb
 -     *
 -     * If the title of an item is an array, the multiple title mode is in use.
 -     * (eg: a widget with multiple views might need to display a title for each view)
 -     * In multiple title mode, the show() callback can check the index it receives
 -     * in order to detect which of its titles has been clicked on by the user.
 +     * Add a new widget to the action manager
       *
 -     * @param {Object} item breadcrumb item
 -     * @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added
 -     * @param {Function} [item.show] triggered whenever the widget should be shown back
 -     * @param {Function} [item.hide] triggered whenever the widget should be shown hidden
 -     * @param {Function} [item.destroy] triggered whenever the widget should be destroyed
 -     * @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb
 -     * @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb
 +     * widget: typically, widgets added are instance.web.ViewManager.  The action manager
 +     *      uses this list of widget to handle the breadcrumbs.
 +     * action: new action
 +     * clear_breadcrumbs: boolean, if true, current widgets are destroyed
       */
 -    push_breadcrumb: function(item) {
 -        var last = this.breadcrumbs.slice(-1)[0];
 -        if (last) {
 -            last.hide();
 -        }
 -        item = _.extend({
 -            show: function(index) {
 -                this.widget.$el.show();
 -            },
 -            hide: function() {
 -                this.widget.$el.hide();
 -            },
 -            destroy: function() {
 -                this.widget.destroy();
 -            },
 -            get_title: function() {
 -                return this.title || this.widget.get('title');
 -            }
 -        }, item);
 -        item.id = _.uniqueId('breadcrumb_');
 -        this.breadcrumbs.push(item);
 -    },
 -    history_back: function() {
 -        var last = this.breadcrumbs.slice(-1)[0];
 -        if (!last) {
 -            return false;
 +    push_widget: function(widget, action, clear_breadcrumbs) {
 +        var self = this,
 +            old_widget = this.inner_widget;
 +
 +        if (clear_breadcrumbs) {
 +            var to_destroy = this.widgets;
 +            this.widgets = [];
          }
 -        var title = last.get_title();
 -        if (_.isArray(title) && title.length > 1) {
 -            return this.select_breadcrumb(this.breadcrumbs.length - 1, title.length - 2);
 -        } else if (this.breadcrumbs.length === 1) {
 -            // Only one single titled item in breadcrumb, most of the time you want to trigger back to home
 -            return false;
 +        if (widget instanceof instance.web.Widget) {
 +            var title = widget.get('title') || action.display_name || action.name;
 +            widget.set('title', title);
 +            this.widgets.push(widget);
          } else {
 -            var prev = this.breadcrumbs[this.breadcrumbs.length - 2];
 -            title = prev.get_title();
 -            return this.select_breadcrumb(this.breadcrumbs.length - 2, _.isArray(title) ? title.length - 1 : undefined);
 +            this.widgets.push({
 +                view_stack: [{
 +                    controller: {get: function () {return action.display_name || action.name; }},
 +                }],
 +                destroy: function () {},
 +            });
          }
 -    },
 -    on_breadcrumb_clicked: function(ev) {
 -        var $e = $(ev.target);
 -        var id = $e.data('id');
 -        var index;
 -        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 -            if (this.breadcrumbs[i].id == id) {
 -                index = i;
 -                break;
 +        this.inner_action = action;
 +        this.inner_widget = widget;
 +        return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
 +            (action.target !== 'inline') && (!action.flags.headless) && widget.$header && widget.$header.show();
 +            old_widget && old_widget.$el.hide();
 +            if (clear_breadcrumbs) {
 +                self.clear_widgets(to_destroy)
              }
 -        }
 -        var subindex = $e.parent().find('a.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e);
 -        this.select_breadcrumb(index, subindex);
 +        });
      },
 -    select_breadcrumb: function(index, subindex) {
 -        var next_item = this.breadcrumbs[index + 1];
 -        if (next_item && next_item.on_reverse_breadcrumb) {
 -            next_item.on_reverse_breadcrumb(this.breadcrumbs[index].widget);
 -        }
 -        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 -            if (i > index) {
 -                if (this.remove_breadcrumb(i) === false) {
 -                    return false;
 -                }
 +    get_breadcrumbs: function () {
 +        return _.flatten(_.map(this.widgets, function (widget) {
 +            if (widget instanceof instance.web.ViewManager) {
 +                return widget.view_stack.map(function (view, index) { 
 +                    return {
 +                        title: view.controller.get('title') || widget.title,
 +                        index: index,
 +                        widget: widget,
 +                    }; 
 +                });
 +            } else {
 +                return {title: widget.get('title'), widget: widget };
              }
 -        }
 -        var item = this.breadcrumbs[index];
 -        item.show(subindex);
 -        this.inner_widget = item.widget;
 -        this.inner_action = item.action;
 -        return true;
 -    },
 -    clear_breadcrumbs: function() {
 -        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 -            if (this.remove_breadcrumb(0) === false) {
 -                break;
 +        }), true);
 +    },
 +    get_title: function () {
 +        if (this.widgets.length === 1) {
 +            // horrible hack to display the action title instead of "New" for the actions
 +            // that use a form view to edit something that do not correspond to a real model
 +            // for example, point of sale "Your Session" or most settings form,
 +            var widget = this.widgets[0];
 +            if (widget instanceof instance.web.ViewManager && widget.view_stack.length === 1) {
 +                return widget.title;
              }
          }
 +        return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
      },
 -    remove_breadcrumb: function(index) {
 -        var item = this.breadcrumbs.splice(index, 1)[0];
 -        if (item) {
 -            var dups = _.filter(this.breadcrumbs, function(it) {
 -                return item.widget === it.widget;
 -            });
 -            if (!dups.length) {
 -                if (this.getParent().has_uncommitted_changes()) {
 -                    this.inner_widget = item.widget;
 -                    this.inner_action = item.action;
 -                    this.breadcrumbs.splice(index, 0, item);
 -                    return false;
 -                } else {
 -                    item.destroy();
 -                }
 +    history_back: function() {
 +        var widget = _.last(this.widgets);
 +        if (widget instanceof instance.web.ViewManager) {
 +            var nbr_views = widget.view_stack.length;
 +            if (nbr_views > 1) {
 +                this.select_widget(widget, nbr_views - 2);
 +            } else if (this.widgets.length > 1) {
 +                widget = this.widgets[this.widgets.length -2];
 +                nbr_views = widget.view_stack.length;
 +                this.select_view(widget, nbr_views - 2)
              }
 -        }
 -        var last_widget = this.breadcrumbs.slice(-1)[0];
 -        if (last_widget) {
 -            this.inner_widget = last_widget.widget;
 -            this.inner_action = last_widget.action;
 +        } else if (this.widgets.length > 1) {
 +            widget = this.widgets[this.widgets.length - 2];
 +            var index = widget.view_stack && widget.view_stack.length - 1;
 +            this.select_widget(widget, index);
          }
      },
 -    add_breadcrumb_url: function (url, label) {
 -        // Add a pseudo breadcrumb that will redirect to an url
 -        this.push_breadcrumb({
 -            show: function() {
 -                instance.web.redirect(url);
 -            },
 -            hide: function() {},
 -            destroy: function() {},
 -            get_title: function() {
 -                return label;
 -            }
 +    select_widget: function(widget, index) {
 +        var self = this;
 +        if (this.webclient.has_uncommitted_changes()) {
 +            return false;
 +        }
 +        var widget_index = this.widgets.indexOf(widget),
 +            def = $.when(widget.select_view && widget.select_view(index));
 +
 +        def.done(function () {
 +            _.each(self.widgets.splice(widget_index + 1), function (w) {
 +                w.destroy();
 +            });
 +            self.inner_widget = _.last(self.widgets);
 +            self.inner_widget.display_breadcrumbs && self.inner_widget.display_breadcrumbs();
 +            self.inner_widget.$el.show();
          });
      },
 -    get_title: function() {
 -        var titles = [];
 -        for (var i = 0; i < this.breadcrumbs.length; i += 1) {
 -            var item = this.breadcrumbs[i];
 -            var tit = item.get_title();
 -            if (item.hide_breadcrumb) {
 -                continue;
 -            }
 -            if (!_.isArray(tit)) {
 -                tit = [tit];
 -            }
 -            for (var j = 0; j < tit.length; j += 1) {
 -                var label = _.escape(tit[j]);
 -                if (i === this.breadcrumbs.length - 1 && j === tit.length - 1) {
 -                    titles.push(_.str.sprintf('<span class="oe_breadcrumb_item">%s</span>', label));
 -                } else {
 -                    titles.push(_.str.sprintf('<a href="#" class="oe_breadcrumb_item" data-id="%s">%s</a>', item.id, label));
 -                }
 -            }
 +    clear_widgets: function(widgets) {
 +        _.invoke(widgets || this.widgets, 'destroy');
 +        if (!widgets) {
 +            this.widgets = [];
 +            this.inner_widget = null;
          }
 -        return titles.join(' <span class="oe_fade">/</span> ');
      },
      do_push_state: function(state) {
 +        if (!this.webclient || !this.webclient.do_push_state || this.dialog) {
 +            return;
 +        }
          state = state || {};
 -        if (this.getParent() && this.getParent().do_push_state) {
 -            if (this.inner_action) {
 -                if (this.inner_action._push_me === false) {
 -                    // this action has been explicitly marked as not pushable
 -                    return;
 -                }
 -                state['title'] = this.inner_action.name;
 -                if(this.inner_action.type == 'ir.actions.act_window') {
 -                    state['model'] = this.inner_action.res_model;
 -                }
 -                if (this.inner_action.menu_id) {
 -                    state['menu_id'] = this.inner_action.menu_id;
 -                }
 -                if (this.inner_action.id) {
 -                    state['action'] = this.inner_action.id;
 -                } else if (this.inner_action.type == 'ir.actions.client') {
 -                    state['action'] = this.inner_action.tag;
 -                    var params = {};
 -                    _.each(this.inner_action.params, function(v, k) {
 -                        if(_.isString(v) || _.isNumber(v)) {
 -                            params[k] = v;
 -                        }
 -                    });
 -                    state = _.extend(params || {}, state);
 -                }
 -                if (this.inner_action.context) {
 -                    var active_id = this.inner_action.context.active_id;
 -                    if (active_id) {
 -                        state["active_id"] = active_id;
 -                    }
 -                    var active_ids = this.inner_action.context.active_ids;
 -                    if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
 -                        // We don't push active_ids if it's a single element array containing the active_id
 -                        // This makes the url shorter in most cases.
 -                        state["active_ids"] = this.inner_action.context.active_ids.join(',');
 +        if (this.inner_action) {
 +            if (this.inner_action._push_me === false) {
 +                // this action has been explicitly marked as not pushable
 +                return;
 +            }
 +            state.title = this.get_title(); 
 +            if(this.inner_action.type == 'ir.actions.act_window') {
 +                state.model = this.inner_action.res_model;
 +            }
 +            if (this.inner_action.menu_id) {
 +                state.menu_id = this.inner_action.menu_id;
 +            }
 +            if (this.inner_action.id) {
 +                state.action = this.inner_action.id;
 +            } else if (this.inner_action.type == 'ir.actions.client') {
 +                state.action = this.inner_action.tag;
 +                var params = {};
 +                _.each(this.inner_action.params, function(v, k) {
 +                    if(_.isString(v) || _.isNumber(v)) {
 +                        params[k] = v;
                      }
 -                }
 +                });
 +                state = _.extend(params || {}, state);
              }
 -            if(!this.dialog) {
 -                this.getParent().do_push_state(state);
 +            if (this.inner_action.context) {
 +                var active_id = this.inner_action.context.active_id;
 +                if (active_id) {
 +                    state.active_id = active_id;
 +                }
 +                var active_ids = this.inner_action.context.active_ids;
 +                if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
 +                    // We don't push active_ids if it's a single element array containing the active_id
 +                    // This makes the url shorter in most cases.
 +                    state.active_ids = this.inner_action.context.active_ids.join(',');
 +                }
              }
          }
 +        this.webclient.do_push_state(state);
      },
      do_load_state: function(state, warm) {
          var self = this,
              action_loaded;
 -        if (!warm && 'return_label' in state) {
 -            var return_url = state.return_url || document.referrer;
 -            if (return_url) {
 -                this.add_breadcrumb_url(return_url, state.return_label);
 -            }
 -        }
          if (state.action) {
              if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) {
                  var action_client = {
              });
          }
  
 +        instance.web.bus.trigger('action', action);
 +
          // Ensure context & domain are evaluated and can be manipulated/used
          var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
          action.context = instance.web.pyeval.eval('context', ncontext);
          var type = action.type.replace(/\./g,'_');
          var popup = action.target === 'new';
          var inline = action.target === 'inline' || action.target === 'inlineview';
+         var form = _.str.startsWith(action.view_mode, 'form');
          action.flags = _.defaults(action.flags || {}, {
              views_switcher : !popup && !inline,
              search_view : !popup && !inline,
              action_buttons : !popup && !inline,
              sidebar : !popup && !inline,
-             pager : !popup && !inline,
+             pager : (!popup || !form) && !inline,
              display_title : !popup,
              search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
          });
      },
      null_action: function() {
          this.dialog_stop();
 -        this.clear_breadcrumbs();
 +        this.clear_widgets();
      },
      /**
       *
       * @return {*}
       */
      ir_actions_common: function(executor, options) {
 -        if (this.inner_widget && executor.action.target !== 'new') {
 -            if (this.getParent().has_uncommitted_changes()) {
 -                return $.Deferred().reject();
 -            } else if (options.clear_breadcrumbs) {
 -                this.clear_breadcrumbs();
 -            }
 -        }
 -        var widget = executor.widget();
 +        var widget;
          if (executor.action.target === 'new') {
              var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
              if (pre_dialog){
                  }
              };
              this.dialog.on("closing", null, this.dialog.on_close);
 +            widget = executor.widget();
              if (widget instanceof instance.web.ViewManager) {
                  _.extend(widget.flags, {
                      $buttons: this.dialog.$buttons,
                      footer_to_buttons: true,
                  });
 +                if (widget.action.view_mode === 'form') {
 +                    widget.flags.headless = true;
 +                }
              }
              this.dialog_widget = widget;
              this.dialog_widget.setParent(this.dialog);
              var initialized = this.dialog_widget.appendTo(this.dialog.$el);
              this.dialog.open();
              return initialized;
 -        } else  {
 -            // explicitly passing a closing action to dialog_stop() prevents
 -            // it from reloading the original form view - we're opening a
 -            // completely new action anyway
 -            this.dialog_stop(executor.action);
 -            this.inner_action = executor.action;
 -            this.inner_widget = widget;
 -            executor.post_process(widget);
 -            return this.inner_widget.appendTo(this.$el);
          }
 +        if (this.inner_widget && this.webclient.has_uncommitted_changes()) {
 +            return $.Deferred().reject();
 +        }
 +        widget = executor.widget();
 +        this.dialog_stop(executor.action);
 +        return this.push_widget(widget, executor.action, options.clear_breadcrumbs);
      },
      ir_actions_act_window: function (action, options) {
          var self = this;
  
          return this.ir_actions_common({
 -            widget: function () { return new instance.web.ViewManagerAction(self, action); },
 +            widget: function () { 
 +                return new instance.web.ViewManager(self, null, null, null, action); 
 +            },
              action: action,
              klass: 'oe_act_window',
 -            post_process: function (widget) {
 -                widget.add_breadcrumb({
 -                    on_reverse_breadcrumb: options.on_reverse_breadcrumb,
 -                    hide_breadcrumb: options.hide_breadcrumb,
 -                });
 -            },
          }, options);
      },
      ir_actions_client: function (action, options) {
              widget: function () { return new ClientWidget(self, action); },
              action: action,
              klass: 'oe_act_client',
 -            post_process: function(widget) {
 -                self.push_breadcrumb({
 -                    widget: widget,
 -                    title: action.name,
 -                    on_reverse_breadcrumb: options.on_reverse_breadcrumb,
 -                    hide_breadcrumb: options.hide_breadcrumb,
 -                });
 -                if (action.tag !== 'reload') {
 -                    self.do_push_state({});
 -                }
 -            }
 -        }, options);
 +        }, options).then(function () {
 +            if (action.tag !== 'reload') {self.do_push_state({});}
 +        });
      },
      ir_actions_act_window_close: function (action, options) {
          if (!this.dialog) {
  
  instance.web.ViewManager =  instance.web.Widget.extend({
      template: "ViewManager",
 -    init: function(parent, dataset, views, flags) {
 +    /**
 +     * @param {Object} [dataset] null object (... historical reasons)
 +     * @param {Array} [views] List of [view_id, view_type]
 +     * @param {Object} [flags] various boolean describing UI state
 +     */
 +    init: function(parent, dataset, views, flags, action) {
 +        if (action) {
 +            var flags = action.flags || {};
 +            if (!('auto_search' in flags)) {
 +                flags.auto_search = action.auto_search !== false;
 +            }
 +            if (action.res_model === 'board.board' && action.view_mode === 'form') {
 +                action.target = 'inline';
 +                // Special case for Dashboards
 +                _.extend(flags, {
 +                    views_switcher : false,
 +                    display_title : false,
 +                    search_view : false,
 +                    pager : false,
 +                    sidebar : false,
 +                    action_buttons : false
 +                });
 +            }
 +            this.action = action;
 +            this.action_manager = parent;
 +            var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
 +            if (action.res_id) {
 +                dataset.ids.push(action.res_id);
 +                dataset.index = 0;
 +            }
 +            views = action.views;
 +        }
 +        var self = this;
          this._super(parent);
 -        this.url_states = {};
 -        this.model = dataset ? dataset.model : undefined;
 +
 +        this.flags = flags || {};
          this.dataset = dataset;
 -        this.searchview = null;
 +        this.view_order = [];
 +        this.url_states = {};
 +        this.views = {};
 +        this.view_stack = []; // used for breadcrumbs
          this.active_view = null;
 -        this.views_src = _.map(views, function(x) {
 -            if (x instanceof Array) {
 -                var view_type = x[1];
 -                var View = instance.web.views.get_object(view_type, true);
 -                var view_label = View ? View.prototype.display_name : (void 'nope');
 -                return {
 -                    view_id: x[0],
 -                    view_type: view_type,
 +        this.searchview = null;
 +        this.active_search = null;
 +        this.registry = instance.web.views;
 +        this.title = this.action && this.action.name;
 +
 +        _.each(views, function (view) {
 +            var view_type = view[1] || view.view_type,
 +                View = instance.web.views.get_object(view_type, true),
 +                view_label = View ? View.prototype.display_name: (void 'nope'),
 +                view = {
 +                    controller: null,
 +                    options: view.options || {},
 +                    view_id: view[0] || view.view_id,
 +                    type: view_type,
                      label: view_label,
 +                    embedded_view: view.embedded_view,
 +                    title: self.action && self.action.name,
                      button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
                  };
 -            } else {
 -                return x;
 -            }
 +            self.view_order.push(view);
 +            self.views[view_type] = view;
          });
 -        this.ActionManager = parent;
 -        this.views = {};
 -        this.flags = flags || {};
 -        this.registry = instance.web.views;
 -        this.views_history = [];
 -        this.view_completely_inited = $.Deferred();
 +        this.multiple_views = (self.view_order.length - ('form' in this.views ? 1 : 0)) > 1;
      },
      /**
       * @returns {jQuery.Deferred} initial view loading promise
       */
      start: function() {
 -        this._super();
          var self = this;
 -        this.$el.find('.oe_view_manager_switch a').click(function() {
 -            self.switch_mode($(this).data('view-type'));
 -        }).tooltip();
 -        var views_ids = {};
 -        _.each(this.views_src, function(view) {
 -            self.views[view.view_type] = $.extend({}, view, {
 -                deferred : $.Deferred(),
 -                controller : null,
 -                options : _.extend({
 -                    $buttons : self.$el.find('.oe_view_manager_buttons'),
 -                    $sidebar : self.flags.sidebar ? self.$el.find('.oe_view_manager_sidebar') : undefined,
 -                    $pager : self.$el.find('.oe_view_manager_pager'),
 -                    action : self.action,
 -                    action_views_ids : views_ids
 -                }, self.flags, self.flags[view.view_type] || {}, view.options || {})
 -            });
 +        var default_view = this.flags.default_view || this.view_order[0].type,
 +            default_options = this.flags[default_view] && this.flags[default_view].options;
  
 -            views_ids[view.view_type] = view.view_id;
 -        });
 -        if (this.flags.views_switcher === false) {
 -            this.$el.find('.oe_view_manager_switch').hide();
 +        if (this.flags.headless) {
 +            this.$('.oe-view-manager-header').hide();
          }
 -        // If no default view defined, switch to the first one in sequence
 -        var default_view = this.flags.default_view || this.views_src[0].view_type;
 -
 -        return this.switch_mode(default_view, null, this.flags[default_view] && this.flags[default_view].options);
 +        this._super();
 +        var $sidebar = this.flags.sidebar ? this.$('.oe-view-manager-sidebar') : undefined,
 +            $pager = this.$('.oe-view-manager-pager');
  
 +        this.$breadcrumbs = this.$('.oe-view-title');
 +        this.$switch_buttons = this.$('.oe-view-manager-switch button');
 +        this.$header = this.$('.oe-view-manager-header');
 +        this.$header_col = this.$header.find('.oe-header-title');
 +        this.$search_col = this.$header.find('.oe-view-manager-search-view');
 +        this.$switch_buttons.click(function (event) {
 +            if (!$(event.target).hasClass('active')) {
 +                self.switch_mode($(this).data('view-type'));
 +            }
 +        });
 +        var views_ids = {};
 +        _.each(this.views, function (view) {
 +            views_ids[view.type] = view.view_id;
 +            view.options = _.extend({
 +                $buttons: self.$('.oe-' + view.type + '-buttons'),
 +                $sidebar : $sidebar,
 +                $pager : $pager,
 +                action : self.action,
 +                action_views_ids : views_ids,
 +            }, self.flags, self.flags[view.type], view.options);
 +            if (view.type !== 'form') {
 +                self.$('.oe-vm-switch-' + view.type).tooltip();
 +            }
 +        });
 +        this.$('.oe_debug_view').click(this.on_debug_changed);
 +        this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current'));
  
 +        this.search_view_loaded = this.setup_search_view();
 +        var main_view_loaded = this.switch_mode(default_view, null, default_options);
 +            
 +        return $.when(main_view_loaded, this.search_view_loaded);
      },
 +
      switch_mode: function(view_type, no_store, view_options) {
 -        var self = this;
 -        var view = this.views[view_type];
 -        var view_promise;
 -        var form = this.views['form'];
 -        if (!view || (form && form.controller && !form.controller.can_be_discarded())) {
 +        var self = this,
 +            view = this.views[view_type];
 +
 +        if (!view) {
              return $.Deferred().reject();
          }
 -        if (!no_store) {
 -            this.views_history.push(view_type);
 -        }
 -        this.active_view = view_type;
 +        if (view_type !== 'form') {
 +            this.view_stack = [];
 +        } 
  
 -        if (!view.controller) {
 -            view_promise = this.do_create_view(view_type);
 -        } else if (this.searchview
 -                && self.flags.auto_search
 -                && view.controller.searchable !== false) {
 -            this.searchview.ready.done(this.searchview.do_search);
 +        this.view_stack.push(view);
 +        this.active_view = view;
 +        if (!view.created) {
 +            view.created = this.create_view.bind(this)(view);
          }
 +        this.active_search = $.Deferred();
  
 -        if (this.searchview) {
 -            this.searchview[(view.controller.searchable === false || this.searchview.options.hidden) ? 'hide' : 'show']();
 +        if (this.searchview
 +                && this.flags.auto_search
 +                && view.controller.searchable !== false) {
 +            $.when(this.search_view_loaded, view.created).done(this.searchview.do_search);
 +        } else {
 +            this.active_search.resolve();
          }
  
 -        this.$el.find('.oe_view_manager_switch a').parent().removeClass('active');
 -        this.$el
 -            .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
 -            .parent().addClass('active');
 -        this.$el.attr("data-view-type", view_type);
 -        return $.when(view_promise).done(function () {
 -            _.each(_.keys(self.views), function(view_name) {
 -                var controller = self.views[view_name].controller;
 -                if (controller) {
 -                    var container = self.$el.find("> div > div > .oe_view_manager_body > .oe_view_manager_view_" + view_name);
 -                    if (view_name === view_type) {
 -                        container.show();
 -                        controller.do_show(view_options || {});
 -                    } else {
 -                        container.hide();
 -                        controller.do_hide();
 -                    }
 +        self.update_header();
 +        return $.when(view.created, this.active_search).done(function () {
 +            self.active_view = view;
 +            self._display_view(view.type, view_options);
 +            self.trigger('switch_mode', view_type, no_store, view_options);
 +            if (self.session.debug) {
 +                self.$('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
 +                    view: self.active_view.controller,
 +                    view_manager: self,
 +                }));
 +            }
 +        });
 +    },
 +    update_header: function () {
 +        this.$switch_buttons.removeClass('active');
 +        this.$('.oe-vm-switch-' + this.active_view.type).addClass('active');
 +    },
 +    _display_view: function (view_type, view_options) {
 +        var self = this;
 +        this.active_view.$container.show();
 +        $.when(this.active_view.controller.do_show(view_options)).done(function () { 
 +            _.each(self.views, function (view) {
 +                if (view.type !== view_type) {
 +                    view.controller && view.controller.do_hide();
 +                    view.$container && view.$container.hide();
 +                    view.options.$buttons && view.options.$buttons.hide();
                  }
              });
 -            self.trigger('switch_mode', view_type, no_store, view_options);
 +            self.active_view.options.$buttons && self.active_view.options.$buttons.show();
 +            if (self.searchview) {
 +                var is_hidden = self.active_view.controller.searchable === false;
 +                self.searchview.toggle_visibility(!is_hidden);
 +                self.$header_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden);
 +                self.$search_col.toggle(!is_hidden);
 +            }
 +            self.display_breadcrumbs();
          });
      },
 -    do_create_view: function(view_type) {
 -        // Lazy loading of views
 +    display_breadcrumbs: function () {
          var self = this;
 -        var view = this.views[view_type];
 -        var viewclass = this.registry.get_object(view_type);
 -        var options = _.clone(view.options);
 -        if (view_type === "form" && this.action && (this.action.target == 'new' || this.action.target == 'inline')) {
 +        if (!this.action_manager) return;
 +        var breadcrumbs = this.action_manager.get_breadcrumbs();
 +        var $breadcrumbs = _.map(_.initial(breadcrumbs), function (bc) {
 +            var $link = $('<a>').text(bc.title);
 +            $link.click(function () {
 +                self.action_manager.select_widget(bc.widget, bc.index);
 +            });
 +            return $('<li>').append($link);
 +        });
 +        $breadcrumbs.push($('<li>').addClass('active').text(_.last(breadcrumbs).title));
 +        this.$breadcrumbs
 +            .empty()
 +            .append($breadcrumbs);
 +    },
 +    create_view: function(view) {
 +        var self = this,
 +            View = this.registry.get_object(view.type),
 +            options = _.clone(view.options),
 +            view_loaded = $.Deferred();
 +
 +        if (view.type === "form" && this.action && (this.action.target === 'new' || this.action.target === 'inline')) {
              options.initial_mode = 'edit';
          }
 -        var controller = new viewclass(this, this.dataset, view.view_id, options);
 +        var controller = new View(this, this.dataset, view.view_id, options),
 +            $container = this.$(".oe-view-manager-view-" + view.type + ":first");
  
 -        controller.on('history_back', this, function() {
 -            var am = self.getParent();
 -            if (am && am.trigger) {
 -                return am.trigger('history_back');
 -            }
 -        });
 -
 -        controller.on("change:title", this, function() {
 -            if (self.active_view === view_type) {
 -                self.set_title(controller.get('title'));
 -            }
 -        });
 +        $container.hide();
 +        view.controller = controller;
 +        view.$container = $container;
  
          if (view.embedded_view) {
              controller.set_embedded_view(view.embedded_view);
          }
 -        controller.on('switch_mode', self, this.switch_mode);
 -        controller.on('previous_view', self, this.prev_view);
 -
 -        var container = this.$el.find("> div > div > .oe_view_manager_body > .oe_view_manager_view_" + view_type);
 -        var view_promise = controller.appendTo(container);
 -        this.views[view_type].controller = controller;
 -        return $.when(view_promise).done(function() {
 -            self.views[view_type].deferred.resolve(view_type);
 -            if (self.searchview
 -                    && self.flags.auto_search
 -                    && view.controller.searchable !== false) {
 -                self.searchview.ready.done(self.searchview.do_search);
 -            } else {
 -                self.view_completely_inited.resolve();
 -            }
 -            self.trigger("controller_inited",view_type,controller);
 +        controller.on('switch_mode', this, this.switch_mode.bind(this));
 +        controller.on('history_back', this, function () {
 +            self.action_manager && self.action_manager.trigger('history_back');
 +        });
 +        controller.on("change:title", this, function() {
 +            self.display_breadcrumbs();
          });
 +        controller.on('view_loaded', this, function () {
 +            view_loaded.resolve();
 +        });
 +        this.$('.oe-view-manager-pager > span').hide();
 +        return $.when(controller.appendTo($container), view_loaded)
 +                .done(function () { 
 +                    self.trigger("controller_inited", view.type, controller);
 +                });
 +    },
 +    select_view: function (index) {
 +        var view_type = this.view_stack[index].type;
 +        this.view_stack.splice(index);
 +        return this.switch_mode(view_type);
      },
 -
      /**
       * @returns {Number|Boolean} the view id of the given type, false if not found
       */
      get_view_id: function(view_type) {
          return this.views[view_type] && this.views[view_type].view_id || false;
 -    },
 -    set_title: function(title) {
 -        this.$el.find('.oe_view_title_text:first').text(title);
 -    },
 -    add_breadcrumb: function(options) {
 -        options = options || {};
 -        var self = this;
 -        var views = [this.active_view || this.views_src[0].view_type];
 -        this.on('switch_mode', self, function(mode) {
 -            var last = views.slice(-1)[0];
 -            if (mode !== last) {
 -                if (mode !== 'form') {
 -                    views.length = 0;
 -                }
 -                views.push(mode);
 -            }
 -        });
 -        var item = _.extend({
 -            widget: this,
 -            action: this.action,
 -            show: function(index) {
 -                var view_to_select = views[index];
 -                var state = self.url_states[view_to_select];
 -                self.do_push_state(state || {});
 -                $.when(self.switch_mode(view_to_select)).done(function() {
 -                    self.$el.show();
 -                });
 -            },
 -            get_title: function() {
 -                var id;
 -                var currentIndex;
 -                _.each(self.getParent().breadcrumbs, function(bc, i) {
 -                    if (bc.widget === self) {
 -                        currentIndex = i;
 -                    }
 -                });
 -                var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0];
 -                var titles = _.map(views, function(v) {
 -                    var controller = self.views[v].controller;
 -                    if (v === 'form') {
 -                        id = controller.datarecord.id;
 -                    }
 -                    return controller.get('title');
 -                });
 -                if (next && next.action && next.action.res_id && self.dataset &&
 -                    self.active_view === 'form' && self.dataset.model === next.action.res_model && id === next.action.res_id) {
 -                    // If the current active view is a formview and the next item in the breadcrumbs
 -                    // is an action on same object (model / res_id), then we omit the current formview's title
 -                    titles.pop();
 -                }
 -                return titles;
 -            }
 -        }, options);
 -        this.getParent().push_breadcrumb(item);
 -    },
 -    /**
 -     * Returns to the view preceding the caller view in this manager's
 -     * navigation history (the navigation history is appended to via
 -     * switch_mode)
 -     *
 -     * @param {Object} [options]
 -     * @param {Boolean} [options.created=false] resource was created
 -     * @param {String} [options.default=null] view to switch to if no previous view
 -     * @returns {$.Deferred} switching end signal
 -     */
 -    prev_view: function (options) {
 -        options = options || {};
 -        var current_view = this.views_history.pop();
 -        var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
 -        if (options.created && current_view === 'form' && previous_view === 'list') {
 -            // APR special case: "If creation mode from list (and only from a list),
 -            // after saving, go to page view (don't come back in list)"
 -            return this.switch_mode('form');
 -        } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') {
 -            // APR special case: "If creation from dashboard, we have no previous view
 -            return this.switch_mode('form');
 -        }
 -        return this.switch_mode(previous_view, true);
 -    },
 +    },    
      /**
       * Sets up the current viewmanager's search view.
       *
       * @param {Number|false} view_id the view to use or false for a default one
       * @returns {jQuery.Deferred} search view startup deferred
       */
 -    setup_search_view: function(view_id, search_defaults) {
 -        var self = this;
 +    setup_search_view: function() {
          if (this.searchview) {
              this.searchview.destroy();
          }
  
 +        var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
 +
 +        var search_defaults = {};
 +
 +        var context = this.action ? this.action.context : [];
 +        _.each(context, function (value, key) {
 +            var match = /^search_default_(.*)$/.exec(key);
 +            if (match) {
 +                search_defaults[match[1]] = value;
 +            }
 +        });
 +
 +
          var options = {
              hidden: this.flags.search_view === false,
              disable_custom_filters: this.flags.search_disable_custom_filters,
 +            $buttons: this.$('.oe-search-options'),
          };
 -        this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, options);
 +        var SearchView = instance.web.SearchView;
 +        this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
  
 -        this.searchview.on('search_data', self, this.do_searchview_search);
 -        return this.searchview.appendTo(this.$(".oe_view_manager_view_search"),
 -                                      this.$(".oe_searchview_drawer_container"));
 +        this.searchview.on('search_data', this, this.search.bind(this));
 +        return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first"));
      },
 -    do_searchview_search: function(domains, contexts, groupbys) {
 +    search: function(domains, contexts, groupbys) {
          var self = this,
 -            controller = this.views[this.active_view].controller,
 +            controller = this.active_view.controller,
              action_context = this.action.context || {};
          instance.web.pyeval.eval_domains_and_contexts({
              domains: [this.action.domain || []].concat(domains || []),
                  groupby = [groupby];
              }
              $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
 -                self.view_completely_inited.resolve();
 +                self.active_search.resolve();
              });
          });
      },
 -    /**
 -     * Called when one of the view want to execute an action
 -     */
 -    on_action: function(action) {
 -    },
 -    on_create: function() {
 -    },
 -    on_remove: function() {
 -    },
 -    on_edit: function() {
 -    },
 -    /**
 -     * Called by children view after executing an action
 -     */
 -    on_action_executed: function () {
 -    },
 -});
 -
 -instance.web.ViewManagerAction = instance.web.ViewManager.extend({
 -    template:"ViewManagerAction",
 -    /**
 -     * @constructs instance.web.ViewManagerAction
 -     * @extends instance.web.ViewManager
 -     *
 -     * @param {instance.web.ActionManager} parent parent object/widget
 -     * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
 -     */
 -    init: function(parent, action) {
 -        // dataset initialization will take the session from ``this``, so if we
 -        // do not have it yet (and we don't, because we've not called our own
 -        // ``_super()``) rpc requests will blow up.
 -        var flags = action.flags || {};
 -        if (!('auto_search' in flags)) {
 -            flags.auto_search = action.auto_search !== false;
 -        }
 -        if (action.res_model == 'board.board' && action.view_mode === 'form') {
 -            // Special case for Dashboards
 -            _.extend(flags, {
 -                views_switcher : false,
 -                display_title : false,
 -                search_view : false,
 -                pager : false,
 -                sidebar : false,
 -                action_buttons : false
 -            });
 -        }
 -        this._super(parent, null, action.views, flags);
 -        this.session = parent.session;
 -        this.action = action;
 -        var context = action.context;
 -        if (action.target === 'current'){
 -            var active_context = {
 -                active_model: action.res_model,
 -            };
 -            context = new instance.web.CompoundContext(context, active_context).eval();
 -            delete context['active_id'];
 -            delete context['active_ids'];
 -            if (action.res_id){
 -                context['active_id'] = action.res_id;
 -                context['active_ids'] = [action.res_id];
 -            }
 -        }
 -        var dataset = new instance.web.DataSetSearch(this, action.res_model, context, action.domain);
 -        if (action.res_id) {
 -            dataset.ids.push(action.res_id);
 -            dataset.index = 0;
 +    do_push_state: function(state) {
 +        if (this.action_manager) {
 +            state.view_type = this.active_view.type;
 +            this.action_manager.do_push_state(state);
          }
 -        this.dataset = dataset;
 -    },
 -    /**
 -     * Initializes the ViewManagerAction: sets up the searchview (if the
 -     * searchview is enabled in the manager's action flags), calls into the
 -     * parent to initialize the primary view and (if the VMA has a searchview)
 -     * launches an initial search after both views are done rendering.
 -     */
 -    start: function() {
 +    },    
 +    do_load_state: function(state, warm) {
          var self = this,
 -            searchview_loaded,
 -            search_defaults = {};
 -        _.each(this.action.context, function (value, key) {
 -            var match = /^search_default_(.*)$/.exec(key);
 -            if (match) {
 -                search_defaults[match[1]] = value;
 -            }
 +            def = this.active_view.created;
 +        if (state.view_type && state.view_type !== this.active_view.type) {
 +            def = def.then(function() {
 +                return self.switch_mode(state.view_type, true);
 +            });
 +        } 
 +        def.done(function() {
 +            self.active_view.controller.do_load_state(state, warm);
          });
      },
      on_debug_changed: function (evt) {
          var self = this,
 -            $sel = $(evt.currentTarget),
 -            $option = $sel.find('option:selected'),
 -            val = $sel.val(),
 -            current_view = this.views[this.active_view].controller;
 +            params = $(evt.target).data(),
 +            val = params.action,
 +            current_view = this.active_view.controller;
          switch (val) {
              case 'fvg':
                  var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get") }).open();
                          var dialog = new instance.web.Dialog(this, {
                              title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
                              size: 'medium',
 +                            buttons: {
 +                                Ok: function() { this.parents('.modal').modal('hide');}
 +                            },
                          }, QWeb.render('ViewManagerDebugViewLog', {
                              perm : result[0],
                              format : instance.web.format_value
                      new instance.web.Dialog(self, {
                          title: _.str.sprintf(_t("Model %s fields"),
                                               self.dataset.model),
 +                        buttons: {
 +                            Ok: function() { this.parents('.modal').modal('hide');}
 +                        },
                          }, $root).open();
                  });
                  break;
              case 'edit_workflow':
                  return this.do_action({
                      res_model : 'workflow',
 +                    name: _t('Edit Workflow'),
                      domain : [['osv', '=', this.dataset.model]],
                      views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
                      type : 'ir.actions.act_window',
                      view_mode : 'list'
                  });
              case 'edit':
 -                this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
 +                this.do_edit_resource(params.model, params.id, evt.target.text);
                  break;
              case 'manage_filters':
                  this.do_action({
                      res_model: 'ir.filters',
 +                    name: _t('Manage Filters'),
                      views: [[false, 'list'], [false, 'form']],
                      type: 'ir.actions.act_window',
                      context: {
                      });
                  }
                  break;
 +            case 'leave_debug':
 +                window.location.search="?";
 +                break;
              default:
                  if (val) {
 -                    console.log("No debug handler for ", val);
 +                    console.warn("No debug handler for ", val);
                  }
          }
 -        evt.currentTarget.selectedIndex = 0;
      },
 -    do_edit_resource: function(model, id, action) {
 -        action = _.extend({
 +    do_edit_resource: function(model, id, name) {
 +        this.do_action({
              res_model : model,
              res_id : id,
 +            name: name,
              type : 'ir.actions.act_window',
              view_type : 'form',
              view_mode : 'form',
              target : 'new',
              flags : {
                  action_buttons : true,
 -                form : {
 -                    resize_textareas : true
 -                }
 -            }
 -        }, action || {});
 -        this.do_action(action);
 -    },
 -    switch_mode: function (view_type, no_store, options) {
 -        var self = this;
 -
 -        return this.alive($.when(this._super.apply(this, arguments))).done(function () {
 -            var controller = self.views[self.active_view].controller;
 -            self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
 -                view: controller,
 -                view_manager: self
 -            }));
 -            self.set_title();
 -        });
 -    },
 -    do_create_view: function(view_type) {
 -        var self = this;
 -        return this._super.apply(this, arguments).then(function() {
 -            var view = self.views[view_type].controller;
 -            view.set({ 'title': self.action.name });
 -        });
 -    },
 -    get_action_manager: function() {
 -        var cur = this;
 -        while ((cur = cur.getParent())) {
 -            if (cur instanceof instance.web.ActionManager) {
 -                return cur;
 +                headless: true,
              }
 -        }
 -        return undefined;
 -    },
 -    set_title: function(title) {
 -        this.$el.find('.oe_breadcrumb_title:first').html(this.get_action_manager().get_title());
 -    },
 -    do_push_state: function(state) {
 -        if (this.getParent() && this.getParent().do_push_state) {
 -            state["view_type"] = this.active_view;
 -            this.url_states[this.active_view] = state;
 -            this.getParent().do_push_state(state);
 -        }
 -    },
 -    do_load_state: function(state, warm) {
 -        var self = this,
 -            defs = [];
 -        if (state.view_type && state.view_type !== this.active_view) {
 -            defs.push(
 -                this.views[this.active_view].deferred.then(function() {
 -                    return self.switch_mode(state.view_type, true);
 -                })
 -            );
 -        }
 -
 -        $.when(this.views[this.active_view] ? this.views[this.active_view].deferred : $.when(), defs).done(function() {
 -            self.views[self.active_view].controller.do_load_state(state, warm);
          });
      },
  });
@@@ -995,7 -1182,7 +996,7 @@@ instance.web.Sidebar = instance.web.Wid
          var self = this;
          this._super(this);
          this.redraw();
 -        this.$el.on('click','.oe_dropdown_menu li a', function(event) {
 +        this.$el.on('click','.dropdown-menu li a', function(event) {
              var section = $(this).data('section');
              var index = $(this).data('index');
              var item = self.items[section][index];
      add_items: function(section_code, items) {
          var self = this;
          if (items) {
 -            this.items[section_code].push.apply(this.items[section_code],items);
 +            this.items[section_code].unshift.apply(this.items[section_code],items);
              this.redraw();
          }
      },
                  a.url = prefix  + '&id=' + a.id + '&t=' + (new Date().getTime());
              }
          });
 -        self.items['files'] = attachments;
 +        self.items.files = attachments;
          self.redraw();
          this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
          this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
              this.$el.find('form.oe_form_binary_form').submit();
              $e.parent().find('input[type=file]').prop('disabled', true);
              $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
 -            this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
 +            this.$('.oe_sidebar_add_attachment a').text(_t('Uploading...'));
              instance.web.blockUI();
          }
      },
@@@ -1324,13 -1511,14 +1325,13 @@@ instance.web.View = instance.web.Widget
      },
      do_show: function () {
          this.$el.show();
 +        instance.web.bus.trigger('view_shown', this);
      },
      do_hide: function () {
          this.$el.hide();
      },
      is_active: function () {
 -        var manager = this.getParent();
 -        return !manager || !manager.active_view
 -             || manager.views[manager.active_view].controller === this;
 +        return this.ViewManager.active_view.controller === this;
      }, /**
       * Wraps fn to only call it if the current view is the active one. If the
       * current view is not active, doesn't call fn.
              this.getParent().do_push_state(state);
          }
      },
 -    do_load_state: function(state, warm) {
 +    do_load_state: function (state, warm) {
 +
      },
      /**
       * Switches to a specific view type
          </div>
      </div>
  </t>
 -<t t-name="Tipsy.alert">
 -    <a class="oe_tooltip_close oe_e">[</a>
 -    <span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert ui-state-error"></span>
 -    <div class="oe_tooltip_message">
 -        <t t-esc="message"/>
 -    </div>
 -</t>
  <t t-name="Dialog">
      <div class="modal" tabindex="-1" data-backdrop="static" role="dialog" aria-hidden="true">
          <div class="modal-dialog modal-lg">
@@@ -42,6 -49,7 +42,6 @@@
  <t t-name="CrashManager.warning">
      <table cellspacing="0" cellpadding="0" border="0" class="oe_dialog_warning">
      <tr>
 -        <td class="oe_dialog_icon"><img t-att-src='_s + "/web/static/src/img/warning.png"'/></td>
          <td>
              <p>
                  <t t-js="d">
                          <td><label for="backup_pwd">Master Password:</label></td>
                          <td><input type="password" name="backup_pwd" class="required" /></td>
                      </tr>
 +                    <tr>
 +                        <td><label>Format:</label></td>
 +                        <td>
 +                            <input type="radio" name="format" checked="checked" value="zip" />
 +                            <label for="format" title="Archive containing a dump of your database and your whole filestore">Zip</label>
 +                            <input type="radio" name="format" value="binary" />
 +                            <label for="format" title="Binary dump of your database (PostgreSQL dump)">Binary</label>
 +                        </td>
 +                    </tr>
                  </table>
              </form>
              <form id="db_restore" name="restore_db_form" style="display: none; ">
      </div>
  </t>
  
 -<t t-name="ViewManager">
 -    <div class="oe_view_manager">
 -        <table class="oe_view_manager_header">
 -            <col width="20%"/>
 -            <col width="35%"/>
 -            <col width="15%"/>
 -            <col width="30%"/>
 -            <tr class="oe_header_row oe_header_row_top">
 -                <td colspan="2">
 -                        <h2 class="oe_view_title" t-if="widget.flags.display_title !== false">
 -                            <span class="oe_view_title_text oe_breadcrumb_title"/>
 -                        </h2>
 -                </td>
 -                <td colspan="2">
 -                        <div class="oe_view_manager_view_search"/>
 -                </td>
 -            </tr>
 -            <tr class="oe_header_row">
 -                <td>
 -                        <div class="oe_view_manager_buttons"/>
 -                </td>
 -                <td colspan="2">
 -                        <div class="oe_view_manager_sidebar"/>
 -                </td>
 -                <td>
 -                    <ul class="oe_view_manager_switch oe_button_group oe_right">
 -                        <t t-if="widget.views_src.length > 1" t-foreach="widget.views_src" t-as="view">
 -                          <li class="oe_e">
 -                            <a t-attf-class="oe_vm_switch_#{view.view_type}" t-att-data-view-type="view.view_type"
 -                               t-att-title="view.button_label"/>
 -                          </li>
 -                        </t>
 -                    </ul>
 -                    <div class="oe_view_manager_pager oe_right"/>
 -                </td>
 -            </tr>
 -        </table>
 +<div t-name="ActionManager" class="oe_application" />
  
 -    <div class="oe_view_manager_wrapper">
 -        <div>
 -            <div class="oe_view_manager_body">
 -                <div class="oe_searchview_drawer_container"/>
 -                    <t t-foreach="widget.views_src" t-as="view">
 -                        <div t-attf-class="oe_view_manager_view_#{view.view_type}"/>
 +<div t-name="ViewManager" class="oe-view-manager">
 +    <div class="oe-view-manager-header container-fluid">
 +        <div class="row">
 +            <div class="col-md-6 oe-header-title">
 +                <div t-if="widget.session.debug" class="oe_debug_view btn-group btn-group-sm"/>
 +                <ol class="oe-view-title breadcrumb" t-if="widget.flags.display_title !== false">
 +                </ol>
 +            </div>
 +            <div class="oe-view-manager-search-view col-md-6" />
 +        </div>
 +        <div class="row">
 +            <div class="col-md-6">
 +                <div class="oe-view-manager-buttons">
 +                    <t t-foreach="widget.views" t-as="view">
 +                        <div t-attf-class="oe-#{view}-buttons"/>
 +                    </t>                    
 +                </div>
 +                <div class="oe-view-manager-sidebar"></div>
 +            </div>
 +            <div class="col-md-6">
 +                <div class="oe-search-options btn-group"/>
 +                <div class="oe-right-toolbar">
 +                    <div class="oe-view-manager-pager"></div>
 +                    <t t-if="widget.multiple_views">
 +                        <div class="oe-view-manager-switch btn-group btn-group-sm">
 +                            <t t-foreach="widget.view_order" t-as="view">
 +                                <button type="button" t-attf-class="btn btn-default fa oe-vm-switch-#{view.type}" t-att-data-view-type="view.type" t-att-title="view.label" t-if="view.type!=='form'">
 +                                </button>
 +                            </t>
 +                        </div>
                      </t>
                  </div>
              </div>
          </div>
      </div>
 -</t>
 -
 -<t t-name="ViewManagerAction" t-extend="ViewManager">
 -    <t t-jquery="h2.oe_view_title" t-operation="before">
 -        <select t-if="widget.session.debug" class="oe_debug_view"/>
 -    </t>
 -</t>
 +    <div class="oe-view-manager-content">
 +        <t t-foreach="widget.views" t-as="view">
 +            <div t-attf-class="oe-view-manager-view-#{view}"/>
 +        </t>
 +    </div>
 +</div>
  <t t-name="ViewManagerDebug">
 -    <option value="">Debug View#<t t-esc="view.fields_view.view_id"/></option>
 -    <t t-if="view_manager.active_view === 'form'">
 -        <option value="get_metadata">View Metadata</option>
 -        <option value="toggle_layout_outline">Toggle Form Layout Outline</option>
 -        <option value="set_defaults">Set Defaults</option>
 -    </t>
 -    <option value="tests">JS Tests</option>
 -    <option value="fields">View Fields</option>
 -    <option value="fvg">Fields View Get</option>
 -    <option value="manage_filters">Manage Filters</option>
 -    <t t-if="view_manager.session.uid === 1">
 -        <option value="translate">Technical translation</option>
 -        <option value="manage_views">Manage Views</option>
 -        <option value="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.fields_view.type)"/>View</option>
 -        <option t-if="view_manager.searchview" value="edit" data-model="ir.ui.view" t-att-data-id="view_manager.searchview.view_id">Edit SearchView</option>
 -        <option t-if="view_manager.action" value="edit" t-att-data-model="view_manager.action.type" t-att-data-id="view_manager.action.id">Edit Action</option>
 -        <option value="edit_workflow">Edit Workflow</option>
 -        <option value="print_workflow">Print Workflow</option>
 -    </t>
 +    <button type="button" class="btn btn-default dropdown-toggle fa fa-bug oe-view-manager-debug" data-toggle="dropdown"> <span class="caret"/>
 +    </button>
 +    <ul class="dropdown-menu" role="menu">
 +        <t t-if="view_manager.active_view.type === 'form'">
 +            <li><a data-action="get_metadata">View Metadata</a></li>
 +            <li><a data-action="toggle_layout_outline">Toggle Form Layout Outline</a></li>
 +            <li><a data-action="set_defaults">Set Defaults</a></li>
 +        </t>
 +        <li><a data-action="tests">JS Tests</a></li>
 +        <li><a data-action="fields">View Fields</a></li>
 +        <li><a data-action="fvg">Fields View Get</a></li>
 +        <li><a data-action="manage_filters">Manage Filters</a></li>
 +        <t t-if="view_manager.session.uid === 1">
 +            <li><a data-action="translate">Technical Translation</a></li>
 +            <li><a data-action="manage_views">Manage Views</a></li>
 +            <li><a data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.fields_view.type)"/> View</a></li>
 +            <li t-if="view_manager.searchview"><a data-action="edit" data-model="ir.ui.view" t-att-data-id="view_manager.searchview.view_id">Edit SearchView</a></li> 
 +            <li t-if="view_manager.action"><a data-action="edit" t-att-data-model="view_manager.action.type" t-att-data-id="view_manager.action.id">Edit Action</a></li> 
 +            <li><a data-action="edit_workflow">Edit Workflow</a></li>
 +            <li><a data-action="print_workflow">Print Workflow</a></li>
 +            <li class="divider"/>
 +            <li><a data-action="leave_debug">Leave Debug Mode</a></li>
 +        </t>
 +    </ul>
  </t>
  <t t-name="ViewManagerDebugViewLog">
      <div class="oe_debug_view_log">
          </table>
      </div>
  </t>
 -<t t-name="ViewPager">
 +<span t-name="ViewPager">
      <div class="oe_pager_value">
          <t t-raw="0"/>
      </div>
 -    <ul class="oe_pager_group">
 -        <!--
 -        <button class="oe_button oe_button_pager" type="button" data-pager-action="first">
 -            <img t-att-src='_s + "/web/static/src/img/pager_first.png"'/>
 -        </button>
 -        -->
 -        <li>
 -            <a class="oe_i" type="button" data-pager-action="previous">(</a>
 -        </li>
 -        <li>
 -            <a class="oe_i" type="button" data-pager-action="next">)</a>
 -        </li>
 -        <!--
 -        <button class="oe_button oe_button_pager" type="button" data-pager-action="last">
 -            <img t-att-src='_s + "/web/static/src/img/pager_last.png"'/>
 -        </button>
 -        -->
 -    </ul>
 -</t>
 +    <div class="btn-group btn-group-sm oe-pager-buttons">
 +        <a class="fa fa-chevron-left btn btn-default oe-pager-button" type="button" data-pager-action="previous"></a>
 +        <a class="fa fa-chevron-right btn btn-default oe-pager-button" type="button" data-pager-action="next"></a>
 +    </div>
 +</span>
  
 -<t t-name="Sidebar">
 -    <div class="oe_sidebar">
 -        <t t-foreach="widget.sections" t-as="section">
 -            <div class="oe_form_dropdown_section">
 -                <button class="oe_dropdown_toggle oe_dropdown_arrow" t-if="section.name != 'buttons'">
 -                    <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/>
 -                    <t t-esc="section.label"/>
 -                    <i class="fa fa-caret-down"/>
 +<div t-name="Sidebar" class="oe_sidebar btn-group">
 +    <t t-foreach="widget.sections" t-as="section">
 +        <div class="oe_form_dropdown_section btn-group btn-group-sm">
 +            <button class="btn btn-default dropdown-toggle" 
 +                    t-if="section.name != 'buttons'"
 +                    data-toggle="dropdown">
 +                <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/>
 +                <t t-esc="section.label"/>
 +                <span class="caret"></span>
 +            </button>
 +            <t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
 +                <button t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"
 +                    target="_blank" class="oe_sidebar_button oe_highlight">
 +                    <t t-raw="item.label"/>
                  </button>
 -                <t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
 -                    <button t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"
 -                        target="_blank" class="oe_sidebar_button oe_highlight">
 -                        <t t-raw="item.label"/>
 -                    </button>
 -                </t>
 -                <ul class="oe_dropdown_menu">
 -                    <li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
 -                        <t t-if="section.name == 'files'">
 -                            <t t-set="item.title">
 -                                <b>Attachment : </b><br/>
 -                                <t t-raw="item.name"/>
 -                            </t>
 -                            <t t-if="item.create_uid and item.create_uid[0]" t-set="item.title">
 -                                <t t-raw="item.title"/><br/>
 -                                <b>Created by : </b><br/>
 -                                <t t-raw="item.create_uid[1] + ' ' + item.create_date"/>
 -                            </t>
 -                            <t t-if="item.create_uid and item.write_uid and item.create_uid[0] != item.write_uid[0]" t-set="item.title">
 -                                <t t-raw="item.title"/><br/>
 -                                <b>Modified by : </b><br/>
 -                                <t t-raw="item.write_uid[1] + ' ' + item.write_date"/>
 -                            </t>
 +            </t>
 +            <ul class="dropdown-menu" role="menu">
 +                <li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
 +                    <t t-if="section.name == 'files'">
 +                        <t t-set="item.title">
 +                            <b>Attachment : </b><br/>
 +                            <t t-raw="item.name"/>
                          </t>
 -                        <a class="oe_sidebar_action_a" t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank">
 -                            <t t-raw="item.label"/>
 -                        </a>
 -                         <a t-if="section.name == 'files' and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment">x</a>
 -                    </li>
 -                    <li t-if="section.name == 'files'" class="oe_sidebar_add_attachment">
 -                        <t t-call="HiddenInputFile">
 -                            <t t-set="fileupload_id" t-value="widget.fileupload_id"/>
 -                            <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
 -                            <input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/>
 -                            <input type="hidden" name="id" t-att-value="widget.model_id"/>
 -                            <input type="hidden" name="session_id" t-att-value="widget.session.session_id" t-if="widget.session.override_session"/>
 -                            <span>Add...</span>
 +                        <t t-if="item.create_uid and item.create_uid[0]" t-set="item.title">
 +                            <t t-raw="item.title"/><br/>
 +                            <b>Created by : </b><br/>
 +                            <t t-raw="item.create_uid[1] + ' ' + item.create_date"/>
                          </t>
 -                    </li>
 -                </ul>
 -            </div>
 -        </t>
 -    </div>
 -</t>
 +                        <t t-if="item.create_uid and item.write_uid and item.create_uid[0] != item.write_uid[0]" t-set="item.title">
 +                            <t t-raw="item.title"/><br/>
 +                            <b>Modified by : </b><br/>
 +                            <t t-raw="item.write_uid[1] + ' ' + item.write_date"/>
 +                        </t>
 +                    </t>
 +                    <a class="oe_file_attachment" t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url">
 +                        <t t-raw="item.label"/>
 +                    </a>
 +                     <a t-if="section.name == 'files' and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment">x</a>
 +                </li>
 +                <li t-if="section.name == 'files'" class="oe_sidebar_add_attachment">
 +                    <t t-call="HiddenInputFile">
 +                        <t t-set="fileupload_id" t-value="widget.fileupload_id"/>
 +                        <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
 +                        <input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/>
 +                        <input type="hidden" name="id" t-att-value="widget.model_id"/>
 +                        <input type="hidden" name="session_id" t-att-value="widget.session.session_id" t-if="widget.session.override_session"/>
 +                        <span>Add...</span>
 +                    </t>
 +                </li>
 +            </ul>
 +        </div>
 +    </t>
 +</div>
  
  <t t-name="TreeView">
      <select t-if="toolbar" style="width: 30%">
  <t t-name="ListView.buttons">
      <div class="oe_list_buttons">
      <t t-if="!widget.no_leaf and widget.options.action_buttons !== false and widget.options.addable and widget.is_action_enabled('create')">
 -        <button type="button" class="oe_button oe_list_add oe_highlight">
 +        <button type="button" class="oe_list_add btn btn-primary btn-sm">
              <t t-esc="widget.options.addable"/>
          </button>
      </t>
      </div>
  </t>
 -<t t-name="ListView.pager">
 -    <div class="oe_list_pager" t-att-colspan="widget.columns_count">
 -        <t t-if="!widget.no_leaf and widget.options.pager !== false" t-call="ViewPager">
 -            <span class="oe_list_pager_state">
 -            </span>
 -        </t>
 -    </div>
 +<t t-name="ListView.pager" t-if="!widget.no_leaf and widget.options.pager !== false" t-call="ViewPager">
 +    <span class="oe_list_pager_state"></span>
  </t>
  <t t-name="ListView.rows" t-foreach="records.length" t-as="index">
      <t t-call="ListView.row">
          <div class="oe_form_container"/>
      </div>
  </t>
 -<div t-name="FormView.buttons" class="oe_form_buttons">
 +<div t-name="FormView.buttons">
      <t t-if="widget.options.action_buttons !== false">
 -        <span class="oe_form_buttons_view">
 -            <!-- required for the bounce effect on button -->
 -            <div t-if="widget.is_action_enabled('edit')" style="display: inline-block;">
 -                <button type="button" class="oe_button oe_form_button_edit" accesskey="E">Edit</button>
 -            </div>
 +        <div class="btn-group btn-group-sm oe_form_buttons_view">
 +            <button t-if="widget.is_action_enabled('edit')" 
 +                    type="button" 
 +                    class="oe_form_button_edit btn btn-default" accesskey="E">
 +                Edit
 +            </button>
              <button t-if="widget.is_action_enabled('create')"
 -                type="button" class="oe_button oe_form_button_create" accesskey="C">Create</button>
 -        </span>
 +                    type="button" class="oe_form_button_create btn btn-default"
 +                    accesskey="C">
 +                Create
 +            </button>
 +        </div>
          <span class="oe_form_buttons_edit">
 -            <button type="button" class="oe_button oe_form_button_save oe_highlight" accesskey="S">Save</button>
 +            <button type="button" class="oe_form_button_save btn btn-primary btn-sm" accesskey="S">Save</button>
              <span class="oe_fade">or</span>
              <a href="#" class="oe_bold oe_form_button_cancel" accesskey="D">Discard</a>
          </span>
      </t>
  </div>
 -<t t-name="FormView.pager">
 -    <div class="oe_form_pager">
 -        <t t-if="widget.options.pager !== false" t-call="ViewPager">
 -            <span class="oe_form_pager_state"></span>
 -        </t>
 -    </div>
 +<t t-name="FormView.pager" t-if="widget.options.pager !== false" t-call="ViewPager">
 +    <span class="oe_form_pager_state"></span>
  </t>
  <form t-name="FormView.set_default">
      <t t-set="args" t-value="widget.dialog_options.args"/>
      </label>
  </t>
  
 +<div t-name="PivotView" class="oe_pivot"/>
 +
 +<t t-name="PivotView.buttons">
 +    <div class="btn-group btn-group-sm">
 +        <button class="btn btn-default fa fa-expand oe-pivot-flip"></button>
 +        <button class="btn btn-default fa fa-arrows-alt oe-pivot-expand-all"></button>
 +        <button class="btn btn-default fa fa-download oe-pivot-download"></button>
 +    </div>
 +    <div class="btn-group btn-group-sm"> 
 +        <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
 +            Measures <span class="caret"/>
 +        </button>
 +        <ul class="oe-measure-list dropdown-menu">
 +            <li t-foreach="measures" t-as="measure" t-att-data-field="measure">
 +                <a><t t-esc="measures[measure].string"/></a>
 +            </li>
 +        </ul>
 +    </div>
 +</t>
 +
 +<div t-name="PivotView.nodata" class="oe_view_nocontent" >
 +    <div class="oe_empty_custom_dashboard">
 +        <p><b>No data to display.</b></p>
 +        <p>
 +                No data available for this pivot table.  Try to add some records, or make sure
 +                that there is at least one measure and no active filter in the search bar.
 +        </p>
 +    </div>
 +</div>
 +
 +
  <t t-name="Widget">
      Unhandled widget
      <t t-js="dict">console.warn('Unhandled widget', dict.widget);</t>
  <t t-name="web.datepicker">
      <span>
          <t t-set="placeholder" t-value="widget.getParent().node and widget.getParent().node.attrs.placeholder"/>
 -        <input type="text" class="oe_datepicker_container" disabled="disabled" style="display: none;"/>
 -        <input type="text"
 -            t-att-name="widget.name"
 -            t-att-placeholder="placeholder"
 -            class="oe_datepicker_master"
 -        /><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
 -               t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
 -               title="Select date" width="16" height="16" border="0"/>
 +        <div class="oe_datepicker_main input-group">
 +            <input type="text"
 +                t-att-name="widget.name"
 +                t-att-placeholder="placeholder"
 +                class="oe_datepicker_master"
 +            /><span class="fa fa-calendar datepickerbutton"/>
 +        </div>
      </span>
  </t>
  <t t-name="FieldDate">
              <t t-foreach="values" t-as="id">
                  <t t-set="file" t-value="widget.data[id]"/>
                  <div class="oe_attachment">
-                     <span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
+                     <span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="#{file.name || file.filename}">
                          <span class="oe_fileuploader_in_process">...Upload in progress...</span>
                          <t t-raw="file.name || file.filename"/>
                      </span>
-                     <a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
+                     <a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
                          <t t-raw="file.name || file.filename"/>
                      </a>
                      <t t-if="(!file.upload or file.percent_loaded&gt;=100)">
-                         <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a>
+                         <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="#{file.id}">[</a>
                      </t>
                  </div>
              </t>
              <t t-foreach="widget.get('value')" t-as="id">
                  <t t-set="file" t-value="widget.data[id]"/>
                  <div>
-                     <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
+                     <a t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
                          <t t-raw="file.name || file.filename"/>
                      </a>
                  </div>
      </div>
  </t>
  <t t-name="WidgetButton">
 +    <span t-if="widget.pre_text"> <t t-esc="widget.pre_text"/> </span>
      <button type="button" t-att-class="widget.is_stat_button ? 'oe_stat_button btn btn-default' : 'oe_button oe_form_button ' + (widget.node.attrs.class ? widget.node.attrs.class : '')"
          t-att-style="widget.node.attrs.style"
          t-att-tabindex="widget.node.attrs.tabindex"
          <span t-if="widget.string and !widget.is_stat_button"><t t-esc="widget.string"/></span>
          <div t-if="widget.string and widget.is_stat_button"><t t-esc="widget.string"/></div>
      </button>
 +    <span t-if="widget.post_text"> <t t-esc="widget.post_text"/> </span>
  </t>
  <t t-name="WidgetButton.tooltip" t-extend="WidgetLabel.tooltip">
      <t t-jquery="div.oe_tooltip_string" t-operation="replace">
  
  <t t-name="AbstractFormPopup.render">
      <div>
 -        <table style="width:100%">
 -            <tr style="width:100%">
 -                <td style="width:100%">
 -                    <div class="oe_popup_search" style="width:100%"></div>
 -                </td>
 -            </tr>
 -            <tr style="width:100%">
 -                <td style="width:100%">
 -                    <div class="oe_popup_list_pager"></div>
 -                </td>
 -            </tr>
 -            <tr style="width:100%">
 -                <td style="width:100%">
 -                    <div class="oe_popup_list" style="width:100%"></div>
 -                </td>
 -            </tr>
 -        </table>
 -        <div class="oe_popup_form" style="width:100%"></div>
 +        <div class="o-modal-header">
 +            <div class="o-popup-search"/>
 +            <div class="o-search-options oe-search-options"/>
 +        </div>
 +        <div class="oe_popup_list"/>
 +        <div class="oe_popup_form"/>
      </div>
  </t>
  <t t-name="SelectCreatePopup.search.buttons">
  
  <t t-name="One2Many.viewmanager" t-extend="ViewManager">
      <t t-jquery=".oe-view-manager-header">
 -        this.attr('t-if', 'views.length != 1');
 +        this.attr('t-if', 'widget.view_order.length != 1');
      </t>
  </t>
  <t t-name="One2Many.formview" t-extend="FormView">
      </t>
  </t>
  
 -<div t-name="SearchView" class="oe_searchview">
 +<div t-name="SearchView" class="oe_searchview form-control input-sm">
 +    <div class="oe_searchview_search fa fa-lg fa-search" title="Search Again"/>
      <div class="oe_searchview_facets"/>
 -    <div class="oe_searchview_clear"/>
 -    <div class="oe_searchview_unfold_drawer" title="Advanced Search..."/>
 -    <button type="button" class="oe_searchview_search"
 -            title="Search Again">Search</button>
 -</div>
 -
 -<div t-name="SearchViewDrawer" class="oe_searchview_drawer" style="display:none;">
 -    <div class="col-md-7"></div>
 -    <div class="col-md-5"></div>
 +    <div class="oe_searchview_unfold_drawer fa fa-lg fa-fw fa-chevron-right" title="Advanced Search..."/>
  </div>
  
  <div t-name="SearchView.InputView"
       tabindex="0"/>
  <!-- tabindex: makes div focusable -->
  <div t-name="SearchView.FacetView"
 -     class="oe_tag oe_tag_dark oe_searchview_facet"
 -     tabindex="0"
 -    ><span class="oe_facet_remove">x</span
 -    ><span class="oe_facet_category oe_i" t-if="widget.model.has('icon')">
 -        <t t-esc="widget.model.get('icon')"/>
 -    </span
 -    ><span class="oe_facet_category" t-if="!widget.model.has('icon')">
 +     class="oe_tag oe_searchview_facet"
 +     tabindex="0">
 +    <span class="oe_facet_remove">x</span>
 +    <span t-if="widget.model.has('icon')"
 +            t-att-class="'label label-default fa ' + widget.model.get('icon')">
 +    </span>
 +    <span class="label label-default" t-if="!widget.model.has('icon')">
          <t t-esc="widget.model.get('category')"/>
 -    </span ><span class="oe_facet_values"
 -/></div>
 +    </span >
 +    <span class="oe_facet_values"/>
 +</div>
  <span t-name="SearchView.FacetView.Value" class="oe_facet_value">
      <t t-esc="widget.model.get('label')"/>
  </span>
 -<t t-name="SearchView.managed-filters">
 -    <option class="oe_search_filters_title" value="">Filters</option>
 -    <optgroup label="-- Filters --">
 -        <t t-foreach="filters" t-as="filter">
 -            <option t-attf-value="get:#{filter_index}"
 -                    t-att-disabled="filter.disabled and 'disabled'"
 -                    t-att-title="filter.disabled and disabled_filter_message">
 -                <t t-esc="filter.name"/>
 -            </option>
 -        </t>
 -    </optgroup>
 -    <optgroup label="-- Actions --">
 -        <option value="advanced_filter">Add Advanced Filter</option>
 -        <option value="save_filter">Save Filter</option>
 -        <option value="manage_filters">Manage Filters</option>
 -    </optgroup>
 -</t>
 -<t t-name="SearchView.managed-filters.add">
 -    <div>
 -        <p>Filter Name:</p>
 -        <input type="text"/>
 -        <p>(Any existing filter with the same name will be replaced)</p>
 -    </div>
 -</t>
 -<t t-name="SearchView.render_lines">
 -    <table class="oe_search_render_line" border="0" cellspacing="0" cellpadding="0"
 -           t-foreach="lines" t-as="line">
 -        <tr>
 -            <td t-foreach="line" t-as="widget" class="oe_searchview_field">
 -                <t t-raw="widget.render(defaults)"/>
 -            </td>
 -        </tr>
 -    </table>
 -</t>
 -<button t-name="SearchView.filter" type="button"
 -        t-att-id="element_id"
 -        t-att-title="attrs.help"
 -        t-att-class="classes.join(' ')"
 -        t-att-style="style"
 -        t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined">
 -    <img t-att-src="_s + '/web/static/src/img/icons/' + (attrs.icon || 'gtk-home') + '.png'" width="16" height="16"/>
 -    <br t-if="attrs.string"/>
 -    <t t-esc="attrs.string"/>
 -</button>
 -<ul t-name="SearchView.filters">
 +<t t-name="SearchView.filters">
      <li t-foreach="widget.filters" t-as="filter" t-if="!filter.visible || filter.visible()"
              t-att-title="filter.attrs.string ? filter.attrs.help : undefined"
              t-att-data-index="filter_index">
 -        <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
 +        <a>
 +            <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
 +        </a>
      </li>
 -</ul>
 -<t t-name="SearchView.filters.facet">
 -    <div class="category oe_filter_category"><t t-esc="facet.get('category')"/></div>
 -
 -    <t t-set="val" t-value="facet.get('json')"/>
 -
 -    <div t-if="!(val instanceof Array)" class="search_facet_input_container">
 -        <t t-esc="facet.get('value')"/>
 -    </div>
 -    <t t-if="val instanceof Array">
 -        <div class="search_facet_input_container"
 -                t-foreach="facet.get('json')" t-as="filter">
 -            <t t-esc="filter.attrs.string || filter.attrs.name"/>
 -        </div>
 -    </t>
 -
 -    <div class="search_facet_remove VS-icon VS-icon-cancel"/>
  </t>
  <t t-name="SearchView.field">
      <label t-att-class="'oe_label' + (attrs.help ? '_help' : '')"
          <t t-if="filters.length" t-raw="filters.render(defaults)"/>
      </div>
  </t>
 -<t t-name="SearchView.util.expand">
 -    <div t-att-class="'searchview_group ' + (expand == '0' ? 'folded' : 'expanded')">
 -        <a t-if="label" class="searchview_group_string" href="#">
 -            <t t-esc="label"/>
 -        </a>
 -        <div class="searchview_group_content">
 -            <t t-raw="content"/>
 -        </div>
 -    </div>
 -</t>
 -<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
 -
 -</div>
 -
 -
 -<div t-name="SearchView.Custom" class="oe_searchview_custom oe_searchview_section">
 -    <dl class="dl-horizontal">
 -        <dt><span class="oe_i">M</span> Favorites</dt>
 -        <dd><ul class="oe_searchview_custom_list"/></dd>
 -    </dl>
 +<div t-name="SearchView.FilterMenu" class="btn-group btn-group-sm">
 +    <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
 +        <span class="fa fa-filter"/> Filters <span class="caret"/>
 +    </button>
 +    <ul class="dropdown-menu filters-menu" role="menu">
 +        <li class="oe-add-filter closed-menu"><a>Add Custom Filter</a></li>
 +        <li class="oe-add-filter-menu"><button class="btn btn-default oe-apply-filter">Apply</button><a class="oe-add-condition"><span class="fa fa-plus-circle"/> Add a condition</a></li>
 +    </ul>
  </div>
  
 - <div t-name="SearchView.SaveFilter" class="oe_searchview_savefilter">
 -    <h4>Save current filter</h4>
 -    <form class="oe_form">
 -        <p class="oe_form_required"><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
 -        <p>
 -            <input id="oe_searchview_custom_public" type="checkbox"/>
 -            <label for="oe_searchview_custom_public">Share with all users</label>
 -            <input id="oe_searchview_custom_default" type="checkbox"/>
 -            <label for="oe_searchview_custom_default">Use by default</label>
 -        </p>
 -        <button>Save</button>
 -    </form>
 +<div t-name="SearchView.GroupByMenu" class="btn-group btn-group-sm oe-groupby-menu">
 +    <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
 +        <span class="fa fa-bars"/> Group By <span class="caret"/>
 +    </button>
 +    <ul class="dropdown-menu group-by-menu" role="menu">
 +        <li class="divider"/>
 +        <li class="add-custom-group closed-menu"><a>Add custom group</a></li>
 +    </ul>
  </div>
  
 -<div t-name="SearchView.advanced" class="oe_searchview_advanced">
 -    <h4>Advanced Search</h4>
 -    <form>
 -        <ul>
 -
 -        </ul>
 -        <button class="oe_add_condition button" type="button">Add a condition</button><br/>
 -        <button class="oe_apply" type="submit">Apply</button>
 -    </form>
 +<t t-name="GroupByMenuSelector">
 +    <li><select class="form-control oe-add-group oe-group-selector">
 +        <t t-foreach="groupable_fields" t-as="field">
 +            <option t-att-data-name="field"><t t-esc="groupable_fields[field].string"/></option>
 +        </t>
 +    </select></li>
 +    <li><button class="btn btn-default oe-add-group oe-select-group">Apply</button></li>
 +</t>
 +<div t-name="SearchView.FavoriteMenu" class="btn-group btn-group-sm">
 +    <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
 +        <span class="fa fa-star"/> Favorites <span class="caret"/>
 +    </button>
 +    <ul class="dropdown-menu favorites-menu" role="menu">
 +        <li class="divider"/>
 +        <li class="oe-save-search closed-menu"><a>Save current search</a></li>
 +        <li class="oe-save-name"><input type="text" class="form-control"></input></li>
 +        <li class="oe-save-name">
 +            <span><input type="checkbox"/>Use by default</span>
 +        </li>
 +        <li class="oe-save-name">
 +            <span><input type="checkbox"/>Share with all users</span>
 +        </li>
 +        <li class="oe-save-name"><button class="btn btn-default">Save</button></li>
 +    </ul>
  </div>
  <t t-name="SearchView.extended_search.proposition">
 -    <li>
 -        <span class="searchview_extended_prop_or">or</span>
 -        <select class="searchview_extended_prop_field">
 -            <t t-foreach="widget.attrs.fields" t-as="field">
 -                <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
 -                        t-att-value="field.name">
 -                    <t t-esc="field.string"/>
 -                </option>
 -            </t>
 -        </select>
 -        <select class="searchview_extended_prop_op"/>
 +    <li class="oe-filter-condition">
 +        <span class="o-or-filter">or</span>
 +        <span>
 +            <select class="searchview_extended_prop_field form-control">
 +                <t t-foreach="widget.attrs.fields" t-as="field">
 +                    <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
 +                            t-att-value="field.name">
 +                        <t t-esc="field.string"/>
 +                    </option>
 +                </t>
 +            </select>
 +            <span class="searchview_extended_delete_prop fa fa-trash-o"/>
 +        </span>
 +        <select class="searchview_extended_prop_op form-control"/>
          <span class="searchview_extended_prop_value"/>
      </li>
  </t>
  <t t-name="SearchView.extended_search.proposition.char">
 -    <input class="field_char"/>
 +    <input class="field_char form-control"/>
  </t>
  <t t-name="SearchView.extended_search.proposition.empty">
      <span/>
  </t>
  <t t-name="SearchView.extended_search.proposition.integer">
 -    <input type="number" class="field_integer" value = "0" step="1"/>
 +    <input type="number" class="field_integer form-control" value = "0" step="1"/>
  </t>
  <t t-name="SearchView.extended_search.proposition.float">
 -    <input type="number" class="field_float" value = "0.0" step="0.01"/>
 +    <input type="number" class="field_float form-control" value = "0.0" step="0.01"/>
  </t>
  <t t-name="SearchView.extended_search.proposition.selection">
 -    <select>
 +    <select class="form-control">
          <t t-foreach="widget.field.selection" t-as="element">
          <option t-att-value="element[0]"><t t-esc="element[1]"/></option>
          </t>
              <button class="oe_button" id="add_field">Add</button>
              <button class="oe_button" id="remove_field">Remove</button>
              <button class="oe_button" id="remove_all_field">Remove All</button>
 +            <button class="oe_button" id="move_up">Move Up</button>
 +            <button class="oe_button" id="move_down">Move Down</button>
          </td>
          <td class="oe_export_fields_selector_right">
              <select name="fields_list" id="fields_list"
      <a href="javascript:void(0)"><t t-esc="text"/></a>
  </t>
  <t t-name="StatInfo">
 -    <strong><t t-esc="value"/></strong><br/><t t-esc="text"/></t>
 +    <strong><t t-esc="value"/></strong><br/><t t-esc="text"/>
 +</t>
 +<button t-name="toggle_button" type="button"
 +    t-att-title="widget.string"
 +    style="box-shadow: none; white-space:nowrap;">
 +    <img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png"
 +    t-att-alt="widget.string"/>
 +</button>
  
  </templates>
@@@ -43,9 -43,6 +43,9 @@@ instance.web_kanban.KanbanView = instan
          this.currently_dragging = {};
          this.limit = options.limit || 40;
          this.add_group_mutex = new $.Mutex();
 +        if (!this.options.$buttons || !this.options.$buttons.length) {
 +            this.options.$buttons = false;
 +        }
      },
      view_loading: function(r) {
          return this.load_kanban(r);
          if (unsorted && default_order) {
              this.dataset.set_sort(default_order.split(','));
          }
 -
          this.$el.addClass(this.fields_view.arch.attrs['class']);
          this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
          if (this.options.$buttons) {
              this.$buttons.appendTo(this.options.$buttons);
          } else {
 -            this.$el.find('.oe_kanban_buttons').replaceWith(this.$buttons);
 +            this.$('.oe_kanban_buttons').replaceWith(this.$buttons);
          }
          this.$buttons
              .on('click', 'button.oe_kanban_button_new', this.do_add_record)
              case 'button':
              case 'a':
                  var type = node.attrs.type || '';
 -                if (_.indexOf('action,object,edit,open,delete'.split(','), type) !== -1) {
 +                if (_.indexOf('action,object,edit,open,delete,url'.split(','), type) !== -1) {
                      _.each(node.attrs, function(v, k) {
                          if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
                              node.attrs['data-' + k] = v;
                              }
                          }];
                      }
 -                    if (node.tag == 'a') {
 +                    if (node.tag == 'a' && node.attrs['data-type'] != "url") {
                          node.attrs.href = '#';
                      } else {
                          node.attrs.type = 'button';
                  self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields));
              }
              var grouping = new instance.web.Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields);
 -            return self.alive($.when(grouping)).done(function(groups) {
 +            return self.alive($.when(grouping)).then(function(groups) {
                  self.remove_no_result();
                  if (groups) {
 -                    self.do_process_groups(groups);
 +                    return self.do_process_groups(groups);
                  } else {
 -                    self.do_process_dataset();
 +                    return self.do_process_dataset();
                  }
              });
          });
          var self = this;
          this.$el.find('table:first').show();
          this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
 -        this.add_group_mutex.exec(function() {
 +        return this.add_group_mutex.exec(function() {
              self.do_clear_groups();
              self.dataset.ids = [];
              if (!groups.length) {
                  self.no_result();
 -                return false;
 +                return $.when();
              }
              self.nb_records = 0;
-             var remaining = groups.length - 1,
-                 groups_array = [];
+             var groups_array = [];
              return $.when.apply(null, _.map(groups, function (group, index) {
                  var def = $.when([]);
                  var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
                          self.nb_records += records.length;
                          self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
                          groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
-                         if (self.dataset.index >= records.length){
-                             self.dataset.index = self.dataset.size() ? 0 : null;
-                         }
-                         if (!remaining--) {
-                             return self.do_add_groups(groups_array);
-                         }
                  });
              })).then(function () {
                  if(!self.nb_records) {
                      self.no_result();
                  }
-                 self.trigger('kanban_groups_processed');
+                 if (self.dataset.index >= self.nb_records){
+                     self.dataset.index = self.dataset.size() ? 0 : null;
+                 }
 -                return self.do_add_groups(groups_array);
++                return self.do_add_groups(groups_array).done(function() {
++                    self.trigger('kanban_groups_processed');
++                });
              });
          });
      },
          var self = this;
          this.$el.find('table:first').show();
          this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
 +        var def = $.Deferred();
          this.add_group_mutex.exec(function() {
 -            var def = $.Deferred();
              self.do_clear_groups();
              self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
                  var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
                      if (_.isEmpty(records)) {
                          self.no_result();
                      }
 +                    self.trigger('kanban_dataset_processed');
                      def.resolve();
                  });
              }).done(null, function() {
                  def.reject();
              });
 -            return def;
          });
 +        return def;
      },
      do_reload: function() {
          this.do_search(this.search_domain, this.search_context, this.search_group_by);
      },
  
      do_show: function() {
 -        if (this.$buttons) {
 +        if (this.options.$buttons) {
              this.$buttons.show();
          }
          this.do_push_state({});
@@@ -1029,10 -1022,7 +1027,10 @@@ instance.web_kanban.KanbanRecord = inst
       *  open on form/edit view : oe_kanban_global_click_edit
       */
      on_card_clicked: function(ev) {
 -        if(this.$el.find('.oe_kanban_global_click_edit').size()>0)
 +        if (this.$el.find('.oe_kanban_global_click').size() > 0 && this.$el.find('.oe_kanban_global_click').data('routing')) {
 +            instance.web.redirect(this.$el.find('.oe_kanban_global_click').data('routing') + "/" + this.id);
 +        }
 +        else if (this.$el.find('.oe_kanban_global_click_edit').size()>0)
              this.do_action_edit();
          else
              this.do_action_open();
          var button_attrs = $action.data();
          this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
      },
 +    do_action_url: function($action) {
 +        return instance.web.redirect($action.attr("href"));
 +     },
      do_reload: function() {
          var self = this;
          this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
@@@ -163,11 -163,11 +163,11 @@@ class Website(openerp.addons.web.contro
      def pagenew(self, path, noredirect=False, add_menu=None):
          xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context)
          if add_menu:
              request.registry['website.menu'].create(request.cr, request.uid, {
                      'name': path,
                      'url': "/page/" + xml_id,
 -                    'parent_id': id,
 +                    'parent_id': request.website.menu_id.id,
 +                    'website_id': request.website.id,
                  }, context=request.context)
          # Reverse action in order to allow shortcut for /page/<website_xml_id>
          url = "/page/" + re.sub(r"^website\.", '', xml_id)
              return werkzeug.wrappers.Response(url, mimetype='text/plain')
          return werkzeug.utils.redirect(url)
  
 -    @http.route('/website/theme_change', type='http', auth="user", website=True)
 -    def theme_change(self, theme_id=False, **kwargs):
 -        imd = request.registry['ir.model.data']
 -        Views = request.registry['ir.ui.view']
 -
 -        _, theme_template_id = imd.get_object_reference(
 -            request.cr, request.uid, 'website', 'theme')
 -        views = Views.search(request.cr, request.uid, [
 -            ('inherit_id', '=', theme_template_id),
 -        ], context=request.context)
 -        Views.write(request.cr, request.uid, views, {
 -            'active': False,
 -        }, context=dict(request.context or {}, active_test=True))
 -
 -        if theme_id:
 -            module, xml_id = theme_id.split('.')
 -            _, view_id = imd.get_object_reference(
 -                request.cr, request.uid, module, xml_id)
 -            Views.write(request.cr, request.uid, [view_id], {
 -                'active': True
 -            }, context=dict(request.context or {}, active_test=True))
 -
 -        return request.render('website.themes', {'theme_changed': True})
 -
      @http.route(['/website/snippets'], type='json', auth="public", website=True)
      def snippets(self):
          return request.website._render('website.snippets')
          return request.redirect(redirect)
  
      @http.route('/website/customize_template_get', type='json', auth='user', website=True)
 -    def customize_template_get(self, xml_id, full=False):
 +    def customize_template_get(self, xml_id, full=False, bundles=False):
          """ Lists the templates customizing ``xml_id``. By default, only
          returns optional templates (which can be toggled on and off), if
          ``full=True`` returns all templates customizing ``xml_id``
 +        ``bundles=True`` returns also the asset bundles
          """
          imd = request.registry['ir.model.data']
          view_model, view_theme_id = imd.get_object_reference(
          user_groups = set(user.groups_id)
  
          views = request.registry["ir.ui.view"]\
 -            ._views_get(request.cr, request.uid, xml_id, context=dict(request.context or {}, active_test=False))
 +            ._views_get(request.cr, request.uid, xml_id, bundles=bundles, context=dict(request.context or {}, active_test=False))
          done = set()
          result = []
          for v in views:
  
      @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
      def attach(self, func, upload=None, url=None, disable_optimization=None):
 -        Attachments = request.registry['ir.attachment']
 -
 -        website_url = message = None
 -        if not upload:
 -            website_url = url
 -            name = url.split("/").pop()
 +        # the upload argument doesn't allow us to access the files if more than
 +        # one file is uploaded, as upload references the first file
 +        # therefore we have to recover the files from the request object
 +        Attachments = request.registry['ir.attachment']  # registry for the attachment table
 +
 +        uploads = []
 +        message = None
 +        if not upload: # no image provided, storing the link and the image name
 +            uploads.append({'website_url': url})
 +            name = url.split("/").pop()                       # recover filename
              attachment_id = Attachments.create(request.cr, request.uid, {
                  'name':name,
                  'type': 'url',
                  'url': url,
                  'res_model': 'ir.ui.view',
              }, request.context)
 -        else:
 +        else:                                                  # images provided
              try:
 -                image_data = upload.read()
 -                image = Image.open(cStringIO.StringIO(image_data))
 -                w, h = image.size
 -                if w*h > 42e6: # Nokia Lumia 1020 photo resolution
 -                    raise ValueError(
 -                        u"Image size excessive, uploaded images must be smaller "
 -                        u"than 42 million pixel")
 -
 +                for c_file in request.httprequest.files.getlist('upload'):
 +                    image_data = c_file.read()
 +                    image = Image.open(cStringIO.StringIO(image_data))
 +                    w, h = image.size
 +                    if w*h > 42e6: # Nokia Lumia 1020 photo resolution
 +                        raise ValueError(
 +                            u"Image size excessive, uploaded images must be smaller "
 +                            u"than 42 million pixel")
 +    
                  if not disable_optimization and image.format in ('PNG', 'JPEG'):
                      image_data = image_save_for_web(image)
  
 -                attachment_id = Attachments.create(request.cr, request.uid, {
 -                    'name': upload.filename,
 -                    'datas': image_data.encode('base64'),
 -                    'datas_fname': upload.filename,
 -                    'res_model': 'ir.ui.view',
 -                }, request.context)
 -
 -                [attachment] = Attachments.read(
 -                    request.cr, request.uid, [attachment_id], ['website_url'],
 -                    context=request.context)
 -                website_url = attachment['website_url']
 +                    attachment_id = Attachments.create(request.cr, request.uid, {
 +                        'name': c_file.filename,
 +                        'datas': image_data.encode('base64'),
 +                        'datas_fname': c_file.filename,
 +                        'res_model': 'ir.ui.view',
 +                    }, request.context)
 +    
 +                    [attachment] = Attachments.read(
 +                        request.cr, request.uid, [attachment_id], ['website_url'],
 +                        context=request.context)
 +                    uploads.append(attachment)
              except Exception, e:
                  logger.exception("Failed to upload image to attachment")
                  message = unicode(e)
  
          return """<script type='text/javascript'>
              window.parent['%s'](%s, %s);
 -        </script>""" % (func, json.dumps(website_url), json.dumps(message))
 +        </script>""" % (func, json.dumps(uploads), json.dumps(message))
  
      @http.route(['/website/publish'], type='json', auth="public", website=True)
      def publish(self, id, object):
          obj = _object.browse(request.cr, request.uid, _id)
  
          values = {}
-         if 'website_published' in _object._all_columns:
+         if 'website_published' in _object._fields:
              values['website_published'] = not obj.website_published
          _object.write(request.cr, request.uid, [_id],
                        values, context=request.context)
          return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']])
  
      #------------------------------------------------------
 +    # Themes
 +    #------------------------------------------------------
 +
 +    def get_view_ids(self, xml_ids):
 +        ids = []
 +        imd = request.registry['ir.model.data']
 +        for xml_id in xml_ids:
 +            if "." in xml_id:
 +                xml = xml_id.split(".")
 +                view_model, id = imd.get_object_reference(request.cr, request.uid, xml[0], xml[1])
 +            else:
 +                id = int(xml_id)
 +            ids.append(id)
 +        return ids
 +
 +    @http.route(['/website/theme_customize_get'], type='json', auth="public", website=True)
 +    def theme_customize_get(self, xml_ids):
 +        view = request.registry["ir.ui.view"]
 +        enable = []
 +        disable = []
 +        ids = self.get_view_ids(xml_ids)
 +        context = dict(request.context or {}, active_test=True)
 +        for v in view.browse(request.cr, request.uid, ids, context=context):
 +            if v.active:
 +                enable.append(v.xml_id)
 +            else:
 +                disable.append(v.xml_id)
 +        return [enable, disable]
 +
 +    @http.route(['/website/theme_customize'], type='json', auth="public", website=True)
 +    def theme_customize(self, enable, disable):
 +        """ enable or Disable lists of ``xml_id`` of the inherit templates
 +        """
 +        cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
 +        view = pool["ir.ui.view"]
 +        context = dict(request.context or {}, active_test=True)
 +
 +        def set_active(ids, active):
 +            if ids:
 +                view.write(cr, uid, self.get_view_ids(ids), {'active': active}, context=context)
 +
 +        set_active(disable, False)
 +        set_active(enable, True)
 +
 +        return True
 +
 +    @http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True)
 +    def theme_customize_reload(self, href, enable, disable):
 +        self.theme_customize(enable and enable.split(",") or [],disable and disable.split(",") or [])
 +        return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true"))
 +
 +    #------------------------------------------------------
      # Helpers
      #------------------------------------------------------
      @http.route(['/website/kanban'], type='http', auth="public", methods=['POST'], website=True)
  
      @http.route([
          '/website/image',
 +        '/website/image/<xmlid>',
 +        '/website/image/<xmlid>/<field>',
          '/website/image/<model>/<id>/<field>',
          '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
          ], auth="public", website=True)
 -    def website_image(self, model, id, field, max_width=None, max_height=None):
 +    def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
          """ Fetches the requested field and ensures it does not go above
          (max_width, max_height), resizing it if necessary.
  
  
          The requested field is assumed to be base64-encoded image data in
          all cases.
 +
 +        xmlid can be used to load the image. But the field image must by base64-encoded
          """
 +        if xmlid and "." in xmlid:
 +            xmlid = xmlid.split(".", 1)
 +            try:
 +                model, id = request.registry['ir.model.data'].get_object_reference(request.cr, request.uid, xmlid[0], xmlid[1])
 +            except:
 +                raise werkzeug.exceptions.NotFound()
 +            if model == 'ir.attachment':
 +                field = "datas"
 +
 +        if not model or not id or not field:
 +            raise werkzeug.exceptions.NotFound()
 +
          try:
              idsha = id.split('_')
              id = idsha[0]
@@@ -45,55 -45,30 +45,55 @@@ class QWeb(orm.AbstractModel)
          'a': 'href',
      }
  
 -    def add_template(self, qcontext, name, node):
 -        # preprocessing for multilang static urls
 -        if request.website:
 -            for tag, attr in self.URL_ATTRS.iteritems():
 -                for e in node.iterdescendants(tag=tag):
 -                    url = e.get(attr)
 -                    if url:
 -                        e.set(attr, qcontext.get('url_for')(url))
 -        super(QWeb, self).add_template(qcontext, name, node)
 -
 -    def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
 -        URL_ATTRS = self.URL_ATTRS.get(element.tag)
 -        is_website = request.website
 -        for att, val in super(QWeb, self).render_att_att(element, attribute_name, attribute_value, qwebcontext):
 -            if is_website and att == URL_ATTRS and isinstance(val, basestring):
 -                val = qwebcontext.get('url_for')(val)
 -            yield (att, val)
 +    re_remove_spaces = re.compile('\s+')
 +    PRESERVE_WHITESPACE = [
 +        'pre',
 +        'textarea',
 +        'script',
 +        'style',
 +    ]
 +
 +    CDN_TRIGGERS = {
 +        'link':    'href',
 +        'script':  'src',
 +        'img':     'src',
 +    }
 +
 +    def render_attribute(self, element, name, value, qwebcontext):
 +        context = qwebcontext.context or {}
 +        if not context.get('rendering_bundle'):
 +            if name == self.URL_ATTRS.get(element.tag):
 +                value = qwebcontext.get('url_for')(value)
 +            if request and request.website and request.website.cdn_activated and name == self.CDN_TRIGGERS.get(element.tag):
 +                value = request.website.get_cdn_url(value)
  
 +        return super(QWeb, self).render_attribute(element, name, value, qwebcontext)
 +
 +    def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
 +        if request and request.website and request.website.cdn_activated:
 +            if qwebcontext.context is None:
 +                qwebcontext.context = {}
 +            qwebcontext.context['url_for'] = request.website.get_cdn_url
 +        return super(QWeb, self).render_tag_call_assets(element, template_attributes, generated_attributes, qwebcontext)
  
      def get_converter_for(self, field_type):
          return self.pool.get(
              'website.qweb.field.' + field_type,
              self.pool['website.qweb.field'])
  
 +    def render_text(self, text, element, qwebcontext):
 +        compress = request and not request.debug and request.website and request.website.compress_html
 +        if compress and element.tag not in self.PRESERVE_WHITESPACE:
 +            text = self.re_remove_spaces.sub(' ', text.lstrip())
 +        return super(QWeb, self).render_text(text, element, qwebcontext)
 +
 +    def render_tail(self, tail, element, qwebcontext):
 +        compress = request and not request.debug and request.website and request.website.compress_html
 +        if compress and element.getparent().tag not in self.PRESERVE_WHITESPACE:
 +            # No need to recurse because those tags children are not html5 parser friendly
 +            tail = self.re_remove_spaces.sub(' ', tail.rstrip())
 +        return super(QWeb, self).render_tail(tail, element, qwebcontext)
 +
  class Field(orm.AbstractModel):
      _name = 'website.qweb.field'
      _inherit = 'ir.qweb.field'
      def attributes(self, cr, uid, field_name, record, options,
                     source_element, g_att, t_att, qweb_context, context=None):
          if options is None: options = {}
-         column = record._model._all_columns[field_name].column
-         attrs = [('data-oe-translate', 1 if column.translate else 0)]
+         field = record._model._fields[field_name]
+         attrs = [('data-oe-translate', 1 if getattr(field, 'translate', False) else 0)]
  
          placeholder = options.get('placeholder') \
                     or source_element.get('placeholder') \
-                    or getattr(column, 'placeholder', None)
+                    or getattr(field, 'placeholder', None)
          if placeholder:
              attrs.append(('placeholder', placeholder))
  
      def value_from_string(self, value):
          return value
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          return self.value_from_string(element.text_content().strip())
  
      def qweb_object(self):
@@@ -136,7 -111,7 +136,7 @@@ class Float(orm.AbstractModel)
      _name = 'website.qweb.field.float'
      _inherit = ['website.qweb.field', 'ir.qweb.field.float']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          lang = self.user_lang(cr, uid, context=context)
  
          value = element.text_content().strip()
@@@ -167,7 -142,7 +167,7 @@@ class Date(orm.AbstractModel)
              qweb_context, context=None)
          return itertools.chain(attrs, [('data-oe-original', record[field_name])])
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          value = element.text_content().strip()
          if not value: return False
  
@@@ -198,7 -173,7 +198,7 @@@ class DateTime(orm.AbstractModel)
              ('data-oe-original', value)
          ])
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          if context is None: context = {}
          value = element.text_content().strip()
          if not value: return False
@@@ -229,16 -204,17 +229,17 @@@ class Text(orm.AbstractModel)
      _name = 'website.qweb.field.text'
      _inherit = ['website.qweb.field', 'ir.qweb.field.text']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          return html_to_text(element)
  
  class Selection(orm.AbstractModel):
      _name = 'website.qweb.field.selection'
      _inherit = ['website.qweb.field', 'ir.qweb.field.selection']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
+         record = self.browse(cr, uid, [], context=context)
          value = element.text_content().strip()
-         selection = column.reify(cr, uid, model, column, context=context)
+         selection = field.get_description(record.env)['selection']
          for k, v in selection:
              if isinstance(v, str):
                  v = ustr(v)
@@@ -252,11 -228,11 +253,11 @@@ class ManyToOne(orm.AbstractModel)
      _name = 'website.qweb.field.many2one'
      _inherit = ['website.qweb.field', 'ir.qweb.field.many2one']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          # FIXME: layering violations all the things
          Model = self.pool[element.get('data-oe-model')]
-         M2O = self.pool[column._obj]
-         field = element.get('data-oe-field')
+         M2O = self.pool[field.comodel_name]
+         field_name = element.get('data-oe-field')
          id = int(element.get('data-oe-id'))
          # FIXME: weird things are going to happen for char-type _rec_name
          value = html_to_text(element)
          # if anything blows up, just ignore it and bail
          try:
              # get parent record
-             [obj] = Model.read(cr, uid, [id], [field])
+             [obj] = Model.read(cr, uid, [id], [field_name])
              # get m2o record id
-             (m2o_id, _) = obj[field]
+             (m2o_id, _) = obj[field_name]
              # assume _rec_name and write directly to it
              M2O.write(cr, uid, [m2o_id], {
                  M2O._rec_name: value
              }, context=context)
          except:
              logger.exception("Could not save %r to m2o field %s of model %s",
-                              value, field, Model._name)
+                              value, field_name, Model._name)
  
          # not necessary, but might as well be explicit about it
          return None
@@@ -282,7 -258,7 +283,7 @@@ class HTML(orm.AbstractModel)
      _name = 'website.qweb.field.html'
      _inherit = ['website.qweb.field', 'ir.qweb.field.html']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          content = []
          if element.text: content.append(element.text)
          content.extend(html.tostring(child)
@@@ -311,7 -287,7 +312,7 @@@ class Image(orm.AbstractModel)
              cr, uid, field_name, record, options,
              source_element, t_att, g_att, qweb_context, context=context)
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
          if options is None: options = {}
          aclasses = ['img', 'img-responsive'] + options.get('class', '').split()
          classes = ' '.join(itertools.imap(escape, aclasses))
              max_size = '%sx%s' % (max_width, max_height)
  
          src = self.pool['website'].image_url(cr, uid, record, field_name, max_size)
-         img = '<img class="%s" src="%s"/>' % (classes, src)
+         img = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', ''))
          return ir_qweb.HTMLSafe(img)
  
      local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          url = element.find('img').get('src')
  
          url_object = urlparse.urlsplit(url)
@@@ -398,7 -375,7 +400,7 @@@ class Monetary(orm.AbstractModel)
      _name = 'website.qweb.field.monetary'
      _inherit = ['website.qweb.field', 'ir.qweb.field.monetary']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          lang = self.user_lang(cr, uid, context=context)
  
          value = element.find('span').text.strip()
@@@ -421,7 -398,7 +423,7 @@@ class Duration(orm.AbstractModel)
              qweb_context, context=None)
          return itertools.chain(attrs, [('data-oe-original', record[field_name])])
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          value = element.text_content().strip()
  
          # non-localized value
@@@ -442,7 -419,7 +444,7 @@@ class Contact(orm.AbstractModel)
      _name = 'website.qweb.field.contact'
      _inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one']
  
-     def from_html(self, cr, uid, model, column, element, context=None):
+     def from_html(self, cr, uid, model, field, element, context=None):
          return None
  
  class QwebView(orm.AbstractModel):
@@@ -3,7 -3,7 +3,7 @@@ import cop
  
  from lxml import etree, html
  
 -from openerp import SUPERUSER_ID
 +from openerp import SUPERUSER_ID, tools
  from openerp.addons.website.models import website
  from openerp.http import request
  from openerp.osv import osv, fields
@@@ -16,19 -16,13 +16,19 @@@ class view(osv.osv)
          'website_meta_description': fields.text("Website meta description", size=160, translate=True),
          'website_meta_keywords': fields.char("Website meta keywords", translate=True),
          'customize_show': fields.boolean("Show As Optional Inherit"),
 +        'website_id': fields.many2one('website',ondelete='cascade', string="Website"),
      }
 +
 +    _sql_constraints = [
 +        ('key_website_id_uniq', 'unique(key, website_id)',
 +            'Key must be unique per website.'),
 +    ]
 +
      _defaults = {
          'page': False,
          'customize_show': False,
      }
  
 -
      def _view_obj(self, cr, uid, view_id, context=None):
          if isinstance(view_id, basestring):
              return self.pool['ir.model.data'].xmlid_to_object(
@@@ -42,7 -36,7 +42,7 @@@
  
      # Returns all views (called and inherited) related to a view
      # Used by translation mechanism, SEO and optional templates
 -    def _views_get(self, cr, uid, view_id, options=True, context=None, root=True):
 +    def _views_get(self, cr, uid, view_id, options=True, bundles=False, context=None, root=True):
          """ For a given view ``view_id``, should return:
  
          * the view itself
          result = [view]
  
          node = etree.fromstring(view.arch)
 -        for child in node.xpath("//t[@t-call]"):
 +        xpath = "//t[@t-call]"
 +        if bundles:
 +            xpath += "| //t[@t-call-assets]"
 +        for child in node.xpath(xpath):
              try:
 -                called_view = self._view_obj(cr, uid, child.get('t-call'), context=context)
 +                called_view = self._view_obj(cr, uid, child.get('t-call', child.get('t-call-assets')), context=context)
              except ValueError:
                  continue
              if called_view not in result:
 -                result += self._views_get(cr, uid, called_view, options=options, context=context)
 +                result += self._views_get(cr, uid, called_view, options=options, bundles=bundles, context=context)
  
          extensions = view.inherit_children_ids
          if not options:
          Model = self.pool[el.get('data-oe-model')]
          field = el.get('data-oe-field')
  
-         column = Model._all_columns[field].column
-         converter = self.pool['website.qweb'].get_converter_for(
-             el.get('data-oe-type'))
-         value = converter.from_html(cr, uid, Model, column, el)
+         converter = self.pool['website.qweb'].get_converter_for(el.get('data-oe-type'))
+         value = converter.from_html(cr, uid, Model, Model._fields[field], el)
  
          if value is not None:
              # TODO: batch writes?
  
          return arch
  
 +    @tools.ormcache_context(accepted_keys=('website_id',))
 +    def get_view_id(self, cr, uid, xml_id, context=None):
 +        if context and 'website_id' in context and not isinstance(xml_id, (int, long)):
 +            domain = [('key', '=', xml_id), '|', ('website_id', '=', context['website_id']), ('website_id', '=', False)]
 +            [xml_id] = self.search(cr, uid, domain, order='website_id', limit=1, context=context)
 +        else:
 +            xml_id = super(view, self).get_view_id(cr, uid, xml_id, context=context)
 +        return xml_id
 +
      def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
          if request and getattr(request, 'website_enabled', False):
              engine='website.qweb'
@@@ -28,7 -28,6 +28,7 @@@ from openerp.osv import orm, osv, field
  from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web
  from openerp.tools.safe_eval import safe_eval
  from openerp.addons.web.http import request
 +from werkzeug.exceptions import NotFound
  
  logger = logging.getLogger(__name__)
  
@@@ -124,12 -123,6 +124,12 @@@ def slug(value)
  # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
  _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
  
 +DEFAULT_CDN_FILTERS = [
 +    "^/[^/]+/static/",
 +    "^/web/(css|js)/",
 +    "^/website/image/",
 +]
 +
  def unslug(s):
      """Extract slug and id from a string.
          Always return un 2-tuple (str|None, int|None)
@@@ -143,19 -136,20 +143,19 @@@ def urlplus(url, params)
      return werkzeug.Href(url)(params or None)
  
  class website(osv.osv):
 -    def _get_menu_website(self, cr, uid, ids, context=None):
 -        # IF a menu is changed, update all websites
 -        return self.search(cr, uid, [], context=context)
 -
      def _get_menu(self, cr, uid, ids, name, arg, context=None):
 -        root_domain = [('parent_id', '=', False)]
 -        menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
 -        menu = menus and menus[0] or False
 -        return dict( map(lambda x: (x, menu), ids) )
 +        res = {}
 +        menu_obj = self.pool.get('website.menu')
 +        for id in ids:
 +            menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
 +            res[id] = menu_ids and menu_ids[0] or False
 +        return res
  
      _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
      _description = "Website"
      _columns = {
 -        'name': fields.char('Domain'),
 +        'name': fields.char('Website Name'),
 +        'domain': fields.char('Website Domain'),
          'company_id': fields.many2one('res.company', string="Company"),
          'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
          'default_lang_id': fields.many2one('res.lang', string="Default language"),
          'social_googleplus': fields.char('Google+ Account'),
          'google_analytics_key': fields.char('Google Analytics Key'),
          'user_id': fields.many2one('res.users', string='Public User'),
 +        'compress_html': fields.boolean('Compress HTML'),
 +        'cdn_activated': fields.boolean('Activate CDN for assets'),
 +        'cdn_url': fields.char('CDN Base URL'),
 +        'cdn_filters': fields.text('CDN Filters', help="URL matching those filters will be rewritten using the CDN Base URL"),
          'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
 -        'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
 -            store= {
 -                'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
 -            })
 +        'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu')
      }
 -
      _defaults = {
 -        'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
 +        'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
 +        'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'),
 +        'compress_html': False,
 +        'cdn_activated': False,
 +        'cdn_url': '//localhost:8069/',
 +        'cdn_filters': '\n'.join(DEFAULT_CDN_FILTERS),
      }
  
      # cf. Wizard hack in website_views.xml
          except ValueError:
              # new page
              _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
 -            page_id = view.copy(cr, uid, template_id, context=context)
 +            website_id = context.get('website_id')
 +            key = template_module+'.'+page_name
 +            page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context)
              page = view.browse(cr, uid, page_id, context=context)
              page.write({
                  'arch': page.arch.replace(template, page_xmlid),
                  'name': page_name,
                  'page': ispage,
              })
 -            imd.create(cr, uid, {
 -                'name': page_name,
 -                'module': template_module,
 -                'model': 'ir.ui.view',
 -                'res_id': page_id,
 -                'noupdate': True
 -            }, context=context)
          return page_xmlid
  
      def page_for_name(self, cr, uid, ids, name, module='website', context=None):
          website = self.browse(cr, uid, id)
          return [(lg.code, lg.name) for lg in website.language_ids]
  
 +    def get_cdn_url(self, cr, uid, uri, context=None):
 +        # Currently only usable in a website_enable request context
 +        if request and request.website and not request.debug:
 +            cdn_url = request.website.cdn_url
 +            cdn_filters = (request.website.cdn_filters or '').splitlines()
 +            for flt in cdn_filters:
 +                if flt and re.match(flt, uri):
 +                    return urlparse.urljoin(cdn_url, uri)
 +        return uri
 +
      def get_languages(self, cr, uid, ids, context=None):
          return self._get_languages(cr, uid, ids[0], context=context)
  
                  lang['hreflang'] = lang['short']
          return langs
  
 +    @openerp.tools.ormcache(skiparg=4)
 +    def _get_current_website_id(self, cr, uid, domain_name, context=None):
 +        website_id = 1
 +        if request:
 +            ids = self.search(cr, uid, [('domain', '=', domain_name)], context=context)
 +            if ids:
 +                website_id = ids[0]
 +        return website_id
 +
      def get_current_website(self, cr, uid, context=None):
 -        # TODO: Select website, currently hard coded
 -        return self.pool['website'].browse(cr, uid, 1, context=context)
 +        domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
 +        website_id = self._get_current_website_id(cr, uid, domain_name, context=context)
 +        return self.browse(cr, uid, website_id, context=context)
  
      def is_publisher(self, cr, uid, ids, context=None):
          Access = self.pool['ir.model.access']
          return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
  
      def get_template(self, cr, uid, ids, template, context=None):
 -        if isinstance(template, (int, long)):
 -            view_id = template
 -        else:
 -            if '.' not in template:
 -                template = 'website.%s' % template
 -            module, xmlid = template.split('.', 1)
 -            model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
 -        return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
 +        if not isinstance(template, (int, long)) and '.' not in template:
 +            template = 'website.%s' % template
 +        View = self.pool['ir.ui.view']
 +        view_id = View.get_view_id(cr, uid, template, context=context)
 +        if not view_id:
 +            raise NotFound
 +        return View.browse(cr, uid, view_id, context=context)
  
      def _render(self, cr, uid, ids, template, values=None, context=None):
          # TODO: remove this. (just kept for backward api compatibility for saas-3)
  
          ids = Model.search(cr, uid,
                             [('id', '=', id)], context=context)
-         if not ids and 'website_published' in Model._all_columns:
+         if not ids and 'website_published' in Model._fields:
              ids = Model.search(cr, openerp.SUPERUSER_ID,
                                 [('id', '=', id), ('website_published', '=', True)], context=context)
          if not ids:
@@@ -1,4 -1,4 +1,4 @@@
 -@charset "utf-8";
 +@charset "UTF-8";
  /*       THIS CSS FILE IS FOR WEBSITE THEMING CUSTOMIZATION ONLY
   *
   * css for editor buttons, openerp widget included in the website and other
  
  /* Extra Styles */
  img.shadow {
 -  -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
 -  -ms-border-radius: 3px;
 -  -o-border-radius: 3px;
 +  -webkit-border-radius: 3px;
    border-radius: 3px;
 -  -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
    -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
 +  -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
    box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
    margin: 0 auto;
  }
@@@ -102,7 -104,7 +102,7 @@@ header a.navbar-brand img 
  }
  
  #wrapwrap p:empty:after {
 -  content: "\2060";
 +  content: "⁠";
  }
  
  /* ----- Snippets Styles ----- */
  #oe_main_menu_navbar {
    min-height: 34px;
    z-index: 1001;
 -  -webkit-border-radius: 0px;
    -moz-border-radius: 0px;
 -  -ms-border-radius: 0px;
 -  -o-border-radius: 0px;
 +  -webkit-border-radius: 0px;
    border-radius: 0px;
    margin-bottom: 0px;
  }
  
  /* ----- BOOTSTRAP HACK FOR STICKY FOOTER ----- */
  html, body, #wrapwrap {
 -  -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
 +  -webkit-box-sizing: border-box;
    box-sizing: border-box;
    height: 100%;
  }
@@@ -179,7 -183,7 +179,7 @@@ footer 
    background: rgba(200, 200, 200, 0.1);
  }
  
- #footer_container {
+ #footer_container, #footer {
    padding-top: 24px;
    padding-bottom: 12px;
  }
    padding-left: 16px;
  }
  
 -#themes-list .well {
 -  padding: 0 0 20px 0;
 -}
 -
  /* -- Hack for removing double scrollbar from mobile preview -- */
  div#mobile-preview.modal {
    overflow: hidden;
@@@ -409,114 -417,6 +409,114 @@@ ul.nav-stacked > li > a 
    margin-right: 32px;
  }
  
 +/* gallery */
 +.o_gallery.o_grid.o_spc-none div.row, .o_gallery.o_masonry.o_spc-none div.row {
 +  margin: 0;
 +}
 +.o_gallery.o_grid.o_spc-none div.row > *, .o_gallery.o_masonry.o_spc-none div.row > * {
 +  padding: 0;
 +}
 +.o_gallery.o_grid.o_spc-small div.row, .o_gallery.o_masonry.o_spc-small div.row {
 +  margin: 5px 0;
 +}
 +.o_gallery.o_grid.o_spc-small div.row > *, .o_gallery.o_masonry.o_spc-small div.row > * {
 +  padding: 0 5px;
 +}
 +.o_gallery.o_grid.o_spc-medium div.row, .o_gallery.o_masonry.o_spc-medium div.row {
 +  margin: 10px 0;
 +}
 +.o_gallery.o_grid.o_spc-medium div.row > *, .o_gallery.o_masonry.o_spc-medium div.row > * {
 +  padding: 0 10px;
 +}
 +.o_gallery.o_grid.o_spc-big div.row, .o_gallery.o_masonry.o_spc-big div.row {
 +  margin: 15px 0;
 +}
 +.o_gallery.o_grid.o_spc-big div.row > *, .o_gallery.o_masonry.o_spc-big div.row > * {
 +  padding: 0 15px;
 +}
 +.o_gallery.o_grid .img, .o_gallery.o_masonry .img {
 +  width: 100%;
 +}
 +.o_gallery.o_grid.size-auto .row {
 +  height: auto;
 +}
 +.o_gallery.o_grid.size-small .row {
 +  height: 100px;
 +}
 +.o_gallery.o_grid.size-medium .row {
 +  height: 250px;
 +}
 +.o_gallery.o_grid.size-big .row {
 +  height: 400px;
 +}
 +.o_gallery.o_grid.size-small .img, .o_gallery.o_grid.size-medium .img, .o_gallery.o_grid.size-big .img {
 +  height: 100%;
 +}
 +.o_gallery.o_nomode.o_spc-none .img {
 +  padding: 0;
 +}
 +.o_gallery.o_nomode.o_spc-small .img {
 +  padding: 5px;
 +}
 +.o_gallery.o_nomode.o_spc-medium .img {
 +  padding: 10px;
 +}
 +.o_gallery.o_nomode.o_spc-big .img {
 +  padding: 15px;
 +}
 +.o_gallery.o_slideshow .carousel ul.carousel-indicators li {
 +  border: 1px solid #aaa;
 +}
 +.o_gallery .carousel-inner .item img {
 +  max-width: none;
 +}
 +
 +.o_gallery.o_slideshow > .container {
 +  height: 100%;
 +}
 +
 +.o_gallery.o_slideshow .carousel, .modal-body.o_slideshow .carousel {
 +  height: 100%;
 +}
 +.o_gallery.o_slideshow .carousel img, .modal-body.o_slideshow .carousel img {
 +  max-height: 100%;
 +  max-width: 100%;
 +  margin: auto;
 +  position: relative;
 +  top: 50%;
 +  -webkit-transform: translateY(-50%);
 +  -ms-transform: translateY(-50%);
 +  transform: translateY(-50%);
 +}
 +.o_gallery.o_slideshow .carousel ul.carousel-indicators, .modal-body.o_slideshow .carousel ul.carousel-indicators {
 +  display: block;
 +  height: auto;
 +  padding: 0;
 +  border-width: 0;
 +  position: absolute;
 +  bottom: 0;
 +}
 +.o_gallery.o_slideshow .carousel ul.carousel-indicators li, .modal-body.o_slideshow .carousel ul.carousel-indicators li {
 +  list-style-image: none;
 +  display: inline-block;
 +  width: 35px;
 +  height: 35px;
 +  margin: 0 0px 5px 5px;
 +  padding: 0;
 +  border: 1px solid #aaa;
 +  text-indent: initial;
 +  background-size: cover;
 +  opacity: 0.5;
 +  background-color: #000;
 +}
 +.o_gallery.o_slideshow .carousel ul.carousel-indicators li.active, .modal-body.o_slideshow .carousel ul.carousel-indicators li.active {
 +  opacity: 1;
 +}
 +.o_gallery.o_slideshow .carousel .carousel-control.left, .o_gallery.o_slideshow .carousel .carousel-control.right, .modal-body.o_slideshow .carousel .carousel-control.left, .modal-body.o_slideshow .carousel .carousel-control.right {
 +  background-image: none;
 +  background-color: transparent;
 +}
 +
  /* Parallax Theme */
  div.carousel .carousel-indicators li {
    border: 1px solid grey;
@@@ -550,7 -450,7 +550,7 @@@ div.carousel div.carousel-content 
    padding: 32px 0;
  }
  
 -/* Background */
 +/* Background (kept for 8.0 compatibility) */
  .oe_dark {
    background: #eff8f8;
    background: rgba(200, 200, 200, 0.14);
  }
  
  .oe_green {
 -  background-color: #169c78;
 +  background-color: #169C78;
    color: white;
  }
  .oe_green .text-muted {
 -  color: #dddddd;
 +  color: #ddd;
  }
  
  .oe_blue_light {
    color: white;
  }
  .oe_blue_light .text-muted {
 -  color: #dddddd;
 +  color: #ddd;
  }
  
  .oe_blue {
    color: white;
  }
  .oe_orange .text-muted {
 -  color: #dddddd;
 +  color: #ddd;
  }
  
  .oe_purple {
    color: white;
  }
  .oe_purple .text-muted {
 -  color: #dddddd;
 +  color: #ddd;
  }
  
  .oe_red {
 -  background-color: #9c1b31;
 +  background-color: #9C1b31;
    color: white;
  }
  .oe_red .text-muted {
 -  color: #dddddd;
 +  color: #ddd;
  }
  
  /* Misc */
@@@ -656,8 -556,10 +656,8 @@@ span[data-oe-type="monetary"] 
  }
  
  .oe_template_fallback {
 -  -webkit-column-count: 3;
    -moz-column-count: 3;
 -  -ms-column-count: 3;
 -  -o-column-count: 3;
 +  -webkit-column-count: 3;
    column-count: 3;
  }
  
@@@ -137,7 -137,7 +137,7 @@@ foote
      background: rgb(239, 248, 248)
      background: rgba(200, 200, 200, 0.1)
  
- #footer_container
+ #footer_container, #footer
      padding-top: 24px
      padding-bottom: 12px
  
  .nav-hierarchy
      padding-left: 16px
  
 -#themes-list .well
 -    padding: 0 0 20px 0
 -
  /* -- Hack for removing double scrollbar from mobile preview -- */
  div#mobile-preview.modal
      overflow: hidden
@@@ -327,102 -330,6 +327,102 @@@ ul.nav-stacked > li > 
              margin-right: 32px
  
  
 +/* gallery */
 +
 +.o_gallery
 +    &.o_grid, &.o_masonry
 +        &.o_spc-none
 +            div.row
 +                margin: 0
 +            div.row > *
 +                padding: 0
 +        &.o_spc-small
 +            div.row
 +                margin: 5px 0
 +            div.row > *
 +                padding: 0 5px
 +        &.o_spc-medium
 +            div.row
 +                margin: 10px 0
 +            div.row > *
 +                padding: 0 10px
 +        &.o_spc-big
 +            div.row
 +                margin: 15px 0
 +            div.row > *
 +                padding: 0 15px
 +        .img
 +            width: 100%
 +    &.o_grid
 +        &.size-auto .row
 +            height: auto
 +        &.size-small .row
 +            height: 100px
 +        &.size-medium .row
 +            height: 250px
 +        &.size-big .row
 +            height: 400px
 +        &.size-small, &.size-medium, &.size-big
 +            .img
 +                height: 100%
 +    &.o_nomode
 +        &.o_spc-none
 +            .img
 +                padding: 0
 +        &.o_spc-small
 +            .img
 +                padding: 5px
 +        &.o_spc-medium
 +            .img
 +                padding: 10px
 +        &.o_spc-big
 +            .img
 +                padding: 15px
 +
 +    &.o_slideshow .carousel ul.carousel-indicators li
 +        border: 1px solid #aaa
 +    .carousel-inner .item img
 +        max-width: none
 +
 +.o_gallery.o_slideshow > .container
 +    height: 100%
 +
 +.o_gallery.o_slideshow .carousel, .modal-body.o_slideshow .carousel
 +    height: 100%
 +    img
 +        max-height: 100%
 +        max-width: 100%
 +        margin: auto
 +        position: relative
 +        top: 50%
 +        -webkit-transform: translateY(-50%)
 +        -ms-transform: translateY(-50%)
 +        transform: translateY(-50%)
 +    ul.carousel-indicators
 +        display: block
 +        height: auto
 +        padding: 0
 +        border-width: 0
 +        position: absolute
 +        bottom: 0
 +        li
 +            list-style-image: none
 +            display: inline-block
 +            width: 35px
 +            height: 35px
 +            margin: 0 0px 5px 5px
 +            padding: 0
 +            border: 1px solid #aaa
 +            text-indent: initial
 +            background-size: cover
 +            opacity: 0.5
 +            background-color: #000
 +        li.active
 +            opacity: 1
 +    .carousel-control.left, .carousel-control.right
 +        background-image: none
 +        background-color: transparent
 +
  /* Parallax Theme */
  
  div.carousel
              vertical-align: middle
              padding: 32px 0
  
 -/* Background */
 +/* Background (kept for 8.0 compatibility) */
  
  .oe_dark
      background: #eff8f8
@@@ -6,6 -6,11 +6,6 @@@
  
  <!-- Layout and generic templates -->
  
 -<template id="website.theme" name="Theme">
 -    <link id="bootstrap_css" rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css' t-ignore="true"/>
 -    <link rel="stylesheet" href='/website/static/src/css/website.css' t-ignore="true"/>
 -</template>
 -
  <template id="website.assets_frontend" name="Website assets">
      <t t-call="website.theme"/>
  
@@@ -15,7 -20,7 +15,7 @@@
  
      <script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
      <script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
 -
 +    
  </template>
  
  <template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend">
            t-att-data-oe-company-name="res_company.name">
          <head>
              <meta charset="utf-8" />
-             <t t-if="main_object and 'website_meta_title' in main_object">
+             <t t-if="main_object and 'website_meta_title' in main_object and not title">
                  <t t-set="title" t-value="main_object.website_meta_title"/>
              </t>
-             <t t-if="not title and main_object and 'name' in main_object">
+             <t t-if="main_object and 'name' in main_object and not title and not additional_title">
                  <t t-set="additional_title" t-value="main_object.name"/>
              </t>
              <t t-if="not title">
                  <t t-set="title"><t t-if="additional_title"><t t-raw="additional_title"/> | </t><t t-esc="(website or res_company).name"/></t>
              </t>
              <meta name="viewport" content="initial-scale=1"/>
              <meta name="description" t-att-content="main_object and 'website_meta_description' in main_object
                  and main_object.website_meta_description or website_meta_description"/>
                                      <span class="icon-bar"></span>
                                      <span class="icon-bar"></span>
                                  </button>
 -                                <a class="navbar-brand" href="/" t-field="res_company.name"/>
 +                                <a class="navbar-brand" href="/">YourCompany</a>
                              </div>
                              <div class="collapse navbar-collapse navbar-top-collapse">
                                  <ul class="nav navbar-nav navbar-right" id="top_menu">
                                      <li class="dropdown" t-ignore="true" t-if="website.user_id != user_id">
                                          <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                              <b>
 -                                                <span t-esc="user_id.name"/>
 +                                                <span t-esc="(len(user_id.name)&gt;25) and (user_id.name[:23]+'...') or user_id.name"/>
                                                  <span class="caret"></span>
                                              </b>
                                          </a>
                                          <ul class="dropdown-menu js_usermenu" role="menu">
 -                                            <li><a href="/web" role="menuitem">My Account</a></li>
 -                                            <li class="divider"/>
                                              <li><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li>
                                          </ul>
                                      </li>
      <script type="text/javascript" src="/website/static/src/js/website.tour.js"></script>
      <script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <!-- groups="base.group_website_designer" -->
      <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script>
 +    <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js" />
      <script type="text/javascript" src="/website/static/src/js/website.ace.js"></script>
      <script type="text/javascript" src="/website/static/src/js/website.translator.js"></script>
 +    <script type="text/javascript" src="/website/static/src/js/website.theme.js"></script>
  
  </template>
  
  <template id="footer_custom" inherit_id="website.layout" name="Footer">
      <xpath expr="//div[@id='footer_container']" position="replace">
          <div class="oe_structure" id="footer">
-             <section class="mt16 mb16">
 -            <section data-snippet-id='three-columns'>
++            <section>
                  <div class="container">
                      <div class="row">
                          <div class="col-md-4">
@@@ -827,7 -833,7 +828,7 @@@ Sitemap: <t t-esc="url_root"/>sitemap.x
          <div id="wrap">
              <div class="oe_structure">
  
 -                <section data-snippet-id="title">
 +                <section>
                      <div class="container">
                          <div class="row">
                              <div class="col-md-12">
                      </div>
                  </section>
  
 -                <section data-snippet-id="text-image">
 +                <section>
                      <div class="container">
                          <div class="row">
                              <div class="col-md-6 mt32">
@@@ -9,16 -9,6 +9,16 @@@
      </xpath>
  </template>
  
 +<template id="assets_frontend" inherit_id="website.assets_frontend" name="Blog Front-end assets">
 +    <xpath expr="." position="inside">
 +        <link rel='stylesheet' href='/website_blog/static/src/css/website_blog.css'/>
 +
 +        <script type="text/javascript" src="/website_blog/static/lib/contentshare.js"/>
 +        <script type="text/javascript" src="/website_blog/static/src/js/website_blog.inline.discussion.js"></script>
 +        <script type="text/javascript" src="/website_blog/static/src/js/website_blog.js"/>
 +    </xpath>
 +</template>
 +
  <!-- Layout add nav and footer -->
  <template id="header_footer_custom" inherit_id="website.footer_default" name="Footer News Blog Link">
      <xpath expr="//div[@id='info']/ul" position="inside">
@@@ -46,7 -36,7 +46,7 @@@
                      <t t-set="classname">pull-right</t>
                  </t>
              </section>
 -            <section data-snippet-id="title" class="container">
 +            <section class="container">
                  <div class="row">
                      <div class="col-md-12 text-center">
                          <h1>Latest Posts</h1>
              <h2 t-field="blog_post.subtitle"/>
              <p class="post-meta text-muted text-center" name="blog_post_data"/>
              <div>
-                 <img class="img-circle" t-att-src="website.image_url(blog_post.author_id, 'image_small')" style="width: 30px; margin-right: 10px;"/>
+                 <span t-field="blog_post.author_avatar" t-field-options='{"widget": "image", "class": "img-circle", "style":"width: 30px; margin-right: 10px; display:inline"}' />
                  <span t-field="blog_post.author_id" style="display: inline-block;" t-field-options='{
                      "widget": "contact",
                      "fields": ["name"]
                  <h1 t-field="next_post.name"/>
                  <h2 t-field="next_post.subtitle"/>
                  <div>
-                     <img class="img-circle" t-att-src="website.image_url(next_post.author_id, 'image_small')" style="width: 30px; margin-right: 10px;"/>
+                     <span t-field="next_post.author_avatar" t-field-options='{"widget": "image", "class": "img-circle", "style":"width: 30px; margin-right: 10px; display:inline"}' />
                      <span t-field="next_post.author_id" style="display: inline-block;" t-field-options='{
                              "widget": "contact",
                              "fields": ["name"]
  </template>
  
  <!-- Page -->
 -<template id="assets_frontend" inherit_id="website.assets_frontend" name="website_blog assets" >
 -    <xpath expr="/t" position="inside">
 -        <link rel='stylesheet' href='/website_blog/static/src/css/website_blog.css'/>
 -        <script type="text/javascript" src="/website_blog/static/src/js/website_blog.inline.discussion.js"></script>
 -        <script type="text/javascript" src="/website_blog/static/src/js/website_blog.js"/>
 -        <script type="text/javascript" src="/website_blog/static/lib/contentshare.js"/>
 -    </xpath>
 -</template>
 -
  <template id="index" name="Blog Navigation">
      <t t-call="website.layout">
          <div id="wrap" class="js_blog website_blog">
@@@ -62,7 -62,7 +62,7 @@@ class contactus(http.Controller)
          for field_name, field_value in kwargs.items():
              if hasattr(field_value, 'filename'):
                  post_file.append(field_value)
-             elif field_name in request.registry['crm.lead']._all_columns and field_name not in _BLACKLIST:
+             elif field_name in request.registry['crm.lead']._fields and field_name not in _BLACKLIST:
                  values[field_name] = field_value
              elif field_name not in _TECHNICAL:  # allow to add some free fields or blacklisted field like ID
                  post_description.append("%s: %s" % (field_name, field_value))
@@@ -79,7 -79,7 +79,7 @@@
  
          try:
              values['medium_id'] = request.registry['ir.model.data'].get_object_reference(request.cr, SUPERUSER_ID, 'crm', 'crm_tracking_medium_website')[1]
 -            values['section_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'website.salesteam_website_sales')
 +            values['team_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'website.salesteam_website_sales')
          except ValueError:
              pass
  
@@@ -3,10 -3,9 +3,10 @@@
  import werkzeug.urls
  import werkzeug.wrappers
  import simplejson
 +import lxml
 +from urllib2 import urlopen
  
  from openerp import tools
 -from openerp import SUPERUSER_ID
  from openerp.addons.web import http
  from openerp.addons.web.controllers.main import login_redirect
  from openerp.addons.web.http import request
@@@ -21,28 -20,31 +21,28 @@@ class WebsiteForum(http.Controller)
      _user_per_page = 30
  
      def _get_notifications(self):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Message = request.registry['mail.message']
 -        badge_st_id = request.registry['ir.model.data'].xmlid_to_res_id(cr, uid, 'gamification.mt_badge_granted')
 -        if badge_st_id:
 -            msg_ids = Message.search(cr, uid, [('subtype_id', '=', badge_st_id), ('to_read', '=', True)], context=context)
 -            msg = Message.browse(cr, uid, msg_ids, context=context)
 +        badge_subtype = request.env.ref('gamification.mt_badge_granted')
 +        if badge_subtype:
 +            msg = request.env['mail.message'].search([('subtype_id', '=', badge_subtype.id), ('to_read', '=', True)])
          else:
              msg = list()
          return msg
  
      def _prepare_forum_values(self, forum=None, **kwargs):
 -        user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context)
          values = {
 -            'user': user,
 -            'is_public_user': user.id == request.website.user_id.id,
 +            'user': request.env.user,
 +            'is_public_user': request.env.user.id == request.website.user_id.id,
              'notifications': self._get_notifications(),
              'header': kwargs.get('header', dict()),
              'searches': kwargs.get('searches', dict()),
 +            'no_introduction_message': request.httprequest.cookies.get('no_introduction_message', False),
              'validation_email_sent': request.session.get('validation_email_sent', False),
              'validation_email_done': request.session.get('validation_email_done', False),
          }
          if forum:
              values['forum'] = forum
          elif kwargs.get('forum_id'):
 -            values['forum'] = request.registry['forum.forum'].browse(request.cr, request.uid, kwargs.pop('forum_id'), context=request.context)
 +            values['forum'] = request.env['forum.forum'].browse(kwargs.pop('forum_id'))
          values.update(kwargs)
          return values
  
@@@ -51,7 -53,7 +51,7 @@@
  
      @http.route('/forum/send_validation_email', type='json', auth='user', website=True)
      def send_validation_email(self, forum_id=None, **kwargs):
 -        request.registry['res.users'].send_forum_validation_email(request.cr, request.uid, request.uid, forum_id=forum_id, context=request.context)
 +        request.env['res.users'].send_forum_validation_email(request.uid, forum_id=forum_id)
          request.session['validation_email_sent'] = True
          return True
  
@@@ -62,7 -64,7 +62,7 @@@
                  forum_id = int(forum_id)
              except ValueError:
                  forum_id = None
 -        done = request.registry['res.users'].process_forum_validation_token(request.cr, request.uid, token, int(id), email, forum_id=forum_id, context=request.context)
 +        done = request.env['res.users'].process_forum_validation_token(token, int(id), email, forum_id=forum_id)
          if done:
              request.session['validation_email_done'] = True
          if forum_id:
  
      @http.route(['/forum'], type='http', auth="public", website=True)
      def forum(self, **kwargs):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Forum = request.registry['forum.forum']
 -        obj_ids = Forum.search(cr, uid, [], context=context)
 -        forums = Forum.browse(cr, uid, obj_ids, context=context)
 +        forums = request.env['forum.forum'].search([])
          return request.website.render("website_forum.forum_all", {'forums': forums})
  
      @http.route('/forum/new', type='http', auth="user", methods=['POST'], website=True)
      def forum_create(self, forum_name="New Forum", **kwargs):
 -        forum_id = request.registry['forum.forum'].create(request.cr, request.uid, {
 -            'name': forum_name,
 -        }, context=request.context)
 -        return request.redirect("/forum/%s" % forum_id)
 +        forum_id = request.env['forum.forum'].create({'name': forum_name})
 +        return request.redirect("/forum/%s" % slug(forum_id))
  
      @http.route('/forum/notification_read', type='json', auth="user", methods=['POST'], website=True)
      def notification_read(self, **kwargs):
 -        request.registry['mail.message'].set_message_read(request.cr, request.uid, [int(kwargs.get('notification_id'))], read=True, context=request.context)
 +        request.env['mail.message'].browse([int(kwargs.get('notification_id'))]).set_message_read(read=True)
          return True
  
      @http.route(['/forum/<model("forum.forum"):forum>',
                   '''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag", "[('forum_id','=',forum[0])]"):tag>/questions''',
                   '''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag", "[('forum_id','=',forum[0])]"):tag>/questions/page/<int:page>''',
                   ], type='http', auth="public", website=True)
 -    def questions(self, forum, tag=None, page=1, filters='all', sorting='date', search='', **post):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Post = request.registry['forum.post']
 -        user = request.registry['res.users'].browse(cr, uid, uid, context=context)
 +    def questions(self, forum, tag=None, page=1, filters='all', sorting=None, search='', post_type=None, **post):
 +        Post = request.env['forum.post']
  
          domain = [('forum_id', '=', forum.id), ('parent_id', '=', False), ('state', '=', 'active')]
          if search:
          if filters == 'unanswered':
              domain += [('child_ids', '=', False)]
          elif filters == 'followed':
 -            domain += [('message_follower_ids', '=', user.partner_id.id)]
 -        else:
 -            filters = 'all'
 -
 -        if sorting == 'answered':
 -            order = 'child_count desc'
 -        elif sorting == 'vote':
 -            order = 'vote_count desc'
 -        elif sorting == 'date':
 -            order = 'write_date desc'
 -        else:
 -            sorting = 'creation'
 -            order = 'create_date desc'
 +            domain += [('message_follower_ids', '=', request.env.user.partner_id.id)]
 +        if post_type:
 +            domain += [('post_type', '=', post_type)]
 +
 +        if not sorting:
 +            sorting = forum.default_order
 +
 +        question_count = Post.search_count(domain)
  
 -        question_count = Post.search(cr, uid, domain, count=True, context=context)
          if tag:
              url = "/forum/%s/tag/%s/questions" % (slug(forum), slug(tag))
          else:
              url = "/forum/%s" % slug(forum)
  
 -        url_args = {}
 +        url_args = {
 +            'sorting': sorting
 +        }
          if search:
              url_args['search'] = search
          if filters:
              url_args['filters'] = filters
 -        if sorting:
 -            url_args['sorting'] = sorting
          pager = request.website.pager(url=url, total=question_count, page=page,
                                        step=self._post_per_page, scope=self._post_per_page,
                                        url_args=url_args)
  
 -        obj_ids = Post.search(cr, uid, domain, limit=self._post_per_page, offset=pager['offset'], order=order, context=context)
 -        question_ids = Post.browse(cr, uid, obj_ids, context=context)
 +        question_ids = Post.search(domain, limit=self._post_per_page, offset=pager['offset'], order=sorting)
  
          values = self._prepare_forum_values(forum=forum, searches=post)
          values.update({
              'filters': filters,
              'sorting': sorting,
              'search': search,
 +            'post_type': post_type,
          })
          return request.website.render("website_forum.forum_index", values)
  
  
      @http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True)
      def tag_read(self, **post):
 -        tags = request.registry['forum.tag'].search_read(request.cr, request.uid, [], ['name'], context=request.context)
 +        tags = request.env['forum.tag'].search_read([], ['name'])
          data = [tag['name'] for tag in tags]
          return simplejson.dumps(data)
  
      @http.route(['/forum/<model("forum.forum"):forum>/tag'], type='http', auth="public", website=True)
      def tags(self, forum, page=1, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Tag = request.registry['forum.tag']
 -        obj_ids = Tag.search(cr, uid, [('forum_id', '=', forum.id), ('posts_count', '>', 0)], limit=None, order='posts_count DESC', context=context)
 -        tags = Tag.browse(cr, uid, obj_ids, context=context)
 +        tags = request.env['forum.tag'].search([('forum_id', '=', forum.id), ('posts_count', '>', 0)], limit=None, order='posts_count DESC')
          values = self._prepare_forum_values(forum=forum, searches={'tags': True}, **post)
          values.update({
              'tags': tags,
      # Questions
      # --------------------------------------------------
  
 -    @http.route(['/forum/<model("forum.forum"):forum>/ask'], type='http', auth="public", website=True)
 -    def question_ask(self, forum, **post):
 -        if not request.session.uid:
 -            return login_redirect()
 -        values = self._prepare_forum_values(forum=forum, searches={},  header={'ask_hide': True})
 -        return request.website.render("website_forum.ask_question", values)
 -
 -    @http.route('/forum/<model("forum.forum"):forum>/question/new', type='http', auth="user", methods=['POST'], website=True)
 -    def question_create(self, forum, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Tag = request.registry['forum.tag']
 -        question_tag_ids = []
 -        if post.get('question_tags').strip('[]'):
 -            tags = post.get('question_tags').strip('[]').replace('"', '').split(",")
 -            for tag in tags:
 -                tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context)
 -                if tag_ids:
 -                    question_tag_ids.append((4, tag_ids[0]))
 -                else:
 -                    question_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id}))
 -
 -        new_question_id = request.registry['forum.post'].create(
 -            request.cr, request.uid, {
 -                'forum_id': forum.id,
 -                'name': post.get('question_name'),
 -                'content': post.get('content'),
 -                'tag_ids': question_tag_ids,
 -            }, context=context)
 -        return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), new_question_id))
 +    @http.route('/forum/get_url_title', type='json', auth="user", methods=['POST'], website=True)
 +    def get_url_title(self, **kwargs):
 +        arch = lxml.html.parse(urlopen(kwargs.get('url')))
 +        return arch.find(".//title").text
  
      @http.route(['''/forum/<model("forum.forum"):forum>/question/<model("forum.post", "[('forum_id','=',forum[0]),('parent_id','=',False)]"):question>'''], type='http', auth="public", website=True)
      def question(self, forum, question, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
          # increment view counter
 -        request.registry['forum.post'].set_viewed(cr, SUPERUSER_ID, [question.id], context=context)
 -
 +        question.sudo().set_viewed()
          if question.parent_id:
              redirect_url = "/forum/%s/question/%s" % (slug(forum), slug(question.parent_id))
              return werkzeug.utils.redirect(redirect_url, 301)
 -
          filters = 'question'
          values = self._prepare_forum_values(forum=forum, searches=post)
          values.update({
              favourite_ids = [(4, request.uid)]
          else:
              favourite_ids = [(3, request.uid)]
 -        request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'favourite_ids': favourite_ids}, context=request.context)
 +        question.sudo().write({'favourite_ids': favourite_ids})
          return favourite
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/ask_for_close', type='http', auth="user", methods=['POST'], website=True)
      def question_ask_for_close(self, forum, question, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Reason = request.registry['forum.post.reason']
 -        reason_ids = Reason.search(cr, uid, [], context=context)
 -        reasons = Reason.browse(cr, uid, reason_ids, context)
 +        reasons = request.env['forum.post.reason'].search([])
  
          values = self._prepare_forum_values(**post)
          values.update({
              'forum': forum,
              'reasons': reasons,
          })
 -        return request.website.render("website_forum.close_question", values)
 +        return request.website.render("website_forum.close_post", values)
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/edit_answer', type='http', auth="user", website=True)
      def question_edit_answer(self, forum, question, **kwargs):
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/close', type='http', auth="user", methods=['POST'], website=True)
      def question_close(self, forum, question, **post):
 -        request.registry['forum.post'].close(request.cr, request.uid, [question.id], reason_id=int(post.get('reason_id', False)), context=request.context)
 +        question.close(reason_id=int(post.get('reason_id', False)))
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/reopen', type='http', auth="user", methods=['POST'], website=True)
      def question_reopen(self, forum, question, **kwarg):
-         question.state = 'active'
 -        request.registry['forum.post'].reopen(request.cr, request.uid, [question.id], context=request.context)
++        question.reopen()
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/delete', type='http', auth="user", methods=['POST'], website=True)
      def question_delete(self, forum, question, **kwarg):
 -        request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': False}, context=request.context)
 +        question.active = False
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/undelete', type='http', auth="user", methods=['POST'], website=True)
      def question_undelete(self, forum, question, **kwarg):
 -        request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': True}, context=request.context)
 +        question.active = True
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      # Post
      # --------------------------------------------------
  
 -    @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/new', type='http', auth="public", methods=['POST'], website=True)
 -    def post_new(self, forum, post, **kwargs):
 +    @http.route(['/forum/<model("forum.forum"):forum>/ask'], type='http', auth="public", website=True)
 +    def forum_post(self, forum, post_type=None, **post):
          if not request.session.uid:
              return login_redirect()
 -        cr, uid, context = request.cr, request.uid, request.context
 -        user = request.registry['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
 +        user = request.env.user
 +        if not post_type in ['question', 'link', 'discussion']:  # fixme: make dynamic
 +            return werkzeug.utils.redirect('/forum/%s' % slug(forum))
          if not user.email or not tools.single_email_re.match(user.email):
 -            return werkzeug.utils.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), uid))
 -        request.registry['forum.post'].create(
 -            request.cr, request.uid, {
 -                'forum_id': forum.id,
 -                'parent_id': post.id,
 -                'content': kwargs.get('content'),
 -            }, context=request.context)
 -        return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(post)))
 +            return werkzeug.utils.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), self._uid))
 +        values = self._prepare_forum_values(forum=forum, searches={},  header={'ask_hide': True})
 +        return request.website.render("website_forum.new_%s" % post_type, values)
 +
 +    @http.route(['/forum/<model("forum.forum"):forum>/new',
 +                 '/forum/<model("forum.forum"):forum>/<model("forum.post"):post_parent>/reply'],
 +                type='http', auth="public", methods=['POST'], website=True)
 +    def post_create(self, forum, post_parent=None, post_type=None, **post):
 +        cr, uid, context = request.cr, request.uid, request.context
 +        if not request.session.uid:
 +            return login_redirect()
 +
 +        post_tag_ids = []
 +        Tag = request.env['forum.tag']
 +        if post.get('post_tags', False) and post.get('post_tags').strip('[]'):
 +            tags = post.get('post_tags').strip('[]').replace('"', '').split(",")
 +            for tag in tags:
 +                tag_rec = Tag.search([('name', '=', tag)])
 +                if tag_rec:
 +                    post_tag_ids.append((4, tag_rec.id))
 +                else:
 +                    post_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id}))
 +
 +        new_question = request.env['forum.post'].create({
 +            'forum_id': forum.id,
 +            'name': post.get('post_name', ''),
 +            'content': post.get('content', False),
 +            'content_link': post.get('content_link', False),
 +            'parent_id': post_parent and post_parent.id or False,
 +            'tag_ids': post_tag_ids,
 +            'post_type': post_parent and post_parent.post_type or post_type,  # tde check in selection field
 +        })
 +        return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), post_parent and slug(post_parent) or new_question.id))
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment', type='http', auth="public", methods=['POST'], website=True)
      def post_comment(self, forum, post, **kwargs):
          if not request.session.uid:
              return login_redirect()
          question = post.parent_id if post.parent_id else post
 -        cr, uid, context = request.cr, request.uid, request.context
          if kwargs.get('comment') and post.forum_id.id == forum.id:
              # TDE FIXME: check that post_id is the question or one of its answers
 -            request.registry['forum.post'].message_post(
 -                cr, uid, post.id,
 +            post.with_context(mail_create_nosubcribe=True).message_post(
                  body=kwargs.get('comment'),
                  type='comment',
 -                subtype='mt_comment',
 -                context=dict(context, mail_create_nosubcribe=True))
 +                subtype='mt_comment')
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
      def post_toggle_correct(self, forum, post, **kwargs):
 -        cr, uid, context = request.cr, request.uid, request.context
          if post.parent_id is False:
              return request.redirect('/')
          if not request.session.uid:
              return {'error': 'anonymous_user'}
  
          # set all answers to False, only one can be accepted
 -        request.registry['forum.post'].write(cr, uid, [c.id for c in post.parent_id.child_ids if not c.id == post.id], {'is_correct': False}, context=context)
 -        request.registry['forum.post'].write(cr, uid, [post.id], {'is_correct': not post.is_correct}, context=context)
 +        (post.parent_id.child_ids - post).write(dict(is_correct=False))
 +        post.is_correct = not post.is_correct
          return post.is_correct
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/delete', type='http', auth="user", methods=['POST'], website=True)
      def post_delete(self, forum, post, **kwargs):
          question = post.parent_id
 -        request.registry['forum.post'].unlink(request.cr, request.uid, [post.id], context=request.context)
 +        post.unlink()
          if question:
              werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
          return werkzeug.utils.redirect("/forum/%s" % slug(forum))
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/save', type='http', auth="user", methods=['POST'], website=True)
      def post_save(self, forum, post, **kwargs):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        question_tags = []
 -        if kwargs.get('question_tag') and kwargs.get('question_tag').strip('[]'):
 -            Tag = request.registry['forum.tag']
 -            tags = kwargs.get('question_tag').strip('[]').replace('"', '').split(",")
 +        post_tags = []
 +        if kwargs.get('post_tag') and kwargs.get('post_tag').strip('[]'):
 +            Tag = request.env['forum.tag']
 +            tags = kwargs.get('post_tag').strip('[]').replace('"', '').split(",")
              for tag in tags:
 -                tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context)
 -                if tag_ids:
 -                    question_tags += tag_ids
 +                tag_rec = Tag.search([('name', '=', tag)])
 +                if tag_rec:
 +                    post_tags += tag_rec.ids
                  else:
 -                    new_tag = Tag.create(cr, uid, {'name': tag, 'forum_id': forum.id}, context=context)
 -                    question_tags.append(new_tag)
 +                    new_tag = Tag.create({'name': tag, 'forum_id': forum.id})
 +                    post_tags.append(new_tag.id)
          vals = {
 -            'tag_ids': [(6, 0, question_tags)],
 -            'name': kwargs.get('question_name'),
 +            'tag_ids': [(6, 0, post_tags)],
 +            'name': kwargs.get('post_name'),
              'content': kwargs.get('content'),
          }
 -        request.registry['forum.post'].write(cr, uid, [post.id], vals, context=context)
 +        post.write(vals)
          question = post.parent_id if post.parent_id else post
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
          if request.uid == post.create_uid.id:
              return {'error': 'own_post'}
          upvote = True if not post.user_vote > 0 else False
 -        return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context)
 +        return post.vote(upvote=upvote)
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/downvote', type='json', auth="public", website=True)
      def post_downvote(self, forum, post, **kwargs):
          if request.uid == post.create_uid.id:
              return {'error': 'own_post'}
          upvote = True if post.user_vote < 0 else False
 -        return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context)
 +        return post.vote(upvote=upvote)
  
      # User
      # --------------------------------------------------
                   '/forum/<model("forum.forum"):forum>/users/page/<int:page>'],
                  type='http', auth="public", website=True)
      def users(self, forum, page=1, **searches):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        User = request.registry['res.users']
 -
 +        User = request.env['res.users']
          step = 30
 -        tag_count = User.search(cr, SUPERUSER_ID, [('karma', '>', 1), ('website_published', '=', True)], count=True, context=context)
 +        tag_count = len(User.search([('karma', '>', 1), ('website_published', '=', True)]))
          pager = request.website.pager(url="/forum/%s/users" % slug(forum), total=tag_count, page=page, step=step, scope=30)
 -
 -        obj_ids = User.search(cr, SUPERUSER_ID, [('karma', '>', 1), ('website_published', '=', True)], limit=step, offset=pager['offset'], order='karma DESC', context=context)
 +        user_obj = User.sudo().search([('karma', '>', 1), ('website_published', '=', True)], limit=step, offset=pager['offset'], order='karma DESC')
          # put the users in block of 3 to display them as a table
 -        users = [[] for i in range(len(obj_ids)/3+1)]
 -        for index, user in enumerate(User.browse(cr, SUPERUSER_ID, obj_ids, context=context)):
 -            users[index/3].append(user)
 +        users = [[] for i in range(len(user_obj) / 3 + 1)]
 +        for index, user in enumerate(user_obj):
 +            users[index / 3].append(user)
          searches['users'] = 'True'
  
          values = self._prepare_forum_values(forum=forum, searches=searches)
  
      @http.route(['/forum/<model("forum.forum"):forum>/partner/<int:partner_id>'], type='http', auth="public", website=True)
      def open_partner(self, forum, partner_id=0, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
          if partner_id:
 -            partner = request.registry['res.partner'].browse(cr, SUPERUSER_ID, partner_id, context=context)
 -            if partner.exists() and partner.user_ids:
 +            partner = request.env['res.partner'].sudo().search([('id', '=', partner_id)])
 +            if partner and partner.user_ids:
                  return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), partner.user_ids[0].id))
          return werkzeug.utils.redirect("/forum/%s" % slug(forum))
  
      @http.route(['/forum/user/<int:user_id>/avatar'], type='http', auth="public", website=True)
      def user_avatar(self, user_id=0, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
          response = werkzeug.wrappers.Response()
 -        User = request.registry['res.users']
 -        Website = request.registry['website']
 -        user = User.browse(cr, SUPERUSER_ID, user_id, context=context)
 +        User = request.env['res.users']
 +        Website = request.env['website']
 +        user = User.sudo().search([('id', '=', user_id)])
          if not user.exists() or (user_id != request.session.uid and user.karma < 1):
              return Website._image_placeholder(response)
 -        return Website._image(cr, SUPERUSER_ID, 'res.users', user.id, 'image', response)
 +        return Website._image('res.users', user.id, 'image', response)
  
      @http.route(['/forum/<model("forum.forum"):forum>/user/<int:user_id>'], type='http', auth="public", website=True)
      def open_user(self, forum, user_id=0, **post):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        User = request.registry['res.users']
 -        Post = request.registry['forum.post']
 -        Vote = request.registry['forum.post.vote']
 -        Activity = request.registry['mail.message']
 -        Followers = request.registry['mail.followers']
 -        Data = request.registry["ir.model.data"]
 -
 -        user = User.browse(cr, SUPERUSER_ID, user_id, context=context)
 -        if not user.exists() or user.karma < 1:
 +        User = request.env['res.users']
 +        Post = request.env['forum.post']
 +        Vote = request.env['forum.post.vote']
 +        Activity = request.env['mail.message']
 +        Followers = request.env['mail.followers']
 +        Data = request.env["ir.model.data"]
 +
 +        user = User.sudo().search([('id', '=', user_id)])
 +        if not user or user.karma < 1:
              return werkzeug.utils.redirect("/forum/%s" % slug(forum))
          values = self._prepare_forum_values(forum=forum, **post)
          if user_id != request.session.uid and not user.website_published:
              return request.website.render("website_forum.private_profile", values)
          # questions and answers by user
          user_questions, user_answers = [], []
 -        user_question_ids = Post.search(cr, uid, [
 -                ('parent_id', '=', False),
 -                ('forum_id', '=', forum.id), ('create_uid', '=', user.id),
 -            ], order='create_date desc', context=context)
 +        user_question_ids = Post.search([
 +            ('parent_id', '=', False),
 +            ('forum_id', '=', forum.id), ('create_uid', '=', user.id)],
 +            order='create_date desc')
          count_user_questions = len(user_question_ids)
          # displaying only the 20 most recent questions
 -        user_questions = Post.browse(cr, uid, user_question_ids[:20], context=context)
 +        user_questions = user_question_ids[:20]
  
 -        user_answer_ids = Post.search(cr, uid, [
 -                ('parent_id', '!=', False),
 -                ('forum_id', '=', forum.id), ('create_uid', '=', user.id),
 -            ], order='create_date desc', context=context)
 +        user_answer_ids = Post.search([
 +            ('parent_id', '!=', False),
 +            ('forum_id', '=', forum.id), ('create_uid', '=', user.id)],
 +            order='create_date desc')
          count_user_answers = len(user_answer_ids)
          # displaying only the 20  most recent answers
 -        user_answers = Post.browse(cr, uid, user_answer_ids[:20], context=context)
 +        user_answers = user_answer_ids[:20]
  
          # showing questions which user following
 -        obj_ids = Followers.search(cr, SUPERUSER_ID, [('res_model', '=', 'forum.post'), ('partner_id', '=', user.partner_id.id)], context=context)
 -        post_ids = [follower.res_id for follower in Followers.browse(cr, SUPERUSER_ID, obj_ids, context=context)]
 -        que_ids = Post.search(cr, uid, [('id', 'in', post_ids), ('forum_id', '=', forum.id), ('parent_id', '=', False)], context=context)
 -        followed = Post.browse(cr, uid, que_ids, context=context)
 +        post_ids = [follower.res_id for follower in Followers.sudo().search([('res_model', '=', 'forum.post'), ('partner_id', '=', user.partner_id.id)])]
 +        followed = Post.search([('id', 'in', post_ids), ('forum_id', '=', forum.id), ('parent_id', '=', False)])
  
 -        #showing Favourite questions of user.
 -        fav_que_ids = Post.search(cr, uid, [('favourite_ids', '=', user.id), ('forum_id', '=', forum.id), ('parent_id', '=', False)], context=context)
 -        favourite = Post.browse(cr, uid, fav_que_ids, context=context)
 +        # showing Favourite questions of user.
 +        favourite = Post.search([('favourite_ids', '=', user.id), ('forum_id', '=', forum.id), ('parent_id', '=', False)])
  
 -        #votes which given on users questions and answers.
 -        data = Vote.read_group(cr, uid, [('forum_id', '=', forum.id), ('recipient_id', '=', user.id)], ["vote"], groupby=["vote"], context=context)
 +        # votes which given on users questions and answers.
 +        data = Vote.read_group([('forum_id', '=', forum.id), ('recipient_id', '=', user.id)], ["vote"], groupby=["vote"])
          up_votes, down_votes = 0, 0
          for rec in data:
              if rec['vote'] == '1':
              elif rec['vote'] == '-1':
                  down_votes = rec['vote_count']
  
 -        #Votes which given by users on others questions and answers.
 -        post_votes = Vote.search(cr, uid, [('user_id', '=', user.id)], context=context)
 -        vote_ids = Vote.browse(cr, uid, post_votes, context=context)
 +        # Votes which given by users on others questions and answers.
 +        vote_ids = Vote.search([('user_id', '=', user.id)])
  
 -        #activity by user.
 -        model, comment = Data.get_object_reference(cr, uid, 'mail', 'mt_comment')
 -        activity_ids = Activity.search(cr, uid, [('res_id', 'in', user_question_ids+user_answer_ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)], order='date DESC', limit=100, context=context)
 -        activities = Activity.browse(cr, uid, activity_ids, context=context)
 +        # activity by user.
 +        model, comment = Data.get_object_reference('mail', 'mt_comment')
 +        activities = Activity.search([('res_id', 'in', (user_question_ids+user_answer_ids).ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)], order='date DESC', limit=100)
  
          posts = {}
          for act in activities:
              posts[act.res_id] = True
 -        posts_ids = Post.browse(cr, uid, posts.keys(), context=context)
 +        posts_ids = Post.search([('id', 'in', posts.keys())])
          posts = dict(map(lambda x: (x.id, (x.parent_id or x, x.parent_id and x or False)), posts_ids))
  
          # TDE CLEANME MASTER: couldn't it be rewritten using a 'menu' key instead of one key for each menu ?
 -        if user.id == uid:
 +        if user == request.env.user:
              post['my_profile'] = True
          else:
              post['users'] = True
  
          values.update({
 -            'uid': uid,
 +            'uid': request.env.user.id,
              'user': user,
              'main_object': user,
              'searches': post,
  
      @http.route('/forum/<model("forum.forum"):forum>/user/<model("res.users"):user>/edit', type='http', auth="user", website=True)
      def edit_profile(self, forum, user, **kwargs):
 -        country = request.registry['res.country']
 -        country_ids = country.search(request.cr, SUPERUSER_ID, [], context=request.context)
 -        countries = country.browse(request.cr, SUPERUSER_ID, country_ids, context=request.context)
 +        countries = request.env['res.country'].search([])
          values = self._prepare_forum_values(forum=forum, searches=kwargs)
          values.update({
              'email_required': kwargs.get('email_required'),
          }
          if request.uid == user.id:  # the controller allows to edit only its own privacy settings; use partner management for other cases
              values['website_published'] = kwargs.get('website_published') == 'True'
 -        request.registry['res.users'].write(request.cr, request.uid, [user.id], values, context=request.context)
 +        user.write(values)
          return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), user.id))
  
      # Badges
  
      @http.route('/forum/<model("forum.forum"):forum>/badge', type='http', auth="public", website=True)
      def badges(self, forum, **searches):
 -        cr, uid, context = request.cr, request.uid, request.context
 -        Badge = request.registry['gamification.badge']
 -        badge_ids = Badge.search(cr, SUPERUSER_ID, [('challenge_ids.category', '=', 'forum')], context=context)
 -        badges = Badge.browse(cr, uid, badge_ids, context=context)
 +        Badge = request.env['gamification.badge']
 +        badges = Badge.sudo().search([('challenge_ids.category', '=', 'forum')])
          badges = sorted(badges, key=lambda b: b.stat_count_distinct, reverse=True)
          values = self._prepare_forum_values(forum=forum, searches={'badges': True})
          values.update({
  
      @http.route(['''/forum/<model("forum.forum"):forum>/badge/<model("gamification.badge"):badge>'''], type='http', auth="public", website=True)
      def badge_users(self, forum, badge, **kwargs):
 -        user_ids = [badge_user.user_id.id for badge_user in badge.owner_ids]
 -        users = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, user_ids, context=request.context)
 +        users = [badge_user.user_id for badge_user in badge.owner_ids]
          values = self._prepare_forum_values(forum=forum, searches={'badges': True})
          values.update({
              'badge': badge,
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment/<model("mail.message"):comment>/convert_to_answer', type='http', auth="user", methods=['POST'], website=True)
      def convert_comment_to_answer(self, forum, post, comment, **kwarg):
 -        new_post_id = request.registry['forum.post'].convert_comment_to_answer(request.cr, request.uid, comment.id, context=request.context)
 -        if not new_post_id:
 +        post = request.env['forum.post'].convert_comment_to_answer(comment.id)
 +        if not post:
              return werkzeug.utils.redirect("/forum/%s" % slug(forum))
 -        post = request.registry['forum.post'].browse(request.cr, request.uid, new_post_id, context=request.context)
          question = post.parent_id if post.parent_id else post
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/convert_to_comment', type='http', auth="user", methods=['POST'], website=True)
      def convert_answer_to_comment(self, forum, post, **kwarg):
          question = post.parent_id
 -        new_msg_id = request.registry['forum.post'].convert_answer_to_comment(request.cr, request.uid, post.id, context=request.context)
 +        new_msg_id = post.convert_answer_to_comment()[0]
          if not new_msg_id:
              return werkzeug.utils.redirect("/forum/%s" % slug(forum))
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
      def delete_comment(self, forum, post, comment, **kwarg):
          if not request.session.uid:
              return {'error': 'anonymous_user'}
 -        return request.registry['forum.post'].unlink_comment(request.cr, request.uid, post.id, comment.id, context=request.context)
 +        return post.unlink_comment(comment.id)[0]
  # -*- coding: utf-8 -*-
  
  from datetime import datetime
++import logging
 +import math
  import uuid
  from werkzeug.exceptions import Forbidden
  
 -import logging
 -import openerp
 -
 -from openerp import api, tools
 +from openerp import _
 +from openerp import api, fields, models
 +from openerp import modules
 +from openerp import tools
  from openerp import SUPERUSER_ID
  from openerp.addons.website.models.website import slug
  from openerp.exceptions import Warning
 -from openerp.osv import osv, fields
 -from openerp.tools import html2plaintext
 -from openerp.tools.translate import _
  
+ _logger = logging.getLogger(__name__)
  
  class KarmaError(Forbidden):
      """ Karma-related error, used for forum and posts. """
      pass
  
  
 -class Forum(osv.Model):
 -    """TDE TODO: set karma values for actions dynamic for a given forum"""
 +class Forum(models.Model):
      _name = 'forum.forum'
 -    _description = 'Forums'
 +    _description = 'Forum'
      _inherit = ['mail.thread', 'website.seo.metadata']
  
      def init(self, cr):
 -        """ Add forum uuid for user email validation. """
 +        """ Add forum uuid for user email validation.
 +
 +        TDE TODO: move me somewhere else, auto_init ? """
          forum_uuids = self.pool['ir.config_parameter'].search(cr, SUPERUSER_ID, [('key', '=', 'website_forum.uuid')])
          if not forum_uuids:
              self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'website_forum.uuid', str(uuid.uuid4()), ['base.group_system'])
  
 -    _columns = {
 -        'name': fields.char('Name', required=True, translate=True),
 -        'faq': fields.html('Guidelines'),
 -        'description': fields.html('Description'),
 -        # karma generation
 -        'karma_gen_question_new': fields.integer('Asking a question'),
 -        'karma_gen_question_upvote': fields.integer('Question upvoted'),
 -        'karma_gen_question_downvote': fields.integer('Question downvoted'),
 -        'karma_gen_answer_upvote': fields.integer('Answer upvoted'),
 -        'karma_gen_answer_downvote': fields.integer('Answer downvoted'),
 -        'karma_gen_answer_accept': fields.integer('Accepting an answer'),
 -        'karma_gen_answer_accepted': fields.integer('Answer accepted'),
 -        'karma_gen_answer_flagged': fields.integer('Answer flagged'),
 -        # karma-based actions
 -        'karma_ask': fields.integer('Ask a question'),
 -        'karma_answer': fields.integer('Answer a question'),
 -        'karma_edit_own': fields.integer('Edit its own posts'),
 -        'karma_edit_all': fields.integer('Edit all posts'),
 -        'karma_close_own': fields.integer('Close its own posts'),
 -        'karma_close_all': fields.integer('Close all posts'),
 -        'karma_unlink_own': fields.integer('Delete its own posts'),
 -        'karma_unlink_all': fields.integer('Delete all posts'),
 -        'karma_upvote': fields.integer('Upvote'),
 -        'karma_downvote': fields.integer('Downvote'),
 -        'karma_answer_accept_own': fields.integer('Accept an answer on its own questions'),
 -        'karma_answer_accept_all': fields.integer('Accept an answer to all questions'),
 -        'karma_editor_link_files': fields.integer('Linking files (Editor)'),
 -        'karma_editor_clickable_link': fields.integer('Clickable links (Editor)'),
 -        'karma_comment_own': fields.integer('Comment its own posts'),
 -        'karma_comment_all': fields.integer('Comment all posts'),
 -        'karma_comment_convert_own': fields.integer('Convert its own answers to comments and vice versa'),
 -        'karma_comment_convert_all': fields.integer('Convert all answers to comments and vice versa'),
 -        'karma_comment_unlink_own': fields.integer('Unlink its own comments'),
 -        'karma_comment_unlink_all': fields.integer('Unlink all comments'),
 -        'karma_retag': fields.integer('Change question tags'),
 -        'karma_flag': fields.integer('Flag a post as offensive'),
 -    }
 -
 -    def _get_default_faq(self, cr, uid, context=None):
 -        fname = openerp.modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html')
 +    @api.model
 +    def _get_default_faq(self):
 +        fname = modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html')
          with open(fname, 'r') as f:
              return f.read()
          return False
  
 -    _defaults = {
 -        'description': 'This community is for professionals and enthusiasts of our products and services.',
 -        'faq': _get_default_faq,
 -        'karma_gen_question_new': 0,  # set to null for anti spam protection
 -        'karma_gen_question_upvote': 5,
 -        'karma_gen_question_downvote': -2,
 -        'karma_gen_answer_upvote': 10,
 -        'karma_gen_answer_downvote': -2,
 -        'karma_gen_answer_accept': 2,
 -        'karma_gen_answer_accepted': 15,
 -        'karma_gen_answer_flagged': -100,
 -        'karma_ask': 3,  # set to not null for anti spam protection
 -        'karma_answer': 3,  # set to not null for anti spam protection
 -        'karma_edit_own': 1,
 -        'karma_edit_all': 300,
 -        'karma_close_own': 100,
 -        'karma_close_all': 500,
 -        'karma_unlink_own': 500,
 -        'karma_unlink_all': 1000,
 -        'karma_upvote': 5,
 -        'karma_downvote': 50,
 -        'karma_answer_accept_own': 20,
 -        'karma_answer_accept_all': 500,
 -        'karma_editor_link_files': 20,
 -        'karma_editor_clickable_link': 20,
 -        'karma_comment_own': 3,
 -        'karma_comment_all': 5,
 -        'karma_comment_convert_own': 50,
 -        'karma_comment_convert_all': 500,
 -        'karma_comment_unlink_own': 50,
 -        'karma_comment_unlink_all': 500,
 -        'karma_retag': 75,
 -        'karma_flag': 500,
 -    }
 -
 -    def create(self, cr, uid, values, context=None):
 -        if context is None:
 -            context = {}
 -        create_context = dict(context, mail_create_nolog=True)
 -        return super(Forum, self).create(cr, uid, values, context=create_context)
 -
 -
 -class Post(osv.Model):
 +    # description and use
 +    name = fields.Char('Forum Name', required=True, translate=True)
 +    faq = fields.Html('Guidelines', default=_get_default_faq, translate=True)
 +    description = fields.Html(
 +        'Description',
 +        default='<p> This community is for professionals and enthusiasts of our products and services.'
 +                'Share and discuss the best content and new marketing ideas,'
 +                'build your professional profile and become a better marketer together.</p>')
 +    default_order = fields.Selection([
 +        ('create_date desc', 'Newest'),
 +        ('write_date desc', 'Last Updated'),
 +        ('vote_count desc', 'Most Voted'),
 +        ('relevancy desc', 'Relevancy'),
 +        ('child_count desc', 'Answered')],
 +        string='Default Order', required=True, default='write_date desc')
 +    relevancy_post_vote = fields.Float('First Relevancy Parameter', default=0.8)
 +    relevancy_time_decay = fields.Float('Second Relevancy Parameter', default=1.8)
 +    default_post_type = fields.Selection([
 +        ('question', 'Question'),
 +        ('discussion', 'Discussion'),
 +        ('link', 'Link')],
 +        string='Default Post', required=True, default='question')
 +    allow_question = fields.Boolean('Questions', help="Users can answer only once per question. Contributors can edit answers and mark the right ones.", default=True)
 +    allow_discussion = fields.Boolean('Discussions', default=False)
 +    allow_link = fields.Boolean('Links', help="When clicking on the post, it redirects to an external link", default=False)
 +    # karma generation
 +    karma_gen_question_new = fields.Integer(string='Asking a question', default=2)
 +    karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5)
 +    karma_gen_question_downvote = fields.Integer(string='Question downvoted', default=-2)
 +    karma_gen_answer_upvote = fields.Integer(string='Answer upvoted', default=10)
 +    karma_gen_answer_downvote = fields.Integer(string='Answer downvoted', default=-2)
 +    karma_gen_answer_accept = fields.Integer(string='Accepting an answer', default=2)
 +    karma_gen_answer_accepted = fields.Integer(string='Answer accepted', default=15)
 +    karma_gen_answer_flagged = fields.Integer(string='Answer flagged', default=-100)
 +    # karma-based actions
 +    karma_ask = fields.Integer(string='Ask a new question', default=3)
 +    karma_answer = fields.Integer(string='Answer a question', default=3)
 +    karma_edit_own = fields.Integer(string='Edit its own posts', default=1)
 +    karma_edit_all = fields.Integer(string='Edit all posts', default=300)
 +    karma_close_own = fields.Integer(string='Close its own posts', default=100)
 +    karma_close_all = fields.Integer(string='Close all posts', default=500)
 +    karma_unlink_own = fields.Integer(string='Delete its own posts', default=500)
 +    karma_unlink_all = fields.Integer(string='Delete all posts', default=1000)
 +    karma_upvote = fields.Integer(string='Upvote', default=5)
 +    karma_downvote = fields.Integer(string='Downvote', default=50)
 +    karma_answer_accept_own = fields.Integer(string='Accept an answer on its own questions', default=20)
 +    karma_answer_accept_all = fields.Integer(string='Accept an answers to all questions', default=500)
 +    karma_editor_link_files = fields.Integer(string='Linking files (Editor)', default=20)
 +    karma_editor_clickable_link = fields.Integer(string='Add clickable links (Editor)', default=20)
 +    karma_comment_own = fields.Integer(string='Comment its own posts', default=1)
 +    karma_comment_all = fields.Integer(string='Comment all posts', default=1)
 +    karma_comment_convert_own = fields.Integer(string='Convert its own answers to comments and vice versa', default=50)
 +    karma_comment_convert_all = fields.Integer(string='Convert all answers to answers and vice versa', default=500)
 +    karma_comment_unlink_own = fields.Integer(string='Unlink its own comments', default=50)
 +    karma_comment_unlink_all = fields.Integer(string='Unlinnk all comments', default=500)
 +    karma_retag = fields.Integer(string='Change question tags', default=75)
 +    karma_flag = fields.Integer(string='Flag a post as offensive', default=500)
 +
 +    @api.model
 +    def create(self, values):
 +        return super(Forum, self.with_context(mail_create_nolog=True)).create(values)
 +
 +
 +class Post(models.Model):
      _name = 'forum.post'
      _description = 'Forum Post'
      _inherit = ['mail.thread', 'website.seo.metadata']
      _order = "is_correct DESC, vote_count DESC, write_date DESC"
  
 -    def _get_user_vote(self, cr, uid, ids, field_name, arg, context):
 -        res = dict.fromkeys(ids, 0)
 -        vote_ids = self.pool['forum.post.vote'].search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
 -        for vote in self.pool['forum.post.vote'].browse(cr, uid, vote_ids, context=context):
 -            res[vote.post_id.id] = vote.vote
 -        return res
 -
 -    def _get_vote_count(self, cr, uid, ids, field_name, arg, context):
 -        res = dict.fromkeys(ids, 0)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            for vote in post.vote_ids:
 -                res[post.id] += int(vote.vote)
 -        return res
 -
 -    def _get_post_from_vote(self, cr, uid, ids, context=None):
 -        result = {}
 -        for vote in self.pool['forum.post.vote'].browse(cr, uid, ids, context=context):
 -            result[vote.post_id.id] = True
 -        return result.keys()
 -
 -    def _get_user_favourite(self, cr, uid, ids, field_name, arg, context):
 -        res = dict.fromkeys(ids, False)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            if uid in [f.id for f in post.favourite_ids]:
 -                res[post.id] = True
 -        return res
 -
 -    def _get_favorite_count(self, cr, uid, ids, field_name, arg, context):
 -        res = dict.fromkeys(ids, 0)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            res[post.id] += len(post.favourite_ids)
 -        return res
 -
 -    def _get_post_from_hierarchy(self, cr, uid, ids, context=None):
 -        post_ids = set(ids)
 -        for post in self.browse(cr, SUPERUSER_ID, ids, context=context):
 -            if post.parent_id:
 -                post_ids.add(post.parent_id.id)
 -        return list(post_ids)
 -
 -    def _get_child_count(self, cr, uid, ids, field_name=False, arg={}, context=None):
 -        res = dict.fromkeys(ids, 0)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            if post.parent_id:
 -                res[post.parent_id.id] = len(post.parent_id.child_ids)
 -            else:
 -                res[post.id] = len(post.child_ids)
 -        return res
 -
 -    def _get_uid_answered(self, cr, uid, ids, field_name, arg, context=None):
 -        res = dict.fromkeys(ids, False)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            res[post.id] = any(answer.create_uid.id == uid for answer in post.child_ids)
 -        return res
 -
 -    def _get_has_validated_answer(self, cr, uid, ids, field_name, arg, context=None):
 -        res = dict.fromkeys(ids, False)
 -        ans_ids = self.search(cr, uid, [('parent_id', 'in', ids), ('is_correct', '=', True)], context=context)
 -        for answer in self.browse(cr, uid, ans_ids, context=context):
 -            res[answer.parent_id.id] = True
 -        return res
 -
 -    def _is_self_reply(self, cr, uid, ids, field_name, arg, context=None):
 -        res = dict.fromkeys(ids, False)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            res[post.id] = post.parent_id and post.parent_id.create_uid == post.create_uid or False
 -        return res
 -
 -    def _get_post_karma_rights(self, cr, uid, ids, field_name, arg, context=None):
 -        user = self.pool['res.users'].browse(cr, uid, uid, context=context)
 -        res = dict.fromkeys(ids, False)
 -        for post in self.browse(cr, uid, ids, context=context):
 -            res[post.id] = {
 -                'karma_ask': post.forum_id.karma_ask,
 -                'karma_answer': post.forum_id.karma_answer,
 -                'karma_accept': post.parent_id and post.parent_id.create_uid.id == uid and post.forum_id.karma_answer_accept_own or post.forum_id.karma_answer_accept_all,
 -                'karma_edit': post.create_uid.id == uid and post.forum_id.karma_edit_own or post.forum_id.karma_edit_all,
 -                'karma_close': post.create_uid.id == uid and post.forum_id.karma_close_own or post.forum_id.karma_close_all,
 -                'karma_unlink': post.create_uid.id == uid and post.forum_id.karma_unlink_own or post.forum_id.karma_unlink_all,
 -                'karma_upvote': post.forum_id.karma_upvote,
 -                'karma_downvote': post.forum_id.karma_downvote,
 -                'karma_comment': post.create_uid.id == uid and post.forum_id.karma_comment_own or post.forum_id.karma_comment_all,
 -                'karma_comment_convert': post.create_uid.id == uid and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all,
 -            }
 -            res[post.id].update({
 -                'can_ask': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_ask'],
 -                'can_answer': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_answer'],
 -                'can_accept': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_accept'],
 -                'can_edit': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_edit'],
 -                'can_close': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_close'],
 -                'can_unlink': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_unlink'],
 -                'can_upvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_upvote'],
 -                'can_downvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_downvote'],
 -                'can_comment': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment'],
 -                'can_comment_convert': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment_convert'],
 -            })
 -        return res
 -
 -    _columns = {
 -        'name': fields.char('Title'),
 -        'forum_id': fields.many2one('forum.forum', 'Forum', required=True),
 -        'content': fields.html('Content'),
 -        'tag_ids': fields.many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', 'Tags'),
 -        'state': fields.selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], 'Status'),
 -        'views': fields.integer('Number of Views'),
 -        'active': fields.boolean('Active'),
 -        'is_correct': fields.boolean('Valid Answer', help='Correct Answer or Answer on this question accepted.'),
 -        'website_message_ids': fields.one2many(
 -            'mail.message', 'res_id',
 -            domain=lambda self: [
 -                '&', ('model', '=', self._name), ('type', 'in', ['email', 'comment'])
 -            ],
 -            string='Post Messages', help="Comments on forum post",
 -        ),
 -        # history
 -        'create_date': fields.datetime('Asked on', select=True, readonly=True),
 -        'create_uid': fields.many2one('res.users', 'Created by', select=True, readonly=True),
 -        'write_date': fields.datetime('Update on', select=True, readonly=True),
 -        'write_uid': fields.many2one('res.users', 'Updated by', select=True, readonly=True),
 -        # vote fields
 -        'vote_ids': fields.one2many('forum.post.vote', 'post_id', 'Votes'),
 -        'user_vote': fields.function(_get_user_vote, string='My Vote', type='integer'),
 -        'vote_count': fields.function(
 -            _get_vote_count, string="Votes", type='integer',
 -            store={
 -                'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['vote_ids'], 10),
 -                'forum.post.vote': (_get_post_from_vote, [], 10),
 -            }),
 -        # favorite fields
 -        'favourite_ids': fields.many2many('res.users', string='Favourite'),
 -        'user_favourite': fields.function(_get_user_favourite, string="My Favourite", type='boolean'),
 -        'favourite_count': fields.function(
 -            _get_favorite_count, string='Favorite Count', type='integer',
 -            store={
 -                'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['favourite_ids'], 10),
 -            }),
 -        # hierarchy
 -        'parent_id': fields.many2one('forum.post', 'Question', ondelete='cascade'),
 -        'self_reply': fields.function(
 -            _is_self_reply, 'Reply to own question', type='boolean',
 -            store={
 -                'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['parent_id', 'create_uid'], 10),
 -            }),
 -        'child_ids': fields.one2many('forum.post', 'parent_id', 'Answers'),
 -        'child_count': fields.function(
 -            _get_child_count, string="Answers", type='integer',
 -            store={
 -                'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids'], 10),
 -            }),
 -        'uid_has_answered': fields.function(
 -            _get_uid_answered, string='Has Answered', type='boolean',
 -        ),
 -        'has_validated_answer': fields.function(
 -            _get_has_validated_answer, string='Has a Validated Answered', type='boolean',
 -            store={
 -                'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids', 'is_correct'], 10),
 -            }
 -        ),
 -        # closing
 -        'closed_reason_id': fields.many2one('forum.post.reason', 'Reason'),
 -        'closed_uid': fields.many2one('res.users', 'Closed by', select=1),
 -        'closed_date': fields.datetime('Closed on', readonly=True),
 -        # karma
 -        'karma_ask': fields.function(_get_post_karma_rights, string='Karma to ask', type='integer', multi='_get_post_karma_rights'),
 -        'karma_answer': fields.function(_get_post_karma_rights, string='Karma to answer', type='integer', multi='_get_post_karma_rights'),
 -        'karma_accept': fields.function(_get_post_karma_rights, string='Karma to accept this answer', type='integer', multi='_get_post_karma_rights'),
 -        'karma_edit': fields.function(_get_post_karma_rights, string='Karma to edit', type='integer', multi='_get_post_karma_rights'),
 -        'karma_close': fields.function(_get_post_karma_rights, string='Karma to close', type='integer', multi='_get_post_karma_rights'),
 -        'karma_unlink': fields.function(_get_post_karma_rights, string='Karma to unlink', type='integer', multi='_get_post_karma_rights'),
 -        'karma_upvote': fields.function(_get_post_karma_rights, string='Karma to upvote', type='integer', multi='_get_post_karma_rights'),
 -        'karma_downvote': fields.function(_get_post_karma_rights, string='Karma to downvote', type='integer', multi='_get_post_karma_rights'),
 -        'karma_comment': fields.function(_get_post_karma_rights, string='Karma to comment', type='integer', multi='_get_post_karma_rights'),
 -        'karma_comment_convert': fields.function(_get_post_karma_rights, string='karma to convert as a comment', type='integer', multi='_get_post_karma_rights'),
 -        # access rights
 -        'can_ask': fields.function(_get_post_karma_rights, string='Can Ask', type='boolean', multi='_get_post_karma_rights'),
 -        'can_answer': fields.function(_get_post_karma_rights, string='Can Answer', type='boolean', multi='_get_post_karma_rights'),
 -        'can_accept': fields.function(_get_post_karma_rights, string='Can Accept', type='boolean', multi='_get_post_karma_rights'),
 -        'can_edit': fields.function(_get_post_karma_rights, string='Can Edit', type='boolean', multi='_get_post_karma_rights'),
 -        'can_close': fields.function(_get_post_karma_rights, string='Can Close', type='boolean', multi='_get_post_karma_rights'),
 -        'can_unlink': fields.function(_get_post_karma_rights, string='Can Unlink', type='boolean', multi='_get_post_karma_rights'),
 -        'can_upvote': fields.function(_get_post_karma_rights, string='Can Upvote', type='boolean', multi='_get_post_karma_rights'),
 -        'can_downvote': fields.function(_get_post_karma_rights, string='Can Downvote', type='boolean', multi='_get_post_karma_rights'),
 -        'can_comment': fields.function(_get_post_karma_rights, string='Can Comment', type='boolean', multi='_get_post_karma_rights'),
 -        'can_comment_convert': fields.function(_get_post_karma_rights, string='Can Convert to Comment', type='boolean', multi='_get_post_karma_rights'),
 -    }
 -
 -    _defaults = {
 -        'state': 'active',
 -        'views': 0,
 -        'active': True,
 -        'vote_ids': list(),
 -        'favourite_ids': list(),
 -        'child_ids': list(),
 -    }
 -
 -    def create(self, cr, uid, vals, context=None):
 -        if context is None:
 -            context = {}
 -        create_context = dict(context, mail_create_nolog=True)
 -        post_id = super(Post, self).create(cr, uid, vals, context=create_context)
 -        post = self.browse(cr, uid, post_id, context=context)
 +    name = fields.Char('Title')
 +    forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
 +    content = fields.Html('Content')
 +    content_link = fields.Char('URL', help="URL of Link Articles")
 +    tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags')
 +    state = fields.Selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], string='Status', default='active')
 +    views = fields.Integer('Number of Views', default=0)
 +    active = fields.Boolean('Active', default=True)
 +    post_type = fields.Selection([
 +        ('question', 'Question'),
 +        ('link', 'Article'),
 +        ('discussion', 'Discussion')],
 +        string='Type', default='question')
 +    website_message_ids = fields.One2many(
 +        'mail.message', 'res_id',
 +        domain=lambda self: ['&', ('model', '=', self._name), ('type', 'in', ['email', 'comment'])],
 +        string='Post Messages', help="Comments on forum post",
 +    )
 +    # history
 +    create_date = fields.Datetime('Asked on', select=True, readonly=True)
 +    create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True)
 +    write_date = fields.Datetime('Update on', select=True, readonly=True)
 +    write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True)
 +    relevancy = fields.Float('Relevancy', compute="_compute_relevancy", store=True)
 +
 +    @api.one
 +    @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay')
 +    def _compute_relevancy(self):
 +        days = (datetime.today() - datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days
 +        self.relevancy = math.copysign(1, self.vote_count) * (abs(self.vote_count - 1) ** self.forum_id.relevancy_post_vote / (days + 2) ** self.forum_id.relevancy_time_decay)
 +
 +    # vote
 +    vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes')
 +    user_vote = fields.Integer('My Vote', compute='_get_user_vote')
 +    vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True)
 +
 +    @api.multi
 +    def _get_user_vote(self):
 +        votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id'])
 +        mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes])
 +        for vote in self:
 +            vote.user_vote = mapped_vote.get(vote.id, 0)
 +
 +    @api.multi
 +    @api.depends('vote_ids')
 +    def _get_vote_count(self):
 +        read_group_res = self.env['forum.post.vote'].read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False)
 +        result = dict.fromkeys(self._ids, 0)
 +        for data in read_group_res:
 +            result[data['post_id'][0]] += data['__count'] * int(data['vote'])
 +        for post in self:
 +            post.vote_count = result[post.id]
 +
 +    # favorite
 +    favourite_ids = fields.Many2many('res.users', string='Favourite')
 +    user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite')
 +    favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True)
 +
 +    @api.one
 +    def _get_user_favourite(self):
 +        self.user_favourite = self._uid in self.favourite_ids.ids
 +
 +    @api.one
 +    @api.depends('favourite_ids')
 +    def _get_favorite_count(self):
 +        self.favourite_count = len(self.favourite_ids)
 +
 +    # hierarchy
 +    is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted')
 +    parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade')
 +    self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True)
 +    child_ids = fields.One2many('forum.post', 'parent_id', string='Answers')
 +    child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True)
 +    uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered')
 +    has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True)
 +
 +    @api.multi
 +    @api.depends('create_uid', 'parent_id')
 +    def _is_self_reply(self):
 +        self_replies = self.search([('parent_id.create_uid', '=', self._uid)])
 +        for post in self:
 +            post.is_self_reply = post in self_replies
 +
 +    @api.one
 +    @api.depends('child_ids')
 +    def _get_child_count(self):
 +        self.child_count = len(self.child_ids)
 +
 +    @api.one
 +    def _get_uid_has_answered(self):
 +        self.uid_has_answered = any(answer.create_uid.id == self._uid for answer in self.child_ids)
 +
 +    @api.multi
 +    @api.depends('child_ids', 'is_correct')
 +    def _get_has_validated_answer(self):
 +        correct_posts = [ans.parent_id for ans in self.search([('parent_id', 'in', self._ids), ('is_correct', '=', True)])]
 +        for post in self:
 +            post.is_correct = post in correct_posts
 +
 +    # closing
 +    closed_reason_id = fields.Many2one('forum.post.reason', string='Reason')
 +    closed_uid = fields.Many2one('res.users', string='Closed by', select=1)
 +    closed_date = fields.Datetime('Closed on', readonly=True)
 +    # karma calculation and access
 +    karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights')
 +    karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights')
 +    karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights')
 +    karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights')
 +    karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights')
 +    karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights')
 +    can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights')
 +    can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights')
 +    can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights')
 +    can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights')
 +    can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights')
 +    can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights')
 +    can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights')
 +    can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights')
 +    can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights')
 +    can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights')
 +
 +    @api.one
 +    def _get_post_karma_rights(self):
 +        user = self.env.user
 +
 +        self.karma_accept = self.parent_id and self.parent_id.create_uid.id == self._uid and self.forum_id.karma_answer_accept_own or self.forum_id.karma_answer_accept_all
 +        self.karma_edit = self.create_uid.id == self._uid and self.forum_id.karma_edit_own or self.forum_id.karma_edit_all
 +        self.karma_close = self.create_uid.id == self._uid and self.forum_id.karma_close_own or self.forum_id.karma_close_all
 +        self.karma_unlink = self.create_uid.id == self._uid and self.forum_id.karma_unlink_own or self.forum_id.karma_unlink_all
 +        self.karma_comment = self.create_uid.id == self._uid and self.forum_id.karma_comment_own or self.forum_id.karma_comment_all
 +        self.karma_comment_convert = self.create_uid.id == self._uid and self.forum_id.karma_comment_convert_own or self.forum_id.karma_comment_convert_all
 +
 +        self.can_ask = user.karma >= self.forum_id.karma_ask
 +        self.can_answer = user.karma >= self.forum_id.karma_answer
 +        self.can_accept = user.karma >= self.karma_accept
 +        self.can_edit = user.karma >= self.karma_edit
 +        self.can_close = user.karma >= self.karma_close
 +        self.can_unlink = user.karma >= self.karma_unlink
 +        self.can_upvote = user.karma >= self.forum_id.karma_upvote
 +        self.can_downvote = user.karma >= self.forum_id.karma_downvote
 +        self.can_comment = user.karma >= self.karma_comment
 +        self.can_comment_convert = user.karma >= self.karma_comment_convert
 +
 +    @api.model
 +    def create(self, vals):
 +        post = super(Post, self.with_context(mail_create_nolog=True)).create(vals)
 +        # deleted or closed questions
 +        if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False):
 +            raise Warning(_('Posting answer on a [Deleted] or [Closed] question is not possible'))
          # karma-based access
          if not post.parent_id and not post.can_ask:
              raise KarmaError('Not enough karma to create a new question')
          elif post.parent_id and not post.can_answer:
              raise KarmaError('Not enough karma to answer to a question')
          # messaging and chatter
 -        base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
 +        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
          if post.parent_id:
              body = _(
                  '<p>A new answer for <i>%s</i> has been posted. <a href="%s/forum/%s/question/%s">Click here to access the post.</a></p>' %
                  (post.parent_id.name, base_url, slug(post.parent_id.forum_id), slug(post.parent_id))
              )
 -            self.message_post(cr, uid, post.parent_id.id, subject=_('Re: %s') % post.parent_id.name, body=body, subtype='website_forum.mt_answer_new', context=context)
 +            post.parent_id.message_post(subject=_('Re: %s') % post.parent_id.name, body=body, subtype='website_forum.mt_answer_new')
          else:
              body = _(
                  '<p>A new question <i>%s</i> has been asked on %s. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' %
                  (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post))
              )
 -            self.message_post(cr, uid, post_id, subject=post.name, body=body, subtype='website_forum.mt_question_new', context=context)
 -            self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_question_new, context=context)
 -        return post_id
 +            post.message_post(subject=post.name, body=body, subtype='website_forum.mt_question_new')
 +            self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new)
 +        return post
  
 -    def write(self, cr, uid, ids, vals, context=None):
 -        posts = self.browse(cr, uid, ids, context=context)
 +    @api.multi
 +    def write(self, vals):
          if 'state' in vals:
 -            if vals['state'] in ['active', 'close'] and any(not post.can_close for post in posts):
 +            if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self):
                  raise KarmaError('Not enough karma to close or reopen a post.')
          if 'active' in vals:
 -            if any(not post.can_unlink for post in posts):
 +            if any(not post.can_unlink for post in self):
                  raise KarmaError('Not enough karma to delete or reactivate a post')
          if 'is_correct' in vals:
 -            if any(not post.can_accept for post in posts):
 +            if any(not post.can_accept for post in self):
                  raise KarmaError('Not enough karma to accept or refuse an answer')
              # update karma except for self-acceptance
              mult = 1 if vals['is_correct'] else -1
 -            for post in self.browse(cr, uid, ids, context=context):
 -                if vals['is_correct'] != post.is_correct and post.create_uid.id != uid:
 -                    self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * mult, context=context)
 -                    self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * mult, context=context)
 -        if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in posts):
 +            for post in self:
 +                if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
 +                    post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult)
 +                    self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
 +        if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in self):
              raise KarmaError('Not enough karma to edit a post.')
  
 -        res = super(Post, self).write(cr, uid, ids, vals, context=context)
 +        res = super(Post, self).write(vals)
          # if post content modify, notify followers
          if 'content' in vals or 'name' in vals:
 -            for post in posts:
 +            for post in self:
                  if post.parent_id:
                      body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit'
 -                    obj_id = post.parent_id.id
 +                    obj_id = post.parent_id
                  else:
                      body, subtype = _('Question Edited'), 'website_forum.mt_question_edit'
 -                    obj_id = post.id
 -                self.message_post(cr, uid, obj_id, body=body, subtype=subtype, context=context)
 +                    obj_id = post
 +                obj_id.message_post(body=body, subtype=subtype)
          return res
  
 -
 -    def reopen(self, cr, uid, ids, context=None):
 -        if any(post.parent_id or post.state != 'close'
 -                    for post in self.browse(cr, uid, ids, context=context)):
 +    @api.multi
++    def reopen(self):
++        if any(post.parent_id or post.state != 'close' for post in self):
+             return False
 -        reason_offensive = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_7')
 -        reason_spam = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_8')
 -        for post in self.browse(cr, uid, ids, context=context):
 -            if post.closed_reason_id.id in (reason_offensive, reason_spam):
++        reason_offensive = self.env.ref('website_forum.reason_7')
++        reason_spam = self.env.ref('website_forum.reason_8')
++        for post in self:
++            if post.closed_reason_id in (reason_offensive, reason_spam):
+                 _logger.info('Upvoting user <%s>, reopening spam/offensive question',
+                              post.create_uid.login)
+                 # TODO: in master, consider making this a tunable karma parameter
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id],
 -                                                 post.forum_id.karma_gen_question_downvote * -5,
 -                                                 context=context)
 -        self.pool['forum.post'].write(cr, SUPERUSER_ID, ids, {'state': 'active'}, context=context)
++                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * -5)
 -    def close(self, cr, uid, ids, reason_id, context=None):
 -        if any(post.parent_id for post in self.browse(cr, uid, ids, context=context)):
++        self.sudo().write({'state': 'active'}}
++
++    @api.multi
 +    def close(self, reason_id):
 +        if any(post.parent_id for post in self):
              return False
 -        reason_offensive = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_7')
 -        reason_spam = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_8')
++        reason_offensive = self.env.ref('website_forum.reason_7').id
++        reason_spam = self.env.ref('website_forum.reason_8').id
+         if reason_id in (reason_offensive, reason_spam):
 -            for post in self.browse(cr, uid, ids, context=context):
++            for post in self:
+                 _logger.info('Downvoting user <%s> for posting spam/offensive contents',
+                              post.create_uid.login)
+                 # TODO: in master, consider making this a tunable karma parameter
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id],
 -                                                 post.forum_id.karma_gen_question_downvote * 5,
 -                                                 context=context)
++                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * 5)
 -        self.pool['forum.post'].write(cr, uid, ids, {
 +        self.write({
              'state': 'close',
 -            'closed_uid': uid,
 +            'closed_uid': self._uid,
              'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
              'closed_reason_id': reason_id,
 -        }, context=context)
 +        })
 +        return True
  
 -    def unlink(self, cr, uid, ids, context=None):
 -        posts = self.browse(cr, uid, ids, context=context)
 -        if any(not post.can_unlink for post in posts):
 +    @api.multi
 +    def unlink(self):
 +        if any(not post.can_unlink for post in self):
              raise KarmaError('Not enough karma to unlink a post')
          # if unlinking an answer with accepted answer: remove provided karma
 -        for post in posts:
 +        for post in self:
              if post.is_correct:
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * -1, context=context)
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * -1, context=context)
 -        return super(Post, self).unlink(cr, uid, ids, context=context)
 -
 -    def vote(self, cr, uid, ids, upvote=True, context=None):
 -        Vote = self.pool['forum.post.vote']
 -        vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
 +                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
 +                self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
 +        return super(Post, self).unlink()
 +
 +    @api.multi
 +    def vote(self, upvote=True):
 +        Vote = self.env['forum.post.vote']
 +        vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)])
          new_vote = '1' if upvote else '-1'
          voted_forum_ids = set()
          if vote_ids:
 -            for vote in Vote.browse(cr, uid, vote_ids, context=context):
 +            for vote in vote_ids:
                  if upvote:
                      new_vote = '0' if vote.vote == '-1' else '1'
                  else:
                      new_vote = '0' if vote.vote == '1' else '-1'
 -                Vote.write(cr, uid, vote_ids, {'vote': new_vote}, context=context)
 +                vote.vote = new_vote
                  voted_forum_ids.add(vote.post_id.id)
 -        for post_id in set(ids) - voted_forum_ids:
 -            for post_id in ids:
 -                Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context)
 -        return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]], 'user_vote': new_vote}
 +        for post_id in set(self._ids) - voted_forum_ids:
 +            for post_id in self._ids:
 +                Vote.create({'post_id': post_id, 'vote': new_vote})
 +        return {'vote_count': self.vote_count, 'user_vote': new_vote}
  
 -    def convert_answer_to_comment(self, cr, uid, id, context=None):
 +    @api.one
 +    def convert_answer_to_comment(self):
          """ Tools to convert an answer (forum.post) to a comment (mail.message).
          The original post is unlinked and a new comment is posted on the question
          using the post create_uid as the comment's author. """
 -        post = self.browse(cr, SUPERUSER_ID, id, context=context)
 -        if not post.parent_id:
 +        if not self.parent_id:
              return False
  
          # karma-based action check: use the post field that computed own/all value
 -        if not post.can_comment_convert:
 +        if not self.can_comment_convert:
              raise KarmaError('Not enough karma to convert an answer to a comment')
  
          # post the message
 -        question = post.parent_id
 +        question = self.parent_id
          values = {
 -            'author_id': post.create_uid.partner_id.id,
 -            'body': html2plaintext(post.content),
 +            'author_id': self.create_uid.partner_id.id,
 +            'body': tools.html2plaintext(self.content),
              'type': 'comment',
              'subtype': 'mail.mt_comment',
 -            'date': post.create_date,
 +            'date': self.create_date,
          }
 -        message_id = self.pool['forum.post'].message_post(
 -            cr, uid, question.id,
 -            context=dict(context, mail_create_nosubcribe=True),
 -            **values)
 +        new_message = self.browse(question.id).with_context(mail_create_nosubcribe=True).message_post(**values)
  
          # unlink the original answer, using SUPERUSER_ID to avoid karma issues
 -        self.pool['forum.post'].unlink(cr, SUPERUSER_ID, [post.id], context=context)
 +        self.sudo().unlink()
  
 -        return message_id
 +        return new_message
  
 -    def convert_comment_to_answer(self, cr, uid, message_id, default=None, context=None):
 +    @api.model
 +    def convert_comment_to_answer(self, message_id, default=None):
          """ Tool to convert a comment (mail.message) into an answer (forum.post).
          The original comment is unlinked and a new answer from the comment's author
          is created. Nothing is done if the comment's author already answered the
          question. """
 -        comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
 -        post = self.pool['forum.post'].browse(cr, uid, comment.res_id, context=context)
 -        user = self.pool['res.users'].browse(cr, uid, uid, context=context)
 +        comment = self.env['mail.message'].sudo().browse(message_id)
 +        post = self.browse(comment.res_id)
          if not comment.author_id or not comment.author_id.user_ids:  # only comment posted by users can be converted
              return False
  
          # karma-based action check: must check the message's author to know if own / all
 -        karma_convert = comment.author_id.id == user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all
 -        can_convert = uid == SUPERUSER_ID or user.karma >= karma_convert
 +        karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all
 +        can_convert = self.env.user.karma >= karma_convert
          if not can_convert:
              raise KarmaError('Not enough karma to convert a comment to an answer')
  
              'parent_id': question.id,
          }
          # done with the author user to have create_uid correctly set
 -        new_post_id = self.pool['forum.post'].create(cr, post_create_uid.id, post_values, context=context)
 +        new_post = self.sudo(post_create_uid.id).create(post_values)
  
          # delete comment
 -        self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [comment.id], context=context)
 +        comment.unlink()
  
 -        return new_post_id
 +        return new_post
  
 -    def unlink_comment(self, cr, uid, id, message_id, context=None):
 -        comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
 -        post = self.pool['forum.post'].browse(cr, uid, id, context=context)
 -        user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
 -        if not comment.model == 'forum.post' or not comment.res_id == id:
 +    @api.one
 +    def unlink_comment(self, message_id):
 +        user = self.env.user
 +        comment = self.env['mail.message'].sudo().browse(message_id)
 +        if not comment.model == 'forum.post' or not comment.res_id == self.id:
              return False
 -
          # karma-based action check: must check the message's author to know if own or all
 -        karma_unlink = comment.author_id.id == user.partner_id.id and post.forum_id.karma_comment_unlink_own or post.forum_id.karma_comment_unlink_all
 -        can_unlink = uid == SUPERUSER_ID or user.karma >= karma_unlink
 +        karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all
 +        can_unlink = user.karma >= karma_unlink
          if not can_unlink:
              raise KarmaError('Not enough karma to unlink a comment')
 +        return comment.unlink()
  
 -        return self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [message_id], context=context)
 -
 -    def set_viewed(self, cr, uid, ids, context=None):
 -        cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (tuple(ids),))
 +    @api.multi
 +    def set_viewed(self):
 +        self._cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids,))
          return True
  
 -    def _get_access_link(self, cr, uid, mail, partner, context=None):
 -        post = self.pool['forum.post'].browse(cr, uid, mail.res_id, context=context)
 +    @api.model
 +    def _get_access_link(self, mail, partner):
 +        post = self.browse(mail.res_id)
          res_id = post.parent_id and "%s#answer-%s" % (post.parent_id.id, post.id) or post.id
          return "/forum/%s/question/%s" % (post.forum_id.id, res_id)
  
              else:
                  post_id = thread_id
              post = self.browse(cr, uid, post_id, context=context)
 +            # TDE FIXME: trigger browse because otherwise the function field is not compted - check with RCO
 +            tmp1, tmp2 = post.karma_comment, post.can_comment
 +            user = self.pool['res.users'].browse(cr, uid, uid)
 +            tmp3 = user.karma
 +            # TDE END FIXME
              if not post.can_comment:
                  raise KarmaError('Not enough karma to comment')
          return super(Post, self).message_post(cr, uid, thread_id, type=type, subtype=subtype, context=context, **kwargs)
  
  
 -class PostReason(osv.Model):
 +class PostReason(models.Model):
      _name = "forum.post.reason"
      _description = "Post Closing Reason"
      _order = 'name'
 -    _columns = {
 -        'name': fields.char('Post Reason', required=True, translate=True),
 -    }
 +
 +    name = fields.Char(string='Closing Reason', required=True, translate=True)
  
  
 -class Vote(osv.Model):
 +class Vote(models.Model):
      _name = 'forum.post.vote'
      _description = 'Vote'
 -    _columns = {
 -        'post_id': fields.many2one('forum.post', 'Post', ondelete='cascade', required=True),
 -        'user_id': fields.many2one('res.users', 'User', required=True),
 -        'vote': fields.selection([('1', '1'), ('-1', '-1'), ('0', '0')], 'Vote', required=True),
 -        'create_date': fields.datetime('Create Date', select=True, readonly=True),
 -
 -        # TODO master: store these two
 -        'forum_id': fields.related('post_id', 'forum_id', type='many2one', relation='forum.forum', string='Forum'),
 -        'recipient_id': fields.related('post_id', 'create_uid', type='many2one', relation='res.users', string='To', help="The user receiving the vote"),
 -    }
 -    _defaults = {
 -        'user_id': lambda self, cr, uid, ctx: uid,
 -        'vote': lambda *args: '1',
 -    }
 +
 +    post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True)
 +    user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid)
 +    vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1')
 +    create_date = fields.Datetime('Create Date', select=True, readonly=True)
 +    forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True)
 +    recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True)
  
      def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma):
          _karma_upd = {
          }
          return _karma_upd[old_vote][new_vote]
  
 -    def create(self, cr, uid, vals, context=None):
 -        vote_id = super(Vote, self).create(cr, uid, vals, context=context)
 -        vote = self.browse(cr, uid, vote_id, context=context)
 +    @api.model
 +    def create(self, vals):
 +        vote = super(Vote, self).create(vals)
  
          # own post check
          if vote.user_id.id == vote.post_id.create_uid.id:
          elif vote.vote == '-1' and not vote.post_id.can_downvote:
              raise KarmaError('Not enough karma to downvote.')
  
 -        # karma update
          if vote.post_id.parent_id:
              karma_value = self._get_karma_value('0', vote.vote, vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote)
          else:
              karma_value = self._get_karma_value('0', vote.vote, vote.forum_id.karma_gen_question_upvote, vote.forum_id.karma_gen_question_downvote)
 -        self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.recipient_id.id], karma_value, context=context)
 -        return vote_id
 +        vote.recipient_id.sudo().add_karma(karma_value)
 +        return vote
  
 -    def write(self, cr, uid, ids, values, context=None):
 +    @api.multi
 +    def write(self, values):
          if 'vote' in values:
 -            for vote in self.browse(cr, uid, ids, context=context):
 +            for vote in self:
                  # own post check
                  if vote.user_id.id == vote.post_id.create_uid.id:
                      raise Warning('Not allowed to vote for its own post')
                      karma_value = self._get_karma_value(vote.vote, values['vote'], vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote)
                  else:
                      karma_value = self._get_karma_value(vote.vote, values['vote'], vote.forum_id.karma_gen_question_upvote, vote.forum_id.karma_gen_question_downvote)
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.recipient_id.id], karma_value, context=context)
 -        res = super(Vote, self).write(cr, uid, ids, values, context=context)
 +                vote.recipient_id.sudo().add_karma(karma_value)
 +        res = super(Vote, self).write(values)
          return res
  
  
 -class Tags(osv.Model):
 +class Tags(models.Model):
      _name = "forum.tag"
 -    _description = "Tag"
 +    _description = "Forum Tag"
      _inherit = ['website.seo.metadata']
  
 -    def _get_posts_count(self, cr, uid, ids, field_name, arg, context=None):
 -        return dict((tag_id, self.pool['forum.post'].search_count(cr, uid, [('tag_ids', 'in', tag_id)], context=context)) for tag_id in ids)
 -
 -    def _get_tag_from_post(self, cr, uid, ids, context=None):
 -        return list(set(
 -            [tag.id for post in self.pool['forum.post'].browse(cr, SUPERUSER_ID, ids, context=context) for tag in post.tag_ids]
 -        ))
 -
 -    _columns = {
 -        'name': fields.char('Name', required=True),
 -        'forum_id': fields.many2one('forum.forum', 'Forum', required=True),
 -        'post_ids': fields.many2many('forum.post', 'forum_tag_rel', 'tag_id', 'post_id', 'Posts'),
 -        'posts_count': fields.function(
 -            _get_posts_count, type='integer', string="Number of Posts",
 -            store={
 -                'forum.post': (_get_tag_from_post, ['tag_ids'], 10),
 -            }
 -        ),
 -        'create_uid': fields.many2one('res.users', 'Created by', readonly=True),
 -    }
 +    name = fields.Char('Name', required=True)
 +    create_uid = fields.Many2one('res.users', string='Created by', readonly=True)
 +    forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
 +    post_ids = fields.Many2many('forum.post', 'forum_tag_rel', 'forum_tag_id', 'forum_id', string='Posts')
 +    posts_count = fields.Integer('Number of Posts', compute='_get_posts_count', store=True)
 +
 +    @api.multi
 +    @api.depends("post_ids.tag_ids")
 +    def _get_posts_count(self):
 +        for tag in self:
 +            tag.posts_count = len(tag.post_ids)
@@@ -175,11 -175,17 +175,11 @@@ class website_sale(http.Controller)
          else:
              pricelist = pool.get('product.pricelist').browse(cr, uid, context['pricelist'], context)
  
 -        product_obj = pool.get('product.template')
 -
          url = "/shop"
 -        product_count = product_obj.search_count(cr, uid, domain, context=context)
          if search:
              post["search"] = search
          if category:
              url = "/shop/category/%s" % slug(category)
 -        pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
 -        product_ids = product_obj.search(cr, uid, domain, limit=PPG, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
 -        products = product_obj.browse(cr, uid, product_ids, context=context)
  
          style_obj = pool['product.style']
          style_ids = style_obj.search(cr, uid, [], context=context)
          categories = category_obj.browse(cr, uid, category_ids, context=context)
          categs = filter(lambda x: not x.parent_id, categories)
  
 +        domain += [('public_categ_ids', 'in', category_ids)]
 +        product_obj = pool.get('product.template')
 +
 +        product_count = product_obj.search_count(cr, uid, domain, context=context)
 +        pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
-         product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
++        product_ids = product_obj.search(cr, uid, domain, limit=PPG, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
 +        products = product_obj.browse(cr, uid, product_ids, context=context)
 +
          attributes_obj = request.registry['product.attribute']
          attributes_ids = attributes_obj.search(cr, uid, [], context=context)
          attributes = attributes_obj.browse(cr, uid, attributes_ids, context=context)
                  'sale_order_id': order.id,
              }, context=context)
              request.session['sale_transaction_id'] = tx_id
 +            tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context)
  
          # update quotation
          request.registry['sale.order'].write(
                  'payment_tx_id': request.session['sale_transaction_id']
              }, context=context)
  
 +        # confirm the quotation
 +        if tx.acquirer_id.auto_confirm == 'at_pay_now':
 +            request.registry['sale.order'].action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context)
 +
          return tx_id
  
      @http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
diff --combined doc/conf.py
@@@ -46,16 -46,16 +46,16 @@@ master_doc = 'index
  
  # General information about the project.
  project = u'odoo'
- copyright = u'2014, OpenERP s.a.'
+ copyright = u'OpenERP S.A.'
  
  # The version info for the project you're documenting, acts as replacement for
  # |version| and |release|, also used in various other places throughout the
  # built documents.
  #
  # The short X.Y version.
 -version = '8.0'
 +version = 'master'
  # The full version, including alpha/beta/rc tags.
 -release = '8.0'
 +release = 'master'
  
  # There are two options for replacing |today|: either, you set today to some
  # non-false value, then it is used:
@@@ -204,7 -204,10 +204,10 @@@ class ir_model(osv.osv)
              vals['state']='manual'
          res = super(ir_model,self).create(cr, user, vals, context)
          if vals.get('state','base')=='manual':
+             # add model in registry
              self.instanciate(cr, user, vals['model'], context)
+             self.pool.setup_models(cr, partial=(not self.pool.ready))
+             # update database schema
              model = self.pool[vals['model']]
              ctx = dict(context,
                  field_name=vals['name'],
                  update_custom_fields=True)
              model._auto_init(cr, ctx)
              model._auto_end(cr, ctx) # actually create FKs!
-             self.pool.setup_models(cr, partial=(not self.pool.ready))
              RegistryManager.signal_registry_change(cr.dbname)
          return res
  
@@@ -351,8 -353,11 +353,11 @@@ class ir_model_fields(osv.osv)
          self._drop_column(cr, user, ids, context)
          res = super(ir_model_fields, self).unlink(cr, user, ids, context)
          if not context.get(MODULE_UNINSTALL_FLAG):
+             # The field we just deleted might have be inherited, and registry is
+             # inconsistent in this case; therefore we reload the registry.
              cr.commit()
-             self.pool.setup_models(cr, partial=(not self.pool.ready))
+             api.Environment.reset()
+             RegistryManager.new(cr.dbname)
              RegistryManager.signal_registry_change(cr.dbname)
          return res
  
                      cr.execute('SELECT * FROM ir_model_fields WHERE id=%s', (res,))
                      self.pool.fields_by_model.setdefault(vals['model'], []).append(cr.dictfetchone())
  
+                 # re-initialize model in registry
                  model.__init__(self.pool, cr)
-                 #Added context to _auto_init for special treatment to custom field for select_level
+                 self.pool.setup_models(cr, partial=(not self.pool.ready))
+                 # update database schema
+                 model = self.pool[vals['model']]
                  ctx = dict(context,
                      field_name=vals['name'],
                      field_state='manual',
                      update_custom_fields=True)
                  model._auto_init(cr, ctx)
                  model._auto_end(cr, ctx) # actually create FKs!
-                 self.pool.setup_models(cr, partial=(not self.pool.ready))
                  RegistryManager.signal_registry_change(cr.dbname)
  
          return res
                  if field.serialization_field_id and (field.name != vals['name']):
                      raise except_orm(_('Error!'),  _('Renaming sparse field "%s" is not allowed')%field.name)
  
-         column_rename = None # if set, *one* column can be renamed here
-         models_patch = {}    # structs of (obj, [(field, prop, change_to),..])
-                              # data to be updated on the orm model
+         # if set, *one* column can be renamed here
+         column_rename = None
+         # field patches {model: {field_name: {prop_name: prop_value, ...}, ...}, ...}
+         patches = defaultdict(lambda: defaultdict(dict))
  
          # static table of properties
          model_props = [ # (our-name, fields.prop, set_fn)
              ('field_description', 'string', tools.ustr),
              ('required', 'required', bool),
              ('readonly', 'readonly', bool),
-             ('domain', '_domain', eval),
+             ('domain', 'domain', eval),
              ('size', 'size', int),
              ('on_delete', 'ondelete', str),
              ('translate', 'translate', bool),
-             ('selectable', 'selectable', bool),
-             ('select_level', 'select', int),
+             ('select_level', 'index', lambda x: bool(int(x))),
              ('selection', 'selection', eval),
-             ]
+         ]
  
          if vals and ids:
              checked_selection = False # need only check it once, so defer
                  # We don't check the 'state', because it might come from the context
                  # (thus be set for multiple fields) and will be ignored anyway.
                  if obj is not None:
-                     models_patch.setdefault(obj._name, (obj,[]))
                      # find out which properties (per model) we need to update
-                     for field_name, field_property, set_fn in model_props:
+                     for field_name, prop_name, func in model_props:
                          if field_name in vals:
-                             property_value = set_fn(vals[field_name])
-                             if getattr(obj._columns[item.name], field_property) != property_value:
-                                 models_patch[obj._name][1].append((final_name, field_property, property_value))
-                         # our dict is ready here, but no properties are changed so far
+                             prop_value = func(vals[field_name])
+                             if getattr(obj._fields[item.name], prop_name) != prop_value:
+                                 patches[obj][final_name][prop_name] = prop_value
  
          # These shall never be written (modified)
          for column_name in ('model_id', 'model', 'state'):
              field = obj._pop_field(rename[1])
              obj._add_field(rename[2], field)
  
-         if models_patch:
+         if patches:
              # We have to update _columns of the model(s) and then call their
              # _auto_init to sync the db with the model. Hopefully, since write()
              # was called earlier, they will be in-sync before the _auto_init.
              # Anything we don't update in _columns now will be reset from
              # the model into ir.model.fields (db).
-             ctx = dict(context, select=vals.get('select_level', '0'),
-                        update_custom_fields=True)
-             for __, patch_struct in models_patch.items():
-                 obj = patch_struct[0]
-                 # TODO: update new-style fields accordingly
-                 for col_name, col_prop, val in patch_struct[1]:
-                     setattr(obj._columns[col_name], col_prop, val)
+             ctx = dict(context,
+                 select=vals.get('select_level', '0'),
+                 update_custom_fields=True,
+             )
+             for obj, model_patches in patches.iteritems():
+                 for field_name, field_patches in model_patches.iteritems():
+                     # update field properties, and adapt corresponding column
+                     field = obj._fields[field_name]
+                     attrs = dict(field._attrs, **field_patches)
+                     obj._add_field(field_name, field.new(**attrs))
+                 # update database schema
                  obj._auto_init(cr, ctx)
                  obj._auto_end(cr, ctx) # actually create FKs!
  
-         if column_rename or models_patch:
+         if column_rename or patches:
              self.pool.setup_models(cr, partial=(not self.pool.ready))
              RegistryManager.signal_registry_change(cr.dbname)
  
@@@ -528,7 -539,6 +539,7 @@@ class ir_model_constraint(Model)
      _columns = {
          'name': fields.char('Constraint', required=True, select=1,
              help="PostgreSQL constraint or foreign key name."),
 +        'definition': fields.char('Definition', help="PostgreSQL constraint definition"),
          'model': fields.many2one('ir.model', string='Model',
              required=True, select=1),
          'module': fields.many2one('ir.module.module', string='Module',
@@@ -256,14 -256,6 +256,14 @@@ class QWeb(orm.AbstractModel)
          generated_attributes = ""
          t_render = None
          template_attributes = {}
 +
 +        debugger = element.get('t-debug')
 +        if debugger is not None:
 +            if openerp.tools.config['dev_mode']:
 +                __import__(debugger).set_trace()  # pdb, ipdb, pudb, ...
 +            else:
 +                _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
 +
          for (attribute_name, attribute_value) in element.attrib.iteritems():
              attribute_name = str(attribute_name)
              if attribute_name == "groups":
              else:
                  generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
  
 -        if 'debug' in template_attributes:
 -            debugger = template_attributes.get('debug', 'pdb')
 -            __import__(debugger).set_trace()  # pdb, ipdb, pudb, ...
          if t_render:
              result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
          else:
              result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
  
          if element.tail:
 -            result += element.tail.encode('utf-8')
 +            result += self.render_tail(element.tail, element, qwebcontext)
  
          if isinstance(result, unicode):
              return result.encode('utf-8')
          if inner:
              g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
          else:
 -            g_inner = [] if element.text is None else [element.text.encode('utf-8')]
 +            g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)]
              for current_node in element.iterchildren(tag=etree.Element):
                  try:
                      g_inner.append(self.render_node(current_node, qwebcontext))
      def render_attribute(self, element, name, value, qwebcontext):
          return ' %s="%s"' % (name, escape(value))
  
 +    def render_text(self, text, element, qwebcontext):
 +        return text.encode('utf-8')
 +
 +    def render_tail(self, tail, element, qwebcontext):
 +        return tail.encode('utf-8')
 +
      # Attributes
      def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
          if attribute_name.startswith("t-attf-"):
          record, field_name = template_attributes["field"].rsplit('.', 1)
          record = self.eval_object(record, qwebcontext)
  
-         column = record._all_columns[field_name].column
+         field = record._fields[field_name]
          options = json.loads(template_attributes.get('field-options') or '{}')
-         field_type = get_field_type(column, options)
+         field_type = get_field_type(field, options)
  
          converter = self.get_converter_for(field_type)
  
@@@ -549,16 -538,16 +549,16 @@@ class FieldConverter(osv.AbstractModel)
          * ``model``, the name of the record's model
          * ``id`` the id of the record to which the field belongs
          * ``field`` the name of the converted field
-         * ``type`` the logical field type (widget, may not match the column's
-           ``type``, may not be any _column subclass name)
+         * ``type`` the logical field type (widget, may not match the field's
+           ``type``, may not be any Field subclass name)
          * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
-           column is translatable
+           field is translatable
          * ``expression``, the original expression
  
          :returns: iterable of (attribute name, attribute value) pairs.
          """
-         column = record._all_columns[field_name].column
-         field_type = get_field_type(column, options)
+         field = record._fields[field_name]
+         field_type = get_field_type(field, options)
          return [
              ('data-oe-model', record._name),
              ('data-oe-id', record.id),
              ('data-oe-expression', t_att['field']),
          ]
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
-         """ value_to_html(cr, uid, value, column, options=None, context=None)
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
+         """ value_to_html(cr, uid, value, field, options=None, context=None)
  
          Converts a single value to its HTML version/output
          """
          if not value: return ''
          return value
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
-         """ record_to_html(cr, uid, field_name, record, column, options=None, context=None)
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
+         """ record_to_html(cr, uid, field_name, record, options=None, context=None)
  
          Converts the specified field of the browse_record ``record`` to HTML
          """
+         field = record._fields[field_name]
          return self.value_to_html(
-             cr, uid, record[field_name], column, options=options, context=context)
+             cr, uid, record[field_name], field, options=options, context=context)
  
      def to_html(self, cr, uid, field_name, record, options,
                  source_element, t_att, g_att, qweb_context, context=None):
          field's own ``_type``.
          """
          try:
-             content = self.record_to_html(
-                 cr, uid, field_name, record,
-                 record._all_columns[field_name].column,
-                 options, context=context)
+             content = self.record_to_html(cr, uid, field_name, record, options, context=context)
              if options.get('html-escape', True):
                  content = escape(content)
              elif hasattr(content, '__html__'):
@@@ -659,14 -646,14 +657,14 @@@ class FloatConverter(osv.AbstractModel)
      _name = 'ir.qweb.field.float'
      _inherit = 'ir.qweb.field'
  
-     def precision(self, cr, uid, column, options=None, context=None):
-         _, precision = column.digits or (None, None)
+     def precision(self, cr, uid, field, options=None, context=None):
+         _, precision = field.digits or (None, None)
          return precision
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          if context is None:
              context = {}
-         precision = self.precision(cr, uid, column, options=options, context=context)
+         precision = self.precision(cr, uid, field, options=options, context=context)
          fmt = '%f' if precision is None else '%.{precision}f'
  
          lang_code = context.get('lang') or 'en_US'
@@@ -685,7 -672,7 +683,7 @@@ class DateConverter(osv.AbstractModel)
      _name = 'ir.qweb.field.date'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          if not value or len(value)<10: return ''
          lang = self.user_lang(cr, uid, context=context)
          locale = babel.Locale.parse(lang.code)
@@@ -708,7 -695,7 +706,7 @@@ class DateTimeConverter(osv.AbstractMod
      _name = 'ir.qweb.field.datetime'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          if not value: return ''
          lang = self.user_lang(cr, uid, context=context)
          locale = babel.Locale.parse(lang.code)
@@@ -734,7 -721,7 +732,7 @@@ class TextConverter(osv.AbstractModel)
      _name = 'ir.qweb.field.text'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          """
          Escapes the value and converts newlines to br. This is bullshit.
          """
@@@ -746,19 -733,19 +744,19 @@@ class SelectionConverter(osv.AbstractMo
      _name = 'ir.qweb.field.selection'
      _inherit = 'ir.qweb.field'
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
          value = record[field_name]
          if not value: return ''
-         selection = dict(fields.selection.reify(
-             cr, uid, record._model, column, context=context))
+         field = record._fields[field_name]
+         selection = dict(field.get_description(record.env)['selection'])
          return self.value_to_html(
-             cr, uid, selection[value], column, options=options)
+             cr, uid, selection[value], field, options=options)
  
  class ManyToOneConverter(osv.AbstractModel):
      _name = 'ir.qweb.field.many2one'
      _inherit = 'ir.qweb.field'
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
          [read] = record.read([field_name])
          if not read[field_name]: return ''
          _, value = read[field_name]
@@@ -768,7 -755,7 +766,7 @@@ class HTMLConverter(osv.AbstractModel)
      _name = 'ir.qweb.field.html'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          return HTMLSafe(value or '')
  
  class ImageConverter(osv.AbstractModel):
      _name = 'ir.qweb.field.image'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          try:
              image = Image.open(cStringIO.StringIO(value.decode('base64')))
              image.verify()
@@@ -816,7 -803,7 +814,7 @@@ class MonetaryConverter(osv.AbstractMod
              cr, uid, field_name, record, options,
              source_element, t_att, g_att, qweb_context, context=context)
  
-     def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options, context=None):
          if context is None:
              context = {}
          Currency = self.pool['res.currency']
@@@ -887,7 -874,7 +885,7 @@@ class DurationConverter(osv.AbstractMod
      _name = 'ir.qweb.field.duration'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          units = dict(TIMEDELTA_UNITS)
          if value < 0:
              raise ValueError(_("Durations can't be negative"))
@@@ -914,7 -901,7 +912,7 @@@ class RelativeDatetimeConverter(osv.Abs
      _name = 'ir.qweb.field.relative'
      _inherit = 'ir.qweb.field'
  
-     def value_to_html(self, cr, uid, value, column, options=None, context=None):
+     def value_to_html(self, cr, uid, value, field, options=None, context=None):
          parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
          locale = babel.Locale.parse(
              self.user_lang(cr, uid, context=context).code)
          if isinstance(value, basestring):
              value = datetime.datetime.strptime(value, parse_format)
  
-         # value should be a naive datetime in UTC. So is fields.datetime.now()
-         reference = datetime.datetime.strptime(column.now(), parse_format)
+         # value should be a naive datetime in UTC. So is fields.Datetime.now()
+         reference = datetime.datetime.strptime(field.now(), parse_format)
  
          return babel.dates.format_timedelta(
              value - reference, add_direction=True, locale=locale)
@@@ -932,33 -919,32 +930,32 @@@ class Contact(orm.AbstractModel)
      _name = 'ir.qweb.field.contact'
      _inherit = 'ir.qweb.field.many2one'
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
          if context is None:
              context = {}
  
          if options is None:
              options = {}
          opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
-         if not getattr(record, field_name):
-             return None
  
-         id = getattr(record, field_name).id
-         context.update(show_address=True)
-         field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context=context)
-         value = field_browse.name_get()[0][1]
+         value_rec = record[field_name]
+         if not value_rec:
+             return None
+         value_rec = value_rec.sudo().with_context(show_address=True)
+         value = value_rec.name_get()[0][1]
  
          val = {
              'name': value.split("\n")[0],
 -            'address': escape("\n".join(value.split("\n")[1:])),
 +            'address': escape("\n".join(value.split("\n")[1:])).strip(),
-             'phone': field_browse.phone,
-             'mobile': field_browse.mobile,
-             'fax': field_browse.fax,
-             'city': field_browse.city,
-             'country_id': field_browse.country_id.display_name,
-             'website': field_browse.website,
-             'email': field_browse.email,
+             'phone': value_rec.phone,
+             'mobile': value_rec.mobile,
+             'fax': value_rec.fax,
+             'city': value_rec.city,
+             'country_id': value_rec.country_id.display_name,
+             'website': value_rec.website,
+             'email': value_rec.email,
              'fields': opf,
-             'object': field_browse,
+             'object': value_rec,
              'options': options
          }
  
@@@ -970,7 -956,7 +967,7 @@@ class QwebView(orm.AbstractModel)
      _name = 'ir.qweb.field.qweb'
      _inherit = 'ir.qweb.field.many2one'
  
-     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+     def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
          if not getattr(record, field_name):
              return None
  
@@@ -1056,10 -1042,9 +1053,9 @@@ def nl2br(string, options=None)
          string = escape(string)
      return HTMLSafe(string.replace('\n', '<br>\n'))
  
- def get_field_type(column, options):
-     """ Gets a t-field's effective type from the field's column and its options
-     """
-     return options.get('widget', column._type)
+ def get_field_type(field, options):
+     """ Gets a t-field's effective type from the field definition and its options """
+     return options.get('widget', field.type)
  
  class AssetError(Exception):
      pass
@@@ -1067,8 -1052,17 +1063,8 @@@ class AssetNotFound(AssetError)
      pass
  
  class AssetsBundle(object):
 -    # Sass installation:
 -    #
 -    #       sudo gem install sass compass bootstrap-sass
 -    #
 -    # If the following error is encountered:
 -    #       'ERROR: Cannot load compass.'
 -    # Use this:
 -    #       sudo gem install compass --pre
 -    cmd_sass = ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', '-r', 'bootstrap-sass']
      rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
 -    rx_sass_import = re.compile("""(@import\s?['"]([^'"]+)['"])""")
 +    rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
      rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
  
      def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
                  media = el.get('media')
                  if el.tag == 'style':
                      if atype == 'text/sass' or src.endswith('.sass'):
 -                        self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
 +                        self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
 +                    elif atype == 'text/less' or src.endswith('.less'):
 +                        self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
                      else:
                          self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
                  elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
                      if href.endswith('.sass') or atype == 'text/sass':
 -                        self.stylesheets.append(SassAsset(self, url=href, media=media))
 +                        self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
 +                    elif href.endswith('.less') or atype == 'text/less':
 +                        self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
                      else:
                          self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
                  elif el.tag == 'script' and not src:
          response = []
          if debug:
              if css and self.stylesheets:
 -                self.compile_sass()
 +                self.preprocess_css()
 +                if self.css_errors:
 +                    msg = '\n'.join(self.css_errors)
 +                    self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
                  for style in self.stylesheets:
                      response.append(style.to_html())
              if js:
          return content
  
      def css(self):
 +        """Generate css content from given bundle"""
          content = self.get_cache('css')
          if content is None:
 -            self.compile_sass()
 -            content = '\n'.join(asset.minify() for asset in self.stylesheets)
 +            content = self.preprocess_css()
  
              if self.css_errors:
                  msg = '\n'.join(self.css_errors)
 -                content += self.css_message(msg.replace('\n', '\\A '))
 +                content += self.css_message(msg)
  
              # move up all @import rules to the top
              matches = []
  
      def set_cache(self, type, content):
          ira = self.registry['ir.attachment']
 -        url_prefix = '/web/%s/%s/' % (type, self.xmlid)
 -        # Invalidate previous caches
 -        oids = ira.search(self.cr, openerp.SUPERUSER_ID, [('url', '=like', url_prefix + '%')], context=self.context)
 -        if oids:
 -            ira.unlink(self.cr, openerp.SUPERUSER_ID, oids, context=self.context)
 -        url = url_prefix + self.version
 +        ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
 +        url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
          ira.create(self.cr, openerp.SUPERUSER_ID, dict(
                      datas=content.encode('utf8').encode('base64'),
                      type='binary',
                  ), context=self.context)
  
      def css_message(self, message):
 +        # '\A' == css content carriage return
 +        message = message.replace('\n', '\\A ').replace('"', '\\"')
          return """
              body:before {
                  background: #ffc;
                  white-space: pre;
                  content: "%s";
              }
 -        """ % message.replace('"', '\\"')
 +        """ % message
  
 -    def compile_sass(self):
 +    def preprocess_css(self):
          """
 -            Checks if the bundle contains any sass content, then compiles it to css.
 -            Css compilation is done at the bundle level and not in the assets
 -            because they are potentially interdependant.
 +            Checks if the bundle contains any sass/less content, then compiles it to css.
 +            Returns the bundle's flat css.
          """
 -        sass = [asset for asset in self.stylesheets if isinstance(asset, SassAsset)]
 -        if not sass:
 -            return
 -        source = '\n'.join([asset.get_source() for asset in sass])
 -
 -        # move up all @import rules to the top and exclude file imports
 +        for atype in (SassStylesheetAsset, LessStylesheetAsset):
 +            assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
 +            if assets:
 +                cmd = assets[0].get_command()
 +                source = '\n'.join([asset.get_source() for asset in assets])
 +                compiled = self.compile_css(cmd, source)
 +
 +                fragments = self.rx_css_split.split(compiled)
 +                at_rules = fragments.pop(0)
 +                if at_rules:
 +                    # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
 +                    self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
 +                while fragments:
 +                    asset_id = fragments.pop(0)
 +                    asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
 +                    asset._content = fragments.pop(0)
 +
 +        return '\n'.join(asset.minify() for asset in self.stylesheets)
 +
 +    def compile_css(self, cmd, source):
 +        """Sanitizes @import rules, remove duplicates @import rules, then compile"""
          imports = []
 -        def push(matchobj):
 +        def sanitize(matchobj):
              ref = matchobj.group(2)
 -            line = '@import "%s"' % ref
 +            line = '@import "%s"%s' % (ref, matchobj.group(3))
              if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
                  imports.append(line)
 +                return line
 +            msg = "Local import '%s' is forbidden for security reasons." % ref
 +            _logger.warning(msg)
 +            self.css_errors.append(msg)
              return ''
 -        source = re.sub(self.rx_sass_import, push, source)
 -        imports.append(source)
 -        source = u'\n'.join(imports)
 +        source = re.sub(self.rx_preprocess_imports, sanitize, source)
  
          try:
 -            compiler = Popen(self.cmd_sass, stdin=PIPE, stdout=PIPE, stderr=PIPE)
 +            compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
          except Exception:
 -            msg = "Could not find 'sass' program needed to compile sass/scss files"
 +            msg = "Could not execute command %r" % cmd[0]
              _logger.error(msg)
              self.css_errors.append(msg)
 -            return
 +            return ''
          result = compiler.communicate(input=source.encode('utf-8'))
          if compiler.returncode:
 -            error = self.get_sass_error(''.join(result), source=source)
 +            error = self.get_preprocessor_error(''.join(result), source=source)
              _logger.warning(error)
              self.css_errors.append(error)
 -            return
 +            return ''
          compiled = result[0].strip().decode('utf8')
 -        fragments = self.rx_css_split.split(compiled)[1:]
 -        while fragments:
 -            asset_id = fragments.pop(0)
 -            asset = next(asset for asset in sass if asset.id == asset_id)
 -            asset._content = fragments.pop(0)
 -
 -    def get_sass_error(self, stderr, source=None):
 -        # TODO: try to find out which asset the error belongs to
 +        return compiled
 +
 +    def get_preprocessor_error(self, stderr, source=None):
 +        """Improve and remove sensitive information from sass/less compilator error messages"""
          error = stderr.split('Load paths')[0].replace('  Use --trace for backtrace.', '')
 +        if 'Cannot load compass' in error:
 +            error += "Maybe you should install the compass gem using this extra argument:\n\n" \
 +                     "    $ sudo gem install compass --pre\n"
          error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
          for asset in self.stylesheets:
 -            if isinstance(asset, SassAsset):
 +            if isinstance(asset, PreprocessedCSS):
                  error += '\n    - %s' % (asset.url if asset.url else '<inline sass>')
          return error
  
@@@ -1369,7 -1343,7 +1365,7 @@@ class WebAsset(object)
  
      @property
      def content(self):
 -        if not self._content:
 +        if self._content is None:
              self._content = self.inline or self._fetch_content()
          return self._content
  
@@@ -1435,26 -1409,22 +1431,26 @@@ class StylesheetAsset(WebAsset)
              content = super(StylesheetAsset, self)._fetch_content()
              web_dir = os.path.dirname(self.url)
  
 -            content = self.rx_import.sub(
 -                r"""@import \1%s/""" % (web_dir,),
 -                content,
 -            )
 +            if self.rx_import:
 +                content = self.rx_import.sub(
 +                    r"""@import \1%s/""" % (web_dir,),
 +                    content,
 +                )
  
 -            content = self.rx_url.sub(
 -                r"url(\1%s/" % (web_dir,),
 -                content,
 -            )
 +            if self.rx_url:
 +                content = self.rx_url.sub(
 +                    r"url(\1%s/" % (web_dir,),
 +                    content,
 +                )
 +
 +            if self.rx_charset:
 +                # remove charset declarations, we only support utf-8
 +                content = self.rx_charset.sub('', content)
  
 -            # remove charset declarations, we only support utf-8
 -            content = self.rx_charset.sub('', content)
 +            return content
          except AssetError, e:
              self.bundle.css_errors.append(e.message)
              return ''
 -        return content
  
      def minify(self):
          # remove existing sourcemaps, make no sense after re-mini
          else:
              return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
  
 -class SassAsset(StylesheetAsset):
 +class PreprocessedCSS(StylesheetAsset):
      html_url = '%s.css'
 -    rx_indent = re.compile(r'^( +|\t+)', re.M)
 -    indent = None
 -    reindent = '    '
 +    rx_import = None
  
      def minify(self):
          return self.with_header()
          if self.url:
              ira = self.registry['ir.attachment']
              url = self.html_url % self.url
 -            domain = [('type', '=', 'binary'), ('url', '=', self.url)]
 +            domain = [('type', '=', 'binary'), ('url', '=', url)]
              ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
 +            datas = self.content.encode('utf8').encode('base64')
              if ira_id:
                  # TODO: update only if needed
 -                ira.write(self.cr, openerp.SUPERUSER_ID, [ira_id], {'datas': self.content}, context=self.context)
 +                ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
              else:
                  ira.create(self.cr, openerp.SUPERUSER_ID, dict(
 -                    datas=self.content.encode('utf8').encode('base64'),
 +                    datas=datas,
                      mimetype='text/css',
                      type='binary',
                      name=url,
                      url=url,
                  ), context=self.context)
 -        return super(SassAsset, self).to_html()
 +        return super(PreprocessedCSS, self).to_html()
 +
 +    def get_source(self):
 +        content = self.inline or self._fetch_content()
 +        return "/*! %s */\n%s" % (self.id, content)
 +
 +    def get_command(self):
 +        raise NotImplementedError
 +
 +class SassStylesheetAsset(PreprocessedCSS):
 +    rx_indent = re.compile(r'^( +|\t+)', re.M)
 +    indent = None
 +    reindent = '    '
  
      def get_source(self):
          content = textwrap.dedent(self.inline or self._fetch_content())
  
          def fix_indent(m):
 +            # Indentation normalization
              ind = m.group()
              if self.indent is None:
                  self.indent = ind
              pass
          return "/*! %s */\n%s" % (self.id, content)
  
 +    def get_command(self):
 +        return ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
 +               '-r', 'bootstrap-sass']
 +
 +class LessStylesheetAsset(PreprocessedCSS):
 +    def get_command(self):
 +        webpath = openerp.http.addons_manifest['web']['addons_path']
 +        lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
 +        return ['lessc', '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
 +
  def rjsmin(script):
      """ Minify js with a clever regex.
      Taken from http://opensource.perlig.de/rjsmin
@@@ -37,10 -37,8 +37,10 @@@ from lxml import etre
  import openerp
  from openerp import tools, api
  from openerp.http import request
 +from openerp.modules.module import get_resource_path, get_resource_from_path
  from openerp.osv import fields, osv, orm
 -from openerp.tools import graph, SKIPPED_ELEMENT_TYPES, SKIPPED_ELEMENTS
 +from openerp.tools import config, graph, SKIPPED_ELEMENT_TYPES, SKIPPED_ELEMENTS
 +from openerp.tools.convert import _fix_multiple_roots
  from openerp.tools.parse_version import parse_version
  from openerp.tools.safe_eval import safe_eval as eval
  from openerp.tools.view_validation import valid_view
@@@ -107,31 -105,6 +107,31 @@@ def _hasclass(context, *cls)
  
      return node_classes.issuperset(cls)
  
 +def get_view_arch_from_file(filename, xmlid):
 +    doc = etree.parse(filename)
 +    node = None
 +    for n in doc.xpath('//*[@id="%s"] | //*[@id="%s"]' % (xmlid, xmlid.split('.')[1])):
 +        if n.tag in ('template', 'record'):
 +            node = n
 +            break
 +    if node is not None:
 +        if node.tag == 'record':
 +            field = node.find('field[@name="arch"]')
 +            _fix_multiple_roots(field)
 +            inner = ''.join([etree.tostring(child) for child in field.iterchildren()])
 +            return field.text + inner
 +        elif node.tag == 'template':
 +            # The following dom operations has been copied from convert.py's _tag_template()
 +            if not node.get('inherit_id'):
 +                node.set('t-name', xmlid)
 +                node.tag = 't'
 +            else:
 +                node.tag = 'data'
 +            node.attrib.pop('id', None)
 +            return etree.tostring(node)
 +    _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'" % (filename, xmlid))
 +    return None
 +
  xpath_utils = etree.FunctionNamespace(None)
  xpath_utils['hasclass'] = _hasclass
  
@@@ -150,52 -123,21 +150,52 @@@ class view(osv.osv)
          data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
          return map(itemgetter('res_id'), data_ids)
  
 +    def _arch_get(self, cr, uid, ids, name, arg, context=None):
 +        result = {}
 +        for view in self.browse(cr, uid, ids, context=context):
 +            arch_fs = None
 +            if config['dev_mode'] and view.arch_fs and view.xml_id:
 +                # It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
 +                fullpath = get_resource_path(*view.arch_fs.split('/'))
 +                arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
 +            result[view.id] = arch_fs or view.arch_db
 +        return result
 +
 +    def _arch_set(self, cr, uid, ids, field_name, field_value, args, context=None):
 +        if not isinstance(ids, list):
 +            ids = [ids]
 +        if field_value:
 +            for view in self.browse(cr, uid, ids, context=context):
 +                data = dict(arch_db=field_value)
 +                key = 'install_mode_data'
 +                if context and key in context:
 +                    imd = context[key]
 +                    if self._model._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
 +                        # we store the relative path to the resource instead of the absolute path
 +                        data['arch_fs'] = '/'.join(get_resource_from_path(imd['xml_file'])[0:2])
 +                self.write(cr, uid, ids, data, context=context)
 +
 +        return True
 +
      _columns = {
          'name': fields.char('View Name', required=True),
          'model': fields.char('Object', select=True),
 +        'key': fields.char(string='Key'),
          'priority': fields.integer('Sequence', required=True),
          'type': fields.selection([
              ('tree','Tree'),
              ('form','Form'),
              ('graph', 'Graph'),
 +            ('pivot', 'Pivot'),
              ('calendar', 'Calendar'),
              ('diagram','Diagram'),
              ('gantt', 'Gantt'),
              ('kanban', 'Kanban'),
              ('search','Search'),
              ('qweb', 'QWeb')], string='View Type'),
 -        'arch': fields.text('View Architecture', required=True),
 +        'arch': fields.function(_arch_get, fnct_inv=_arch_set, string='View Architecture', type="text", nodrop=True),
 +        'arch_db': fields.text('Arch Blob'),
 +        'arch_fs': fields.char('Arch Filename'),
          'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
          'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
          'field_parent': fields.char('Child Field'),
          if context is None:
              context = {}
  
 +        # If view is modified we remove the arch_fs information thus activating the arch_db
 +        # version. An `init` of the view will restore the arch_fs for the --dev mode
 +        if 'arch' in vals and 'install_mode_data' not in context:
 +            vals['arch_fs'] = False
 +
          # drop the corresponding view customizations (used for dashboards for example), otherwise
          # not all users would see the updated views
          custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
                  if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
                      node.set(action, 'false')
          if node.tag in ('kanban'):
-             group_by_field = node.get('default_group_by')
-             if group_by_field and Model._all_columns.get(group_by_field):
-                 group_by_column = Model._all_columns[group_by_field].column
-                 if group_by_column._type == 'many2one':
-                     group_by_model = Model.pool.get(group_by_column._obj)
+             group_by_name = node.get('default_group_by')
+             if group_by_name in Model._fields:
+                 group_by_field = Model._fields[group_by_name]
+                 if group_by_field.type == 'many2one':
+                     group_by_model = Model.pool[group_by_field.comodel_name]
                      for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
                          if not node.get(action) and not group_by_model.check_access_rights(cr, user, operation, raise_exception=False):
                              node.set(action, 'false')
      #------------------------------------------------------
      # QWeb template views
      #------------------------------------------------------
 -    @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
 -    def read_template(self, cr, uid, xml_id, context=None):
 -        if isinstance(xml_id, (int, long)):
 -            view_id = xml_id
 -        else:
 -            if '.' not in xml_id:
 -                raise ValueError('Invalid template id: %r' % (xml_id,))
 -            view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
 -
 +    _read_template_cache = dict(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
 +    if config['dev_mode']:
 +        _read_template_cache['size'] = 0
 +    @tools.ormcache_context(**_read_template_cache)
 +    def _read_template(self, cr, uid, view_id, context=None):
          arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
          arch_tree = etree.fromstring(arch)
  
          arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
          return arch
  
 +    def read_template(self, cr, uid, xml_id, context=None):
 +        if isinstance(xml_id, (int, long)):
 +            view_id = xml_id
 +        else:
 +            if '.' not in xml_id:
 +                raise ValueError('Invalid template id: %r' % (xml_id,))
 +            view_id = self.get_view_id(cr, uid, xml_id, context=context)
 +        return self._read_template(cr, uid, view_id, context=context)
 +
 +    @tools.ormcache(skiparg=3)
 +    def get_view_id(self, cr, uid, xml_id, context=None):
 +        return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
 +
      def clear_cache(self):
 -        self.read_template.clear_cache(self)
 +        self._read_template.clear_cache(self)
  
      def _contains_branded(self, node):
          return node.tag == 't'\
@@@ -235,7 -235,7 +235,7 @@@ class res_partner(osv.Model, format_add
          'parent_id': fields.many2one('res.partner', 'Related Company', select=True),
          'parent_name': fields.related('parent_id', 'name', type='char', readonly=True, string='Parent name'),
          'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
 -        'ref': fields.char('Contact Reference', select=1),
 +        'ref': fields.char('Internal Reference', select=1),
          'lang': fields.selection(_lang_get, 'Language',
              help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."),
          'tz': fields.selection(_tz_get,  'Timezone', size=64,
          'credit_limit': fields.float(string='Credit Limit'),
          'ean13': fields.char('EAN13', size=13),
          'active': fields.boolean('Active'),
 -        'customer': fields.boolean('Customer', help="Check this box if this contact is a customer."),
 -        'supplier': fields.boolean('Supplier', help="Check this box if this contact is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."),
 +        'customer': fields.boolean('Is a Customer', help="Check this box if this contact is a customer."),
 +        'supplier': fields.boolean('Is a Supplier', help="Check this box if this contact is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."),
          'employee': fields.boolean('Employee', help="Check this box if this contact is an Employee."),
          'function': fields.char('Job Position'),
          'type': fields.selection([('default', 'Default'), ('invoice', 'Invoice'),
              help="Small-sized image of this contact. It is automatically "\
                   "resized as a 64x64px image, with aspect ratio preserved. "\
                   "Use this field anywhere a small image is required."),
 -        'has_image': fields.function(_has_image, type="boolean"),
 +        'has_image': fields.function(_has_image, string="Has image", type="boolean"),
          'company_id': fields.many2one('res.company', 'Company', select=1),
          'color': fields.integer('Color Index'),
          'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
      def _update_fields_values(self, cr, uid, partner, fields, context=None):
          """ Returns dict of write() values for synchronizing ``fields`` """
          values = {}
-         for field in fields:
-             column = self._all_columns[field].column
-             if column._type == 'one2many':
+         for fname in fields:
+             field = self._fields[fname]
+             if field.type == 'one2many':
                  raise AssertionError('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`')
-             if column._type == 'many2one':
-                 values[field] = partner[field].id if partner[field] else False
-             elif column._type == 'many2many':
-                 values[field] = [(6,0,[r.id for r in partner[field] or []])]
+             if field.type == 'many2one':
+                 values[fname] = partner[fname].id if partner[fname] else False
+             elif field.type == 'many2many':
+                 values[fname] = [(6,0,[r.id for r in partner[fname] or []])]
              else:
-                 values[field] = partner[field]
+                 values[fname] = partner[fname]
          return values
  
      def _address_fields(self, cr, uid, context=None):
@@@ -163,7 -163,7 +163,7 @@@ class res_users(osv.osv)
  
      _columns = {
          'id': fields.integer('ID'),
 -        'login_date': fields.date('Latest connection', select=1, copy=False),
 +        'login_date': fields.datetime('Latest connection', select=1, copy=False),
          'partner_id': fields.many2one('res.partner', required=True,
              string='Related Partner', ondelete='restrict',
              help='Partner-related data of the user', auto_join=True),
      def context_get(self, cr, uid, context=None):
          user = self.browse(cr, SUPERUSER_ID, uid, context)
          result = {}
-         for k in self._all_columns.keys():
+         for k in self._fields:
              if k.startswith('context_'):
                  context_key = k[8:]
              elif k in ['lang', 'tz']:
diff --combined openerp/models.py
@@@ -334,6 -334,7 +334,7 @@@ class BaseModel(object)
      # This is similar to _inherit_fields but:
      # 1. includes self fields,
      # 2. uses column_info instead of a triple.
+     # Warning: _all_columns is deprecated, use _fields instead
      _all_columns = {}
  
      _table = None
          # basic setup of field
          field.set_class_name(cls, name)
  
-         if field.store:
+         if field.store or field.column:
              cls._columns[name] = field.to_column()
          else:
              # remove potential column that may be overridden by field
          # check defaults
          for k in cls._defaults:
              assert k in cls._fields, \
 -                "Model %s has a default for nonexiting field %s" % (cls._name, k)
 +                "Model %s has a default for non-existing field %s" % (cls._name, k)
  
          # restart columns
          for column in cls._columns.itervalues():
          * "id" is the External ID for the record
          * ".id" is the Database ID for the record
          """
-         columns = dict((k, v.column) for k, v in self._all_columns.iteritems())
-         # Fake columns to avoid special cases in extractor
-         columns[None] = fields.char('rec_name')
-         columns['id'] = fields.char('External ID')
-         columns['.id'] = fields.integer('Database ID')
+         from openerp.fields import Char, Integer
+         fields = dict(self._fields)
+         # Fake fields to avoid special cases in extractor
+         fields[None] = Char('rec_name')
+         fields['id'] = Char('External ID')
+         fields['.id'] = Integer('Database ID')
  
          # m2o fields can't be on multiple lines so exclude them from the
          # is_relational field rows filter, but special-case it later on to
          # be handled with relational fields (as it can have subfields)
-         is_relational = lambda field: columns[field]._type in ('one2many', 'many2many', 'many2one')
+         is_relational = lambda field: fields[field].relational
          get_o2m_values = itemgetter_tuple(
              [index for index, field in enumerate(fields_)
-                   if columns[field[0]]._type == 'one2many'])
+                    if fields[field[0]].type == 'one2many'])
          get_nono2m_values = itemgetter_tuple(
              [index for index, field in enumerate(fields_)
-                   if columns[field[0]]._type != 'one2many'])
+                    if fields[field[0]].type != 'one2many'])
          # Checks if the provided row has any non-empty non-relational field
          def only_o2m_values(row, f=get_nono2m_values, g=get_o2m_values):
              return any(g(row)) and not any(f(row))
              for relfield in set(
                      field[0] for field in fields_
                               if is_relational(field[0])):
-                 column = columns[relfield]
                  # FIXME: how to not use _obj without relying on fields_get?
-                 Model = self.pool[column._obj]
+                 Model = self.pool[fields[relfield].comodel_name]
  
                  # get only cells for this sub-field, should be strictly
-                 # non-empty, field path [None] is for name_get column
+                 # non-empty, field path [None] is for name_get field
                  indices, subfields = zip(*((index, field[1:] or [None])
                                             for index, field in enumerate(fields_)
                                             if field[0] == relfield))
          """
          if context is None: context = {}
          Converter = self.pool['ir.fields.converter']
-         columns = dict((k, v.column) for k, v in self._all_columns.iteritems())
          Translation = self.pool['ir.translation']
+         fields = dict(self._fields)
          field_names = dict(
              (f, (Translation._get_source(cr, uid, self._name + ',' + f, 'field',
                                           context.get('lang'))
-                  or column.string))
-             for f, column in columns.iteritems())
+                  or field.string))
+             for f, field in fields.iteritems())
  
          convert = Converter.for_model(cr, uid, self, context=context)
  
              order_field = order_split[0]
              if order_field in groupby_fields:
  
-                 if self._all_columns[order_field.split(':')[0]].column._type == 'many2one':
+                 if self._fields[order_field.split(':')[0]].type == 'many2one':
                      order_clause = self._generate_order_by(order_part, query).replace('ORDER BY ', '')
                      if order_clause:
                          orderby_terms.append(order_clause)
              field name, type, time informations, qualified name, ...
          """
          split = gb.split(':')
-         field_type = self._all_columns[split[0]].column._type
+         field_type = self._fields[split[0]].type
          gb_function = split[1] if len(split) == 2 else None
          temporal = field_type in ('date', 'datetime')
          tz_convert = field_type == 'datetime' and context.get('tz') in pytz.all_timezones
              assert gb in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
              groupby_def = self._columns.get(gb) or (self._inherit_fields.get(gb) and self._inherit_fields.get(gb)[2])
              assert groupby_def and groupby_def._classic_write, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True"
-             if not (gb in self._all_columns):
+             if not (gb in self._fields):
                  # Don't allow arbitrary values, as this would be a SQL injection vector!
                  raise except_orm(_('Invalid group_by'),
                                   _('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(gb,))
              f for f in fields
              if f not in ('id', 'sequence')
              if f not in groupby_fields
-             if f in self._all_columns
-             if self._all_columns[f].column._type in ('integer', 'float')
-             if getattr(self._all_columns[f].column, '_classic_write')]
+             if f in self._fields
+             if self._fields[f].type in ('integer', 'float')
+             if getattr(self._fields[f].base_field.column, '_classic_write')
+         ]
  
-         field_formatter = lambda f: (self._all_columns[f].column.group_operator or 'sum', self._inherits_join_calc(f, query), f)
+         field_formatter = lambda f: (self._fields[f].group_operator or 'sum', self._inherits_join_calc(f, query), f)
          select_terms = ["%s(%s) AS %s" % field_formatter(f) for f in aggregated_fields]
  
          for gb in annotated_groupbys:
                  _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
                                self._table, column['attname'])
  
 -    def _save_constraint(self, cr, constraint_name, type):
 +    def _save_constraint(self, cr, constraint_name, type, definition):
          """
          Record the creation of a constraint for this model, to make it possible
          to delete it later when the module is uninstalled. Type can be either
              return
          assert type in ('f', 'u')
          cr.execute("""
 -            SELECT 1 FROM ir_model_constraint, ir_module_module
 +            SELECT type, definition FROM ir_model_constraint, ir_module_module
              WHERE ir_model_constraint.module=ir_module_module.id
                  AND ir_model_constraint.name=%s
                  AND ir_module_module.name=%s
              """, (constraint_name, self._module))
 -        if not cr.rowcount:
 +        constraints = cr.dictfetchone()
 +        if not constraints:
              cr.execute("""
                  INSERT INTO ir_model_constraint
 -                    (name, date_init, date_update, module, model, type)
 +                    (name, date_init, date_update, module, model, type, definition)
                  VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
                      (SELECT id FROM ir_module_module WHERE name=%s),
 -                    (SELECT id FROM ir_model WHERE model=%s), %s)""",
 -                    (constraint_name, self._module, self._name, type))
 +                    (SELECT id FROM ir_model WHERE model=%s), %s, %s)""",
 +                    (constraint_name, self._module, self._name, type, definition))
 +        elif constraints['type'] != type or (definition and constraints['definition'] != definition):
 +            cr.execute("""
 +                UPDATE ir_model_constraint
 +                SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s
 +                WHERE name=%s AND module = (SELECT id FROM ir_module_module WHERE name=%s)""",
 +                    (type, definition, constraint_name, self._module))
  
      def _save_relation_table(self, cr, relation_table):
          """
          """ Create the foreign keys recorded by _auto_init. """
          for t, k, r, d in self._foreign_keys:
              cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d))
 -            self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f')
 +            self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f', False)
          cr.commit()
          del self._foreign_keys
  
          for (key, con, _) in self._sql_constraints:
              conname = '%s_%s' % (self._table, key)
  
 -            self._save_constraint(cr, conname, 'u')
 -            cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,))
 -            existing_constraints = cr.dictfetchall()
 +            # using 1 to get result if no imc but one pgc
 +            cr.execute("""SELECT definition, 1
 +                          FROM ir_model_constraint imc
 +                          RIGHT JOIN pg_constraint pgc
 +                          ON (pgc.conname = imc.name)
 +                          WHERE pgc.conname=%s
 +                          """, (conname, ))
 +            existing_constraints = cr.dictfetchone()
              sql_actions = {
                  'drop': {
                      'execute': False,
                  # constraint does not exists:
                  sql_actions['add']['execute'] = True
                  sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
 -            elif unify_cons_text(con) not in [unify_cons_text(item['condef']) for item in existing_constraints]:
 +            elif unify_cons_text(con) != existing_constraints['definition']:
                  # constraint exists but its definition has changed:
                  sql_actions['drop']['execute'] = True
 -                sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints[0]['condef'].lower(), )
 +                sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints['definition'] or '', )
                  sql_actions['add']['execute'] = True
                  sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
  
              # we need to add the constraint:
 +            self._save_constraint(cr, conname, 'u', unify_cons_text(con))
              sql_actions = [item for item in sql_actions.values()]
              sql_actions.sort(key=lambda x: x['order'])
              for sql_action in [action for action in sql_actions if action['execute']]:
  
          # update columns (fields may have changed), and column_infos
          for name, field in self._fields.iteritems():
-             if field.store:
+             if field.column:
                  self._columns[name] = field.to_column()
          self._inherits_reload()
  
          if isinstance(ids, (int, long)):
              ids = [ids]
  
-         result_store = self._store_get_values(cr, uid, ids, self._all_columns.keys(), context)
+         result_store = self._store_get_values(cr, uid, ids, self._fields.keys(), context)
  
          # for recomputing new-style fields
          recs = self.browse(cr, uid, ids, context)
          for key, val in vals.iteritems():
              field = self._fields.get(key)
              if field:
-                 if field.store or field.inherited:
+                 if field.column or field.inherited:
                      old_vals[key] = val
                  if field.inverse and not field.inherited:
                      new_vals[key] = val
                  cr.execute(query, (tuple(ids),))
              parents_changed = map(operator.itemgetter(0), cr.fetchall())
  
-         upd0 = []
-         upd1 = []
+         updates = []            # list of (column, expr) or (column, pattern, value)
          upd_todo = []
          updend = []
          direct = []
          totranslate = context.get('lang', False) and (context['lang'] != 'en_US')
          for field in vals:
-             field_column = self._all_columns.get(field) and self._all_columns.get(field).column
-             if field_column and field_column.deprecated:
-                 _logger.warning('Field %s.%s is deprecated: %s', self._name, field, field_column.deprecated)
+             ffield = self._fields.get(field)
+             if ffield and ffield.deprecated:
+                 _logger.warning('Field %s.%s is deprecated: %s', self._name, field, ffield.deprecated)
              if field in self._columns:
-                 if self._columns[field]._classic_write and not (hasattr(self._columns[field], '_fnct_inv')):
-                     if (not totranslate) or not self._columns[field].translate:
-                         upd0.append('"'+field+'"='+self._columns[field]._symbol_set[0])
-                         upd1.append(self._columns[field]._symbol_set[1](vals[field]))
+                 column = self._columns[field]
+                 if hasattr(column, 'selection') and vals[field]:
+                     self._check_selection_field_value(cr, user, field, vals[field], context=context)
+                 if column._classic_write and not hasattr(column, '_fnct_inv'):
+                     if (not totranslate) or not column.translate:
+                         updates.append((field, '%s', column._symbol_set[1](vals[field])))
                      direct.append(field)
                  else:
                      upd_todo.append(field)
              else:
                  updend.append(field)
-             if field in self._columns \
-                     and hasattr(self._columns[field], 'selection') \
-                     and vals[field]:
-                 self._check_selection_field_value(cr, user, field, vals[field], context=context)
  
          if self._log_access:
-             upd0.append('write_uid=%s')
-             upd0.append("write_date=(now() at time zone 'UTC')")
-             upd1.append(user)
+             updates.append(('write_uid', '%s', user))
+             updates.append(('write_date', "(now() at time zone 'UTC')"))
              direct.append('write_uid')
              direct.append('write_date')
  
-         if len(upd0):
+         if updates:
              self.check_access_rule(cr, user, ids, 'write', context=context)
+             query = 'UPDATE "%s" SET %s WHERE id IN %%s' % (
+                 self._table, ','.join('"%s"=%s' % u[:2] for u in updates),
+             )
+             params = tuple(u[2] for u in updates if len(u) > 2)
              for sub_ids in cr.split_for_in_conditions(ids):
-                 cr.execute('update ' + self._table + ' set ' + ','.join(upd0) + ' ' \
-                            'where id IN %s', upd1 + [sub_ids])
+                 cr.execute(query, params + (sub_ids,))
                  if cr.rowcount != len(sub_ids):
                      raise MissingError(_('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
  
          for key, val in vals.iteritems():
              field = self._fields.get(key)
              if field:
-                 if field.store or field.inherited:
+                 if field.column or field.inherited:
                      old_vals[key] = val
                  if field.inverse and not field.inherited:
                      new_vals[key] = val
                          for f in value.keys():
                              if f in field_dict[id]:
                                  value.pop(f)
-                     upd0 = []
-                     upd1 = []
+                     updates = []        # list of (column, pattern, value)
                      for v in value:
                          if v not in val:
                              continue
-                         if self._columns[v]._type == 'many2one':
+                         column = self._columns[v]
+                         if column._type == 'many2one':
                              try:
                                  value[v] = value[v][0]
                              except:
                                  pass
-                         upd0.append('"'+v+'"='+self._columns[v]._symbol_set[0])
-                         upd1.append(self._columns[v]._symbol_set[1](value[v]))
-                     upd1.append(id)
-                     if upd0 and upd1:
-                         cr.execute('update "' + self._table + '" set ' + \
-                             ','.join(upd0) + ' where id = %s', upd1)
+                         updates.append((v, '%s', column._symbol_set[1](value[v])))
+                     if updates:
+                         query = 'UPDATE "%s" SET %s WHERE id = %%s' % (
+                             self._table, ','.join('"%s"=%s' % u[:2] for u in updates),
+                         )
+                         params = tuple(u[2] for u in updates)
+                         cr.execute(query, params + (id,))
  
              else:
                  for f in val:
+                     column = self._columns[f]
                      # use admin user for accessing objects having rules defined on store fields
-                     result = self._columns[f].get(cr, self, ids, f, SUPERUSER_ID, context=context)
+                     result = column.get(cr, self, ids, f, SUPERUSER_ID, context=context)
                      for r in result.keys():
                          if field_flag:
                              if r in field_dict.keys():
                                  if f in field_dict[r]:
                                      result.pop(r)
                      for id, value in result.items():
-                         if self._columns[f]._type == 'many2one':
+                         if column._type == 'many2one':
                              try:
                                  value = value[0]
                              except:
                                  pass
-                         cr.execute('update "' + self._table + '" set ' + \
-                             '"'+f+'"='+self._columns[f]._symbol_set[0] + ' where id = %s', (self._columns[f]._symbol_set[1](value), id))
+                         query = 'UPDATE "%s" SET "%s"=%%s WHERE id = %%s' % (
+                             self._table, f,
+                         )
+                         cr.execute(query, (column._symbol_set[1](value), id))
  
          # invalidate and mark new-style fields to recompute
          self.browse(cr, uid, ids, context).modified(fields)
          domain = domain[:]
          # if the object has a field named 'active', filter out all inactive
          # records unless they were explicitely asked for
-         if 'active' in self._all_columns and (active_test and context.get('active_test', True)):
+         if 'active' in self._fields and active_test and context.get('active_test', True):
              if domain:
                  # the item[0] trick below works for domain items and '&'/'|'/'!'
                  # operators too
  
          # build a black list of fields that should not be copied
          blacklist = set(MAGIC_COLUMNS + ['parent_left', 'parent_right'])
+         whitelist = set(name for name, field in self._fields.iteritems() if not field.inherited)
          def blacklist_given_fields(obj):
              # blacklist the fields that are given by inheritance
              for other, field_to_other in obj._inherits.items():
                  if field_to_other in default:
                      # all the fields of 'other' are given by the record: default[field_to_other],
                      # except the ones redefined in self
-                     blacklist.update(set(self.pool[other]._all_columns) - set(self._columns))
+                     blacklist.update(set(self.pool[other]._fields) - whitelist)
                  else:
                      blacklist_given_fields(self.pool[other])
              # blacklist deprecated fields
-             for name, field in obj._columns.items():
+             for name, field in obj._fields.iteritems():
                  if field.deprecated:
                      blacklist.add(name)
  
          blacklist_given_fields(self)
  
  
-         fields_to_copy = dict((f,fi) for f, fi in self._all_columns.iteritems()
-                                      if fi.column.copy
+         fields_to_copy = dict((f,fi) for f, fi in self._fields.iteritems()
+                                      if fi.copy
                                       if f not in default
                                       if f not in blacklist)
  
              raise IndexError( _("Record #%d of %s not found, cannot copy!") %( id, self._name))
  
          res = dict(default)
-         for f, colinfo in fields_to_copy.iteritems():
-             field = colinfo.column
-             if field._type == 'many2one':
+         for f, field in fields_to_copy.iteritems():
+             if field.type == 'many2one':
                  res[f] = data[f] and data[f][0]
-             elif field._type == 'one2many':
-                 other = self.pool[field._obj]
+             elif field.type == 'one2many':
+                 other = self.pool[field.comodel_name]
                  # duplicate following the order of the ids because we'll rely on
                  # it later for copying translations in copy_translation()!
                  lines = [other.copy_data(cr, uid, line_id, context=context) for line_id in sorted(data[f])]
                  # the lines are duplicated using the wrong (old) parent, but then
                  # are reassigned to the correct one thanks to the (0, 0, ...)
                  res[f] = [(0, 0, line) for line in lines if line]
-             elif field._type == 'many2many':
+             elif field.type == 'many2many':
                  res[f] = [(6, 0, data[f])]
              else:
                  res[f] = data[f]
          seen_map[self._name].append(old_id)
  
          trans_obj = self.pool.get('ir.translation')
-         # TODO it seems fields_get can be replaced by _all_columns (no need for translation)
-         fields = self.fields_get(cr, uid, context=context)
  
-         for field_name, field_def in fields.items():
+         for field_name, field in self._fields.iteritems():
              # removing the lang to compare untranslated values
              context_wo_lang = dict(context, lang=None)
              old_record, new_record = self.browse(cr, uid, [old_id, new_id], context=context_wo_lang)
              # we must recursively copy the translations for o2o and o2m
-             if field_def['type'] == 'one2many':
-                 target_obj = self.pool[field_def['relation']]
+             if field.type == 'one2many':
+                 target_obj = self.pool[field.comodel_name]
                  # here we rely on the order of the ids to match the translations
                  # as foreseen in copy_data()
                  old_children = sorted(r.id for r in old_record[field_name])
                  for (old_child, new_child) in zip(old_children, new_children):
                      target_obj.copy_translations(cr, uid, old_child, new_child, context=context)
              # and for translatable fields we keep them for copy
-             elif field_def.get('translate'):
+             elif getattr(field, 'translate', False):
                  if field_name in self._columns:
                      trans_name = self._name + "," + field_name
                      target_id = new_id
          :return: **True** if the operation can proceed safely, or **False** if an infinite loop is detected.
          """
  
-         field = self._all_columns.get(field_name)
-         field = field.column if field else None
-         if not field or field._type != 'many2many' or field._obj != self._name:
+         field = self._fields.get(field_name)
+         if not (field and field.type == 'many2many' and
+                 field.comodel_name == self._name and field.store):
              # field must be a many2many on itself
              raise ValueError('invalid field_name: %r' % (field_name,))
  
-         query = 'SELECT distinct "%s" FROM "%s" WHERE "%s" IN %%s' % (field._id2, field._rel, field._id1)
+         query = 'SELECT distinct "%s" FROM "%s" WHERE "%s" IN %%s' % \
+                     (field.column2, field.relation, field.column1)
          ids_parent = ids[:]
          while ids_parent:
              ids_parent2 = []
                  result, record_ids = [], list(command[2])
  
          # read the records and apply the updates
-         other_model = self.pool[self._all_columns[field_name].column._obj]
+         other_model = self.pool[self._fields[field_name].comodel_name]
          for record in other_model.read(cr, uid, record_ids, fields=fields, context=context):
              record.update(updates.get(record['id'], {}))
              result.append(record)
  
      def _mapped_func(self, func):
          """ Apply function `func` on all records in `self`, and return the
-             result as a list or a recordset (if `func` return recordsets).
+             result as a list or a recordset (if `func` returns recordsets).
          """
-         vals = [func(rec) for rec in self]
-         val0 = vals[0] if vals else func(self)
-         if isinstance(val0, BaseModel):
-             return reduce(operator.or_, vals, val0)
-         return vals
+         if self:
+             vals = [func(rec) for rec in self]
+             return reduce(operator.or_, vals) if isinstance(vals[0], BaseModel) else vals
+         else:
+             vals = func(self)
+             return vals if isinstance(vals, BaseModel) else []
  
      def mapped(self, func):
          """ Apply `func` on all records in `self`, and return the result as a
              func = lambda rec: filter(None, rec.mapped(name))
          return self.browse([rec.id for rec in self if func(rec)])
  
-     def sorted(self, key=None):
-         """ Return the recordset `self` ordered by `key` """
+     def sorted(self, key=None, reverse=False):
+         """ Return the recordset `self` ordered by `key`.
+             :param key: either a function of one argument that returns a
+                 comparison key for each record, or ``None``, in which case
+                 records are ordered according the default model's order
+             :param reverse: if ``True``, return the result in reverse order
+         """
          if key is None:
-             return self.search([('id', 'in', self.ids)])
+             recs = self.search([('id', 'in', self.ids)])
+             return self.browse(reversed(recs._ids)) if reverse else recs
          else:
-             return self.browse(map(int, sorted(self, key=key)))
+             return self.browse(map(int, sorted(self, key=key, reverse=reverse)))
  
      def update(self, values):
          """ Update record `self[0]` with `values`. """
@@@ -41,13 -41,6 +41,13 @@@ from openerp.tools.misc import stripped
  
  _logger = logging.getLogger(__name__)
  
 +try:
 +    import watchdog
 +    from watchdog.observers import Observer
 +    from watchdog.events import FileCreatedEvent, FileModifiedEvent
 +except ImportError:
 +    watchdog = None
 +
  SLEEP_INTERVAL = 60     # 1 min
  
  #----------------------------------------------------------
@@@ -112,37 -105,90 +112,37 @@@ class ThreadedWSGIServerReloadable(Logg
              super(ThreadedWSGIServerReloadable, self).server_activate()
  
  #----------------------------------------------------------
 -# AutoReload watcher
 +# FileSystem Watcher for autoreload and cache invalidation
  #----------------------------------------------------------
 -
 -class AutoReload(object):
 -    def __init__(self, server):
 -        self.server = server
 -        self.files = {}
 -        self.modules = {}
 -        import pyinotify
 -        class EventHandler(pyinotify.ProcessEvent):
 -            def __init__(self, autoreload):
 -                self.autoreload = autoreload
 -
 -            def process_IN_CREATE(self, event):
 -                _logger.debug('File created: %s', event.pathname)
 -                self.autoreload.files[event.pathname] = 1
 -
 -            def process_IN_MODIFY(self, event):
 -                _logger.debug('File modified: %s', event.pathname)
 -                self.autoreload.files[event.pathname] = 1
 -
 -        self.wm = pyinotify.WatchManager()
 -        self.handler = EventHandler(self)
 -        self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0)
 -        mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE  # IN_MOVED_FROM, IN_MOVED_TO ?
 +class FSWatcher(object):
 +    def __init__(self):
 +        self.observer = Observer()
          for path in openerp.modules.module.ad_paths:
              _logger.info('Watching addons folder %s', path)
 -            self.wm.add_watch(path, mask, rec=True)
 -
 -    def process_data(self, files):
 -        xml_files = [i for i in files if i.endswith('.xml')]
 -        for i in xml_files:
 -            for path in openerp.modules.module.ad_paths:
 -                if i.startswith(path):
 -                    # find out wich addons path the file belongs to
 -                    # and extract it's module name
 -                    right = i[len(path) + 1:].split('/')
 -                    if len(right) < 2:
 -                        continue
 -                    module = right[0]
 -                    self.modules[module] = 1
 -        if self.modules:
 -            _logger.info('autoreload: xml change detected, autoreload activated')
 -            restart()
 -
 -    def process_python(self, files):
 -        # process python changes
 -        py_files = [i for i in files if i.endswith('.py')]
 -        py_errors = []
 -        # TODO keep python errors until they are ok
 -        if py_files:
 -            for i in py_files:
 -                try:
 -                    source = open(i, 'rb').read() + '\n'
 -                    compile(source, i, 'exec')
 -                except SyntaxError:
 -                    py_errors.append(i)
 -            if py_errors:
 -                _logger.info('autoreload: python code change detected, errors found')
 -                for i in py_errors:
 -                    _logger.info('autoreload: SyntaxError %s', i)
 -            else:
 -                _logger.info('autoreload: python code updated, autoreload activated')
 -                restart()
 -
 -    def check_thread(self):
 -        # Check if some files have been touched in the addons path.
 -        # If true, check if the touched file belongs to an installed module
 -        # in any of the database used in the registry manager.
 -        while 1:
 -            while self.notifier.check_events(1000):
 -                self.notifier.read_events()
 -                self.notifier.process_events()
 -            l = self.files.keys()
 -            self.files.clear()
 -            self.process_data(l)
 -            self.process_python(l)
 +            self.observer.schedule(self, path, recursive=True)
 +
 +    def dispatch(self, event):
 +        if isinstance(event, (FileCreatedEvent, FileModifiedEvent)):
 +            if not event.is_directory:
 +                path = event.src_path
 +                if path.endswith('.py'):
 +                    try:
 +                        source = open(path, 'rb').read() + '\n'
 +                        compile(source, path, 'exec')
 +                    except SyntaxError:
 +                        _logger.error('autoreload: python code change detected, SyntaxError in %s', path)
 +                    else:
 +                        _logger.info('autoreload: python code updated, autoreload activated')
 +                        restart()
  
 -    def run(self):
 -        t = threading.Thread(target=self.check_thread)
 -        t.setDaemon(True)
 -        t.start()
 +    def start(self):
 +        self.observer.start()
          _logger.info('AutoReload watcher running')
  
 +    def stop(self):
 +        self.observer.stop()
 +        self.observer.join()
 +
  #----------------------------------------------------------
  # Servers: Threaded, Gevented and Prefork
  #----------------------------------------------------------
@@@ -203,8 -249,8 +203,8 @@@ class ThreadedServer(CommonServer)
              time.sleep(SLEEP_INTERVAL + number)     # Steve Reich timing style
              registries = openerp.modules.registry.RegistryManager.registries
              _logger.debug('cron%d polling for jobs', number)
-             for db_name, registry in registries.items():
-                 while True and registry.ready:
+             for db_name, registry in registries.iteritems():
+                 while registry.ready:
                      acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
                      if not acquired:
                          break
@@@ -346,7 -392,11 +346,11 @@@ class GeventServer(CommonServer)
          gevent.spawn(self.watch_parent)
          self.httpd = WSGIServer((self.interface, self.port), self.app)
          _logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port)
-         self.httpd.serve_forever()
+         try:
+             self.httpd.serve_forever()
+         except:
+             _logger.exception("Evented Service (longpolling): uncaught error during main loop")
+             raise
  
      def stop(self):
          import gevent
@@@ -428,6 -478,8 +432,8 @@@ class PreforkServer(CommonServer)
          self.long_polling_pid = popen.pid
  
      def worker_pop(self, pid):
+         if pid == self.long_polling_pid:
+             self.long_polling_pid = None
          if pid in self.workers:
              _logger.debug("Worker (%s) unregistered", pid)
              try:
@@@ -808,8 -860,7 +814,8 @@@ def _reexec(updated_modules=None)
          subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True)
      exe = os.path.basename(sys.executable)
      args = stripped_sys_argv()
 -    args += ["-u", ','.join(updated_modules)]
 +    if updated_modules:
 +        args += ["-u", ','.join(updated_modules)]
      if not args or args[0] != exe:
          args.insert(0, exe)
      os.execv(sys.executable, args)
@@@ -881,21 -932,18 +887,21 @@@ def start(preload=None, stop=False)
      else:
          server = ThreadedServer(openerp.service.wsgi_server.application)
  
 -    if config['auto_reload']:
 -        autoreload = AutoReload(server)
 -        autoreload.run()
 +    watcher = None
 +    if config['dev_mode']:
 +        if watchdog:
 +            watcher = FSWatcher()
 +            watcher.start()
 +        else:
 +            _logger.warning("'watchdog' module not installed. Code autoreload feature is disabled")
  
      rc = server.run(preload, stop)
  
      # like the legend of the phoenix, all ends with beginnings
      if getattr(openerp, 'phoenix', False):
 -        modules = []
 -        if config['auto_reload']:
 -            modules = autoreload.modules.keys()
 -        _reexec(modules)
 +        if watcher:
 +            watcher.stop()
 +        _reexec()
  
      return rc if rc else 0
  
diff --combined openerp/tools/convert.py
@@@ -692,17 -692,9 +692,17 @@@ form: module.record_id""" % (xml_id,
          rec_model = rec.get("model").encode('ascii')
          model = self.pool[rec_model]
          rec_id = rec.get("id",'').encode('ascii')
 -        rec_context = rec.get("context", None)
 +        rec_context = rec.get("context", {})
          if rec_context:
              rec_context = unsafe_eval(rec_context)
 +
 +        if self.xml_filename and rec_id:
 +            rec_context['install_mode_data'] = dict(
 +                xml_file=self.xml_filename,
 +                xml_id=rec_id,
 +                model=rec_model,
 +            )
 +
          self._test_xml_id(rec_id)
          # in update mode, the record won't be updated if the data node explicitely
          # opt-out using @noupdate="1". A second check will be performed in
              f_ref = field.get("ref",'').encode('utf-8')
              f_search = field.get("search",'').encode('utf-8')
              f_model = field.get("model",'').encode('utf-8')
-             if not f_model and model._all_columns.get(f_name):
-                 f_model = model._all_columns[f_name].column._obj
+             if not f_model and f_name in model._fields:
+                 f_model = model._fields[f_name].comodel_name
              f_use = field.get("use",'').encode('utf-8') or 'id'
              f_val = False
  
                  # browse the objects searched
                  s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
                  # column definitions of the "local" object
-                 _cols = self.pool[rec_model]._all_columns
+                 _fields = self.pool[rec_model]._fields
                  # if the current field is many2many
-                 if (f_name in _cols) and _cols[f_name].column._type=='many2many':
+                 if (f_name in _fields) and _fields[f_name].type == 'many2many':
                      f_val = [(6, 0, map(lambda x: x[f_use], s))]
                  elif len(s):
                      # otherwise (we are probably in a many2one field),
                      # take the first element of the search
                      f_val = s[0][f_use]
              elif f_ref:
-                 if f_name in model._all_columns \
-                           and model._all_columns[f_name].column._type == 'reference':
+                 if f_name in model._fields and model._fields[f_name].type == 'reference':
                      val = self.model_id_get(cr, f_ref)
                      f_val = val[0] + ',' + str(val[1])
                  else:
                      f_val = self.id_get(cr, f_ref)
              else:
                  f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
-                 if f_name in model._all_columns:
-                     import openerp.osv as osv
-                     if isinstance(model._all_columns[f_name].column, osv.fields.integer):
+                 if f_name in model._fields:
+                     if model._fields[f_name].type == 'integer':
                          f_val = int(f_val)
              res[f_name] = f_val
  
  
          record = etree.Element('record', attrib=record_attrs)
          record.append(Field(name, name='name'))
 +        record.append(Field(full_tpl_id, name='key'))
          record.append(Field("qweb", name='type'))
          record.append(Field(el.get('priority', "16"), name='priority'))
          if 'inherit_id' in el.attrib:
                          raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
          return True
  
 -    def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
 +    def __init__(self, cr, module, idref, mode, report=None, noupdate=False, xml_filename=None):
  
          self.mode = mode
          self.module = module
              report = assertion_report.assertion_report()
          self.assertion_report = report
          self.noupdate = noupdate
 +        self.xml_filename = xml_filename
          self._tags = {
              'record': self._tag_record,
              'delete': self._tag_delete,
@@@ -991,11 -979,7 +989,11 @@@ def convert_xml_import(cr, module, xmlf
  
      if idref is None:
          idref={}
 -    obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
 +    if isinstance(xmlfile, file):
 +        xml_filename = xmlfile.name
 +    else:
 +        xml_filename = xmlfile
 +    obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate, xml_filename=xml_filename)
      obj.parse(doc.getroot(), mode=mode)
      return True
  
diff --combined openerp/tools/misc.py
@@@ -460,7 -460,7 +460,7 @@@ ALL_LANGUAGES = 
          'am_ET': u'Amharic / አምሃርኛ',
          'ar_SY': u'Arabic / الْعَرَبيّة',
          'bg_BG': u'Bulgarian / български език',
-         'bs_BS': u'Bosnian / bosanski jezik',
+         'bs_BA': u'Bosnian / bosanski jezik',
          'ca_ES': u'Catalan / Català',
          'cs_CZ': u'Czech / Čeština',
          'da_DK': u'Danish / Dansk',
@@@ -1161,7 -1161,7 +1161,7 @@@ class CountingStream(object)
  
  def stripped_sys_argv(*strip_args):
      """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
 -    strip_args = sorted(set(strip_args) | set(['-s', '--save', '-d', '--database', '-u', '--update', '-i', '--init']))
 +    strip_args = sorted(set(strip_args) | set(['-s', '--save', '-u', '--update', '-i', '--init']))
      assert all(config.parser.has_option(s) for s in strip_args)
      takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)