diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js
index fd22b2e9ce4..2ec8864cfd9 100644
--- a/addons/web/static/src/js/views/form_view.js
+++ b/addons/web/static/src/js/views/form_view.js
@@ -142,7 +142,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
// Add bounce effect on button 'Edit' when click on readonly page view.
this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
- if(self.get("actual_mode") == "view" && self.$buttons) {
+ if(self.get("actual_mode") == "view" && self.$buttons && !$(e.target).is('[data-toggle]')) {
var $button = self.$buttons.find(".oe_form_button_edit");
$button.openerpBounce();
e.stopPropagation();
diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js
index 880398ca60b..449fe57bd65 100644
--- a/addons/web/static/src/js/views/search_view.js
+++ b/addons/web/static/src/js/views/search_view.js
@@ -694,9 +694,16 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
current_group = [];
}
if (filter.item.tag === 'field') {
- var attrs = filter.item.attrs,
- field = self.fields_view_get.fields[attrs.name],
- Obj = core.search_widgets_registry.get_any([attrs.widget, field.type]);
+ var attrs = filter.item.attrs;
+ var field = self.fields_view_get.fields[attrs.name];
+
+ // M2O combined with selection widget is pointless and broken in search views,
+ // but has been used in the past for unsupported hacks -> ignore it
+ if (field.type === "many2one" && attrs.widget === "selection") {
+ attrs.widget = undefined;
+ }
+
+ var Obj = my.fields.get_any([attrs.widget, field.type]);
if (Obj) {
self.search_fields.push(new (Obj) (filter.item, field, self));
}
diff --git a/addons/web_view_editor/static/src/js/view_editor.js b/addons/web_view_editor/static/src/js/view_editor.js
index d45216ed19a..29d4b642920 100644
--- a/addons/web_view_editor/static/src/js/view_editor.js
+++ b/addons/web_view_editor/static/src/js/view_editor.js
@@ -543,6 +543,8 @@ var ViewEditor = Widget.extend({
var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object.main_object, [])[0].att_list[0];
var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0];
var field_dataset = new data.DataSetSearch(this, this.model, null, null);
+ if(_.isUndefined(parent_tr))
+ return;
parent_tr = self.get_object_by_id(parseInt($(parent_tr).attr('id').replace(/[^0-9]+/g, '')), this.one_object.main_object, [])[0].att_list[0];
_.each([tr, parent_tr],function(element) {
var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false;
@@ -1019,7 +1021,7 @@ var ViewEditor = Widget.extend({
type_widget.set_value(node.value);
self.add_widget.push(type_widget);
});
- table_selector.find("td[id^=]").attr("width","100px");
+ table_selector.find("td[id^='']").attr("width","100px");
self.add_node_dialog.$el.find('#new_field').click(function() {
var model_data = new data.DataSetSearch(self,'ir.model', null, null);
model_data.read_slice([], {domain: [['model','=', self.model]]}).done(function(result) {
diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv
index 47aca893c43..1d2fe44188b 100644
--- a/addons/website_sale/security/ir.model.access.csv
+++ b/addons/website_sale/security/ir.model.access.csv
@@ -17,4 +17,6 @@ access_product_attribute_line_public,product.attribute line public,product.model
access_website_pricelist,website_pricelist,model_website_pricelist,,1,0,0,0
access_website_pricelist_sale_manager,website_pricelist,model_website_pricelist,,base.group_sale_manager,1,1,1,1
access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0
-access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0
\ No newline at end of file
+access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0
+access_product_supplierinfo_user,product.supplierinfo.user,product.model_product_supplierinfo,base.group_public,1,0,0,0
+access_pricelist_partnerinfo_user,pricelist.partnerinfo.user,product.model_pricelist_partnerinfo,base.group_public,1,0,0,0
\ No newline at end of file
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 01e925d0aeb..4c160e0018b 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -323,6 +323,11 @@ $('.oe_website_sale').each(function () {
$parent.find(".js_check_product").attr("disabled", "disabled");
}
});
+
+ $('div.js_product', oe_website_sale).each(function () {
+ $('input.js_product_change', this).first().trigger('change');
+ });
+
$('ul.js_add_cart_variants', oe_website_sale).each(function () {
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
});
diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst
index 9aa83478ba9..cd59db73d5d 100644
--- a/doc/howtos/themes.rst
+++ b/doc/howtos/themes.rst
@@ -9,7 +9,7 @@ Theme Tutorial
Odoo celebrates freedom. Freedom for the designer to go further and
freedom for the user to customize everything according to their needs.
-Ready to create your own theme? Great. Here some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
+Ready to create your own theme? Great. Here are some things you should know before you begin. This tutorial is a guide to creating an Odoo theme.
.. image:: theme_tutorial_assets/img/Intro.jpg
@@ -219,8 +219,8 @@ Odoo’s themes are packaged like modules. Even if you are designing a very simp
Create another empty file and name it ``__init__.py``. It's a
mandatory system file. Create and leave it blank.
-``view`` and ``static`` folders
- Create them in the main folder. In ``view`` you'll place your xml
+``views`` and ``static`` folders
+ Create them in the main folder. In ``views`` you'll place your xml
files that define your snippets, your pages and your
options. ``static`` folder is the right place for your style ,
images and custom js code.
@@ -276,7 +276,6 @@ The ``data`` property will contain the xml files list. Right now it’s empty, b
Installing your theme
---------------------
-To install your theme you just place your theme folder inside "addons"
To install your theme, you just place your theme folder inside addons in your Odoo installation.
After that, navigate to the Settings page, look for your theme and click on the install button.
@@ -310,7 +309,7 @@ Extend the default Header
By default, Odoo header contains a responsive navigation menu and the company’s logo. You can easily add new elements or style the existing one.
-To do so, create a **layout.xml** file in your **views** folder folder and add the default Odoo xml markup.
+To do so, create a **layout.xml** file in your **views** folder and add the default Odoo xml markup.
.. code-block:: xml
@@ -429,7 +428,7 @@ default page layout with our code.
As you can see, we wrapped our code into two ````, one with ID ``wrap`` and the other one with class ``container``. This is to provide a minimal layout.
The next step is to add an empty area that the user
-can fill with snippets. To archive this, just create a ``div`` with
+can fill with snippets. To achieve this, just create a ``div`` with
``oe_structure`` class just before closing the ``div#wrap`` element.
.. code-block:: xml
@@ -463,7 +462,7 @@ can fill with snippets. To archive this, just create a ``div`` with
You can create as many snippet areas as you like and place them anywhere in your pages.
-Our page is almost ready. Now all we have to do is add **pages.xml** in our **__opererp_.py** file
+Our page is almost ready. Now all we have to do is add **pages.xml** in our **__openerp__.py** file
.. code-block:: python
@@ -555,8 +554,8 @@ order to operate.
In this case, we are referring to ``assets_frontend`` template,
located in the ``website`` module. ``assets_frontend`` specifies the
-list of assets loaded by the the website builder and our goal is to
-add our less file to this list.
+list of assets loaded by the website builder and our goal is to add
+our less file to this list.
This can be achieved using xpath with the attributes
``expr="link[last()]"`` and ``position="after"``, which means "*take my
@@ -615,12 +614,12 @@ The template contains the HTML markup that will be displayed by the snippet.
As you can see, we used Bootstrap default classes for our three columns. It’s not just about layout, these classes **will be triggered by the Website Builder to make them resizable by the user**.
-The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippet.xml** file.
+The previous code will create the snippet’s content, but we still need to place it into the editor bar, so the user will be able to drag&drop it into the page. Copy/paste this template in your **snippets.xml** file.
.. code-block:: xml
-
+
@@ -666,7 +665,7 @@ Options group properties
Options are wrapped in groups. Groups can have properties that define how the included options will interact with the user interface.
``data-selector=" css selector(s) "``
- Bind the all the options included into the group to a particular element.
+ Bind all the options included into the group to a particular element.
``data-js=" custom method name "``
Is used to bind custom Javascript methods.
``data-drop-in=" css selector(s) "``
@@ -709,7 +708,7 @@ We start by adding a new file in our views folder - name it **options.xml** and
.. note::
- The previous template will inherit the default **snippet_option template** adding our options after the **background** options (xpath expr attribute).
+ The previous template will inherit the default **snippet_options template** adding our options after the **background** options (xpath expr attribute).
To place your options in a particular order, inspect the **snippet_options template** from the **website module** and add your options before/after the desired position.
As you can see, we wrapped all our options inside a DIV tag that will
@@ -724,6 +723,7 @@ Since ``select_class`` method avoids multiple selections, the last "empty"
option will reset the snippet to default.
Add **options.xml** to ``__openerp__.py`` and update your theme.
+
.. image:: theme_tutorial_assets/img/restart.png
Dropping our snippet onto the page, you will notice that our new options are automatically added to the customize menu. Inspecting the page, you will also notice that the class will be applied to the element when selecting an option.
diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py
index b27b954edd8..a3a93971b59 100644
--- a/openerp/addons/base/tests/test_expression.py
+++ b/openerp/addons/base/tests/test_expression.py
@@ -94,6 +94,41 @@ class test_expression(common.TransactionCase):
# self.assertTrue(a not in with_any_other_than_a, "Search for category_id with any other than cat_a failed (1).")
# self.assertTrue(ab in with_any_other_than_a, "Search for category_id with any other than cat_a failed (2).")
+ def test_05_not_str_m2m(self):
+ registry, cr, uid = self.registry, self.cr, self.uid
+
+ partners = registry('res.partner')
+ categories = registry('res.partner.category')
+
+ cats = {}
+ for cat in 'A B AB'.split():
+ cats[cat] = categories.create(cr, uid, {'name': cat})
+
+ _partners = {
+ '0': [],
+ 'a': [cats['A']],
+ 'b': [cats['B']],
+ 'ab': [cats['AB']],
+ 'a b': [cats['A'], cats['B']],
+ 'b ab': [cats['B'], cats['AB']],
+ }
+ pids = {}
+ for p in _partners:
+ pids[p] = partners.create(cr, uid, {'name': p, 'category_id': [(6, 0, _partners[p])]})
+
+ base_domain = [('id', 'in', pids.values())]
+
+ def test(op, value, expected):
+ ids = set(partners.search(cr, uid, base_domain + [('category_id', op, value)]))
+ expected_ids = set(map(pids.__getitem__, expected))
+ self.assertSetEqual(ids, expected_ids, '%s %r should return %r' % (op, value, expected))
+
+ test('=', 'A', ['a', 'a b'])
+ test('!=', 'B', ['0', 'a', 'ab'])
+ test('like', 'A', ['a', 'ab', 'a b', 'b ab'])
+ test('not ilike', 'B', ['0', 'a'])
+ test('not like', 'AB', ['0', 'a', 'b', 'a b'])
+
def test_10_expression_parse(self):
# TDE note: those tests have been added when refactoring the expression.parse() method.
# They come in addition to the already existing test_osv_expression.yml; maybe some tests
diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py
index 40e55a9e7cb..424137c8571 100644
--- a/openerp/addons/test_new_api/models.py
+++ b/openerp/addons/test_new_api/models.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from openerp.exceptions import AccessError
from openerp.osv import osv, fields
class res_partner(osv.Model):
@@ -32,6 +33,8 @@ class Category(models.Model):
parent = fields.Many2one('test_new_api.category')
display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name')
dummy = fields.Char(store=False)
+ discussions = fields.Many2many('test_new_api.discussion', 'test_new_api_discussion_category',
+ 'category', 'discussion')
@api.one
@api.depends('name', 'parent.display_name') # this definition is recursive
@@ -57,6 +60,10 @@ class Category(models.Model):
# assign name of last category, and reassign display_name (to normalize it)
self.name = names[-1].strip()
+ def read(self, fields=None, load='_classic_read'):
+ if self.search_count([('id', 'in', self._ids), ('name', '=', 'NOACCESS')]):
+ raise AccessError('Sorry')
+ return super(Category, self).read(fields, load)
class Discussion(models.Model):
_name = 'test_new_api.discussion'
diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py
index 4eb0e06c9fb..0972a8bdfd6 100644
--- a/openerp/addons/test_new_api/tests/test_new_fields.py
+++ b/openerp/addons/test_new_api/tests/test_new_fields.py
@@ -3,6 +3,7 @@
#
from datetime import date, datetime
+from openerp.exceptions import AccessError
from openerp.tests import common
@@ -344,6 +345,24 @@ class TestNewFields(common.TransactionCase):
self.assertEqual(data['display_name'], display_name)
self.assertEqual(data['size'], size)
+ def test_31_prefetch(self):
+ """ test prefetch of records handle AccessError """
+ Category = self.env['test_new_api.category']
+ cat_1 = Category.create({'name': 'NOACCESS'}).id
+ cat_2 = Category.create({'name': 'ACCESS', 'parent': cat_1}).id
+
+ self.env.clear()
+
+ cat = Category.browse(cat_2)
+ self.assertEqual(cat.name, 'ACCESS')
+ # both categories should be in prefetch ids
+ self.assertSetEqual(self.env.prefetch[Category._name], set([cat_1, cat_2]))
+ # but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
+ self.assertFalse(len(cat.discussions))
+ self.assertEqual(cat.parent.id, cat_1)
+ with self.assertRaises(AccessError):
+ Category.browse(cat_1).name
+
def test_40_new(self):
""" test new records. """
discussion = self.env.ref('test_new_api.discussion_0')
diff --git a/openerp/models.py b/openerp/models.py
index 45d761e187a..a884d1ed3a2 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -3189,7 +3189,8 @@ class BaseModel(object):
try:
result = records.read(list(fnames), load='_classic_write')
except AccessError:
- pass
+ # not all records may be accessible, try with only current record
+ result = self.read(list(fnames), load='_classic_write')
# check the cache, and update it if necessary
if not self._cache.contains(field):
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index 46a4235acf7..48426f9e83a 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -909,9 +909,10 @@ class expression(object):
if right is not False:
if isinstance(right, basestring):
- ids2 = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context, limit=None)]
+ op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
+ ids2 = [x[0] for x in comodel.name_search(cr, uid, right, [], op, context=context, limit=None)]
if ids2:
- operator = 'in'
+ operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
elif isinstance(right, collections.Iterable):
ids2 = right
else:
@@ -935,7 +936,6 @@ class expression(object):
elif column._type == 'many2many':
rel_table, rel_id1, rel_id2 = column._sql_names(model)
- #FIXME
if operator == 'child_of':
def _rec_convert(ids):
if comodel == model:
@@ -950,9 +950,10 @@ class expression(object):
call_null_m2m = True
if right is not False:
if isinstance(right, basestring):
- res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context)]
+ op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
+ res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], op, context=context)]
if res_ids:
- operator = 'in'
+ operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
else:
if not isinstance(right, list):
res_ids = [right]