diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 61d3feff07a..a8093611c29 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -305,6 +305,7 @@ class Pricelist(models.Model): return pricelist.get_products_price( list(pycompat.izip(**products_by_qty_by_partner))) + # DEPRECATED (Not used anymore, see d39d583b2) -> Remove me in master (saas12.3) def _get_partner_pricelist(self, partner_id, company_id=None): """ Retrieve the applicable pricelist for a given partner in a given company. diff --git a/addons/website/models/ir_rule.py b/addons/website/models/ir_rule.py index ba5e07128ee..a11ddef1e53 100644 --- a/addons/website/models/ir_rule.py +++ b/addons/website/models/ir_rule.py @@ -1,5 +1,6 @@ # coding: utf-8 from odoo import api, models +from odoo.addons.website.models import ir_http class IrRule(models.Model): @@ -8,7 +9,14 @@ class IrRule(models.Model): @api.model def _eval_context(self): res = super(IrRule, self)._eval_context() - res['website_id'] = self.env['website'].get_current_website().id + + # We need is_frontend to avoid showing website's company items in backend + # (that could be different than current company). We can't use + # `get_current_website(falback=False)` as it could also return a website + # in backend (if domain set & match).. + is_frontend = ir_http.get_request_website() + Website = self.env['website'] + res['website'] = is_frontend and Website.get_current_website() or Website return res def _compute_domain_keys(self): diff --git a/addons/website_sale/__init__.py b/addons/website_sale/__init__.py index 5e8764bb37f..03800bab040 100644 --- a/addons/website_sale/__init__.py +++ b/addons/website_sale/__init__.py @@ -1,7 +1,19 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +from odoo import api, SUPERUSER_ID from . import controllers from . import models from . import wizard from . import report + + +def uninstall_hook(cr, registry): + ''' Need to reenable the `product` pricelist multi-company rule that were + disabled to be 'overriden' for multi-website purpose + ''' + env = api.Environment(cr, SUPERUSER_ID, {}) + pl_rule = env.ref('product.product_pricelist_comp_rule', raise_if_not_found=False) + pl_item_rule = env.ref('product.product_pricelist_item_comp_rule', raise_if_not_found=False) + multi_company_rules = pl_rule or env['ir.rule'] + multi_company_rules += pl_item_rule or env['ir.rule'] + multi_company_rules.write({'active': True}) diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index 91ce59ec5eb..b07dc32747b 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -30,4 +30,5 @@ 'qweb': ['static/src/xml/*.xml'], 'installable': True, 'application': True, + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index d9c421e76b9..66f29184670 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -91,6 +91,17 @@ class ProductPricelist(models.Model): '|', ('selectable', '=', True), ('code', '!=', False), ] + def _get_partner_pricelist_multi(self, partner_ids, company_id=None): + ''' If `property_product_pricelist` is read from website, we should use + the website's company and not the user's one. + Passing a `company_id` to super will avoid using the current user's + company. + ''' + website = ir_http.get_request_website() + if not company_id and website: + company_id = website.company_id.id + return super(ProductPricelist, self)._get_partner_pricelist_multi(partner_ids, company_id) + class ProductPublicCategory(models.Model): _name = "product.public.category" diff --git a/addons/website_sale/security/website_sale.xml b/addons/website_sale/security/website_sale.xml index 5d73c4d38b7..0af26e6d7d7 100644 --- a/addons/website_sale/security/website_sale.xml +++ b/addons/website_sale/security/website_sale.xml @@ -52,6 +52,31 @@ - + + + + + + + + + product pricelist company rule + + + [('company_id','in',(False,user.company_id.id,website.company_id.id))] + + + product pricelist item company rule + + + [('company_id','in',(False,user.company_id.id,website.company_id.id))] + diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py index dc04f55301a..46ae0e56a28 100644 --- a/addons/website_sale/tests/test_website_sale_pricelist.py +++ b/addons/website_sale/tests/test_website_sale_pricelist.py @@ -4,7 +4,7 @@ try: from unittest.mock import patch except ImportError: from mock import patch -from odoo.tests.common import TransactionCase +from odoo.tests.common import HttpCase, TransactionCase class TestWebsitePriceList(TransactionCase): @@ -125,3 +125,98 @@ class TestWebsitePriceList(TransactionCase): pls = self.get_pl(show, current_pl, country) self.assertEquals(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)' % (country, len(pls), pls.mapped('name'), len(result), result)) + + +def simulate_frontend_context(self, website_id=1): + # Mock this method will be enough to simulate frontend context in most methods + def get_request_website(): + return self.env['website'].browse(website_id) + patcher = patch('odoo.addons.website.models.ir_http.get_request_website', wraps=get_request_website) + patcher.start() + self.addCleanup(patcher.stop) + + +class TestWebsitePriceListMultiCompany(TransactionCase): + def setUp(self): + ''' Create a basic multi-company pricelist environment: + - Set up 2 companies with their own company-restricted pricelist each. + - Add demo user in those 2 companies + - For each company, add that company pricelist to the demo user partner. + - Set website's company to company 2 + - Demo user will still be in company 1 + ''' + super(TestWebsitePriceListMultiCompany, self).setUp() + + self.demo_user = self.env.ref('base.user_demo') + + # Create and add demo user to 2 companies + self.company1 = self.demo_user.company_id + self.company2 = self.env['res.company'].create({'name': 'Test Company'}) + self.demo_user.company_ids += self.company2 + # Set company2 as current company for demo user + self.website = self.env['website'].browse(1) + self.website.company_id = self.company2 + + # Create a company pricelist for each company and set it to demo user + self.c1_pl = self.env['product.pricelist'].create({ + 'name': 'Company 1 Pricelist', + 'company_id': self.company1.id, + }) + self.c2_pl = self.env['product.pricelist'].create({ + 'name': 'Company 2 Pricelist', + 'company_id': self.company2.id, + 'website_id': False, + }) + self.demo_user.partner_id.property_product_pricelist = self.c1_pl + # Switch env.user company to create ir.property in company2 + self.env.user.company_id = self.company2 + self.demo_user.partner_id.property_product_pricelist = self.c2_pl + + # Ensure everything was done correctly + self.assertEqual(self.demo_user.partner_id.with_context(force_company=self.company1.id).property_product_pricelist, self.c1_pl) + self.assertEqual(self.demo_user.partner_id.with_context(force_company=self.company2.id).property_product_pricelist, self.c2_pl) + irp1 = self.env['ir.property'].search([ + ('name', '=', 'property_product_pricelist'), + ('company_id', '=', self.company1.id), + ('res_id', '=', 'res.partner,%s' % self.demo_user.partner_id.id), + ('value_reference', '=', 'product.pricelist,%s' % self.c1_pl.id), + ]) + irp2 = self.env['ir.property'].search([ + ('name', '=', 'property_product_pricelist'), + ('company_id', '=', self.company2.id), + ('res_id', '=', 'res.partner,%s' % self.demo_user.partner_id.id), + ('value_reference', '=', 'product.pricelist,%s' % self.c2_pl.id), + ]) + self.assertEqual(len(irp1 + irp2), 2, "Ensure there is an `ir.property` for demo partner for every company, and that the pricelist is the company specific one.") + simulate_frontend_context(self) + # ---------------------------------- IR.PROPERTY ------------------------------------- + # id | name | res_id | company_id | value_reference + # ------------------------------------------------------------------------------------ + # 1 | 'property_product_pricelist' | | 1 | product.pricelist,1 + # 2 | 'property_product_pricelist' | | 2 | product.pricelist,2 + # 3 | 'property_product_pricelist' | res.partner,8 | 1 | product.pricelist,10 + # 4 | 'property_product_pricelist' | res.partner,8 | 2 | product.pricelist,11 + + def test_property_product_pricelist_multi_company(self): + ''' Test that the `property_product_pricelist` of `res.partner` is read + for the company of the website and not the current user company. + This is the case if the user visit a website for which the company + is not the same as its user's company. + + Here, as demo user (company1), we will visit website1 (company2). + It should return the ir.property for demo user for company2 and not + for the company1 as we should get the website's company pricelist + and not the demo user's current company pricelist. + ''' + # First check: It should return ir.property,4 as company_id is + # website.company_id and not env.user.company_id + company_id = self.website.company_id.id + partner = self.demo_user.partner_id.with_context(force_company=company_id) + demo_pl = partner.property_product_pricelist + self.assertEqual(demo_pl, self.c2_pl) + + # Second thing to check: It should not error in read right access error + # Indeed, the ir.rule for pricelists rights about company should allow to + # also read a pricelist from another company if that company is the one + # from the currently visited website. + self.env(user=self.env.ref('base.user_demo'))['product.pricelist'].browse(demo_pl.id).name