From a5a5a57e482ccecb3e18abf7f04778ffe3c63d38 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Tue, 12 Mar 2019 10:06:59 +0000 Subject: [PATCH] [FIX] website(_sale): allow to use and access website pricelist Before this commit, if the website's company was different than the user company, the ecommerce would use the user's company instead of its own pricelists. Step to reproduce: - Set company2 to website1 - Create a pricelist1 for company1 and a pricelist2 for company2 - With an user from company1, visit the website1. - The pricelist used will be the one from the user, pricelist1 and not the pricelist2 from company2 which is the website1 company. --- addons/product/models/product_pricelist.py | 1 + addons/website/models/ir_rule.py | 10 +- addons/website_sale/__init__.py | 14 ++- addons/website_sale/__manifest__.py | 1 + addons/website_sale/models/product.py | 11 +++ addons/website_sale/security/website_sale.xml | 27 +++++- .../tests/test_website_sale_pricelist.py | 97 ++++++++++++++++++- 7 files changed, 157 insertions(+), 4 deletions(-) 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