summaryrefslogtreecommitdiff
path: root/addons/website_sale/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_sale/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_sale/tests')
-rw-r--r--addons/website_sale/tests/__init__.py13
-rw-r--r--addons/website_sale/tests/test_customize.py313
-rw-r--r--addons/website_sale/tests/test_sale_process.py230
-rw-r--r--addons/website_sale/tests/test_sitemap.py25
-rw-r--r--addons/website_sale/tests/test_website_sale_cart_recovery.py126
-rw-r--r--addons/website_sale/tests/test_website_sale_image.py320
-rw-r--r--addons/website_sale/tests/test_website_sale_mail.py31
-rw-r--r--addons/website_sale/tests/test_website_sale_pricelist.py469
-rw-r--r--addons/website_sale/tests/test_website_sale_product_attribute_value_config.py191
-rw-r--r--addons/website_sale/tests/test_website_sale_visitor.py53
-rw-r--r--addons/website_sale/tests/test_website_sequence.py77
11 files changed, 1848 insertions, 0 deletions
diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py
new file mode 100644
index 00000000..dc94da27
--- /dev/null
+++ b/addons/website_sale/tests/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_customize
+from . import test_sale_process
+from . import test_sitemap
+from . import test_website_sale_cart_recovery
+from . import test_website_sale_mail
+from . import test_website_sale_pricelist
+from . import test_website_sale_product_attribute_value_config
+from . import test_website_sale_image
+from . import test_website_sequence
+from . import test_website_sale_visitor
diff --git a/addons/website_sale/tests/test_customize.py b/addons/website_sale/tests/test_customize.py
new file mode 100644
index 00000000..285a70d6
--- /dev/null
+++ b/addons/website_sale/tests/test_customize.py
@@ -0,0 +1,313 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+
+from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
+from odoo.modules.module import get_module_resource
+from odoo.tests import tagged
+
+@tagged('post_install', '-at_install')
+class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
+
+ def setUp(self):
+ super(TestUi, self).setUp()
+ # create a template
+ product_template = self.env['product.template'].create({
+ 'name': 'Test Product',
+ 'is_published': True,
+ 'list_price': 750,
+ })
+
+ tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 10})
+ product_template.taxes_id = tax
+
+ product_attribute = self.env['product.attribute'].create({
+ 'name': 'Legs',
+ 'sequence': 10,
+ })
+ product_attribute_value_1 = self.env['product.attribute.value'].create({
+ 'name': 'Steel - Test',
+ 'attribute_id': product_attribute.id,
+ 'sequence': 1,
+ })
+ product_attribute_value_2 = self.env['product.attribute.value'].create({
+ 'name': 'Aluminium',
+ 'attribute_id': product_attribute.id,
+ 'sequence': 2,
+ })
+
+ # set attribute and attribute values on the template
+ self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': product_template.id,
+ 'value_ids': [(6, 0, [product_attribute_value_1.id, product_attribute_value_2.id])]
+ }])
+
+ # set a different price on the variants to differentiate them
+ product_template_attribute_values = self.env['product.template.attribute.value'] \
+ .search([('product_tmpl_id', '=', product_template.id)])
+
+ for ptav in product_template_attribute_values:
+ if ptav.name == "Steel - Test":
+ ptav.price_extra = 0
+ else:
+ ptav.price_extra = 50.4
+
+ def test_01_admin_shop_customize_tour(self):
+ # Enable Variant Group
+ self.env.ref('product.group_product_variant').write({'users': [(4, self.env.ref('base.user_admin').id)]})
+ self.start_tour("/", 'shop_customize', login="admin")
+
+ def test_02_admin_shop_custom_attribute_value_tour(self):
+ # Make sure pricelist rule exist
+ self.product_attribute_1 = self.env['product.attribute'].create({
+ 'name': 'Legs',
+ 'sequence': 10,
+ })
+ product_attribute_value_1 = self.env['product.attribute.value'].create({
+ 'name': 'Steel',
+ 'attribute_id': self.product_attribute_1.id,
+ 'sequence': 1,
+ })
+ product_attribute_value_2 = self.env['product.attribute.value'].create({
+ 'name': 'Aluminium',
+ 'attribute_id': self.product_attribute_1.id,
+ 'sequence': 2,
+ })
+ product_attribute_2 = self.env['product.attribute'].create({
+ 'name': 'Color',
+ 'sequence': 20,
+ })
+ product_attribute_value_3 = self.env['product.attribute.value'].create({
+ 'name': 'White',
+ 'attribute_id': product_attribute_2.id,
+ 'sequence': 1,
+ })
+ product_attribute_value_4 = self.env['product.attribute.value'].create({
+ 'name': 'Black',
+ 'attribute_id': product_attribute_2.id,
+ 'sequence': 2,
+ })
+
+ # Create product template
+ self.product_product_4_product_template = self.env['product.template'].create({
+ 'name': 'Customizable Desk (TEST)',
+ 'standard_price': 500.0,
+ 'list_price': 750.0,
+ })
+
+ # Generate variants
+ self.env['product.template.attribute.line'].create([{
+ 'product_tmpl_id': self.product_product_4_product_template.id,
+ 'attribute_id': self.product_attribute_1.id,
+ 'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
+ }, {
+ 'product_tmpl_id': self.product_product_4_product_template.id,
+ 'attribute_id': product_attribute_2.id,
+ 'value_ids': [(4, product_attribute_value_3.id), (4, product_attribute_value_4.id)],
+
+ }])
+ product_template = self.product_product_4_product_template
+
+ # Add Custom Attribute
+ product_attribute_value_7 = self.env['product.attribute.value'].create({
+ 'name': 'Custom TEST',
+ 'attribute_id': self.product_attribute_1.id,
+ 'sequence': 3,
+ 'is_custom': True
+ })
+ self.product_product_4_product_template.attribute_line_ids[0].write({'value_ids': [(4, product_attribute_value_7.id)]})
+
+ img_path = get_module_resource('product', 'static', 'img', 'product_product_11-image.png')
+ img_content = base64.b64encode(open(img_path, "rb").read())
+ self.product_product_11_product_template = self.env['product.template'].create({
+ 'name': 'Conference Chair (TEST)',
+ 'website_sequence': 9999, # laule
+ 'image_1920': img_content,
+ 'list_price': 16.50,
+ })
+
+ self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.product_product_11_product_template.id,
+ 'attribute_id': self.product_attribute_1.id,
+ 'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
+ })
+ self.product_product_11_product_template.attribute_line_ids[0].product_template_value_ids[1].price_extra = 6.40
+
+ # Setup a second optional product
+ self.product_product_1_product_template = self.env['product.template'].create({
+ 'name': 'Chair floor protection',
+ 'list_price': 12.0,
+ })
+
+ # fix runbot, sometimes one pricelist is chosen, sometimes the other...
+ pricelists = self.env['website'].get_current_website().get_current_pricelist() | self.env.ref('product.list0')
+
+ for pricelist in pricelists:
+ if not pricelist.item_ids.filtered(lambda i: i.product_tmpl_id == product_template and i.price_discount == 20):
+ self.env['product.pricelist.item'].create({
+ 'base': 'list_price',
+ 'applied_on': '1_product',
+ 'pricelist_id': pricelist.id,
+ 'product_tmpl_id': product_template.id,
+ 'price_discount': 20,
+ 'min_quantity': 2,
+ 'compute_price': 'formula',
+ })
+
+ pricelist.discount_policy = 'without_discount'
+
+ self.start_tour("/", 'shop_custom_attribute_value', login="admin")
+
+ def test_03_public_tour_shop_dynamic_variants(self):
+ """ The goal of this test is to make sure product variants with dynamic
+ attributes can be created by the public user (when being added to cart).
+ """
+
+ # create the attribute
+ product_attribute = self.env['product.attribute'].create({
+ 'name': "Dynamic Attribute",
+ 'create_variant': 'dynamic',
+ })
+
+ # create the attribute values
+ product_attribute_values = self.env['product.attribute.value'].create([{
+ 'name': "Dynamic Value 1",
+ 'attribute_id': product_attribute.id,
+ 'sequence': 1,
+ }, {
+ 'name': "Dynamic Value 2",
+ 'attribute_id': product_attribute.id,
+ 'sequence': 2,
+ }])
+
+ # create the template
+ product_template = self.env['product.template'].create({
+ 'name': 'Dynamic Product',
+ 'website_published': True,
+ 'list_price': 0,
+ })
+
+ # set attribute and attribute values on the template
+ self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': product_template.id,
+ 'value_ids': [(6, 0, product_attribute_values.ids)]
+ }])
+
+ # set a different price on the variants to differentiate them
+ product_template_attribute_values = self.env['product.template.attribute.value'] \
+ .search([('product_tmpl_id', '=', product_template.id)])
+
+ for ptav in product_template_attribute_values:
+ if ptav.name == "Dynamic Value 1":
+ ptav.price_extra = 10
+ else:
+ # 0 to not bother with the pricelist of the public user
+ ptav.price_extra = 0
+
+ self.start_tour("/", 'tour_shop_dynamic_variants')
+
+ def test_04_portal_tour_deleted_archived_variants(self):
+ """The goal of this test is to make sure deleted and archived variants
+ are shown as impossible combinations.
+
+ Using "portal" to have various users in the tests.
+ """
+
+ # create the attribute
+ product_attribute = self.env['product.attribute'].create({
+ 'name': "My Attribute",
+ 'create_variant': 'always',
+ })
+
+ # create the attribute values
+ product_attribute_values = self.env['product.attribute.value'].create([{
+ 'name': "My Value 1",
+ 'attribute_id': product_attribute.id,
+ 'sequence': 1,
+ }, {
+ 'name': "My Value 2",
+ 'attribute_id': product_attribute.id,
+ 'sequence': 2,
+ }, {
+ 'name': "My Value 3",
+ 'attribute_id': product_attribute.id,
+ 'sequence': 3,
+ }])
+
+ # create the template
+ product_template = self.env['product.template'].create({
+ 'name': 'Test Product 2',
+ 'is_published': True,
+ })
+
+ # set attribute and attribute values on the template
+ self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': product_template.id,
+ 'value_ids': [(6, 0, product_attribute_values.ids)]
+ }])
+
+ # set a different price on the variants to differentiate them
+ product_template_attribute_values = self.env['product.template.attribute.value'] \
+ .search([('product_tmpl_id', '=', product_template.id)])
+
+ product_template_attribute_values[0].price_extra = 10
+ product_template_attribute_values[1].price_extra = 20
+ product_template_attribute_values[2].price_extra = 30
+
+ # archive first combination (first variant)
+ product_template.product_variant_ids[0].active = False
+ # delete second combination (which is now first variant since cache has been cleared)
+ product_template.product_variant_ids[0].unlink()
+
+ self.start_tour("/", 'tour_shop_deleted_archived_variants', login="portal")
+
+ def test_05_demo_tour_no_variant_attribute(self):
+ """The goal of this test is to make sure attributes no_variant are
+ correctly added to cart.
+
+ Using "demo" to have various users in the tests.
+ """
+
+ # create the attribute
+ product_attribute_no_variant = self.env['product.attribute'].create({
+ 'name': "No Variant Attribute",
+ 'create_variant': 'no_variant',
+ })
+
+ # create the attribute value
+ product_attribute_value_no_variant = self.env['product.attribute.value'].create({
+ 'name': "No Variant Value",
+ 'attribute_id': product_attribute_no_variant.id,
+ })
+
+ # create the template
+ product_template = self.env['product.template'].create({
+ 'name': 'Test Product 3',
+ 'website_published': True,
+ })
+
+ # set attribute and attribute value on the template
+ ptal = self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute_no_variant.id,
+ 'product_tmpl_id': product_template.id,
+ 'value_ids': [(6, 0, product_attribute_value_no_variant.ids)]
+ }])
+
+ # set a price on the value
+ ptal.product_template_value_ids.price_extra = 10
+
+ self.start_tour("/", 'tour_shop_no_variant_attribute', login="demo")
+
+ def test_06_admin_list_view_b2c(self):
+ self.env.ref('product.group_product_variant').write({'users': [(4, self.env.ref('base.user_admin').id)]})
+
+ # activate b2c
+ config = self.env['res.config.settings'].create({})
+ config.show_line_subtotals_tax_selection = "tax_included"
+ config._onchange_sale_tax()
+ config.execute()
+
+ self.start_tour("/", 'shop_list_view_b2c', login="admin")
diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py
new file mode 100644
index 00000000..558e9965
--- /dev/null
+++ b/addons/website_sale/tests/test_sale_process.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+from odoo import api
+from odoo.addons.base.tests.common import HttpCaseWithUserDemo, TransactionCaseWithUserDemo
+from odoo.addons.website_sale.controllers.main import WebsiteSale
+from odoo.addons.website.tools import MockRequest
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestUi(HttpCaseWithUserDemo):
+
+ def setUp(self):
+ super(TestUi, self).setUp()
+ product_product_7 = self.env['product.product'].create({
+ 'name': 'Storage Box',
+ 'standard_price': 70.0,
+ 'list_price': 79.0,
+ 'website_published': True,
+ })
+ self.product_attribute_1 = self.env['product.attribute'].create({
+ 'name': 'Legs',
+ 'sequence': 10,
+ })
+ product_attribute_value_1 = self.env['product.attribute.value'].create({
+ 'name': 'Steel',
+ 'attribute_id': self.product_attribute_1.id,
+ 'sequence': 1,
+ })
+ product_attribute_value_2 = self.env['product.attribute.value'].create({
+ 'name': 'Aluminium',
+ 'attribute_id': self.product_attribute_1.id,
+ 'sequence': 2,
+ })
+ self.product_product_11_product_template = self.env['product.template'].create({
+ 'name': 'Conference Chair (CONFIG)',
+ 'list_price': 16.50,
+ 'accessory_product_ids': [(4, product_product_7.id)],
+ })
+ self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.product_product_11_product_template.id,
+ 'attribute_id': self.product_attribute_1.id,
+ 'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
+ })
+
+ self.product_product_1_product_template = self.env['product.template'].create({
+ 'name': 'Chair floor protection',
+ 'list_price': 12.0,
+ })
+
+ cash_journal = self.env['account.journal'].create({'name': 'Cash - Test', 'type': 'cash', 'code': 'CASH - Test'})
+ self.env.ref('payment.payment_acquirer_transfer').journal_id = cash_journal
+
+ # Avoid Shipping/Billing address page
+ (self.env.ref('base.partner_admin') + self.partner_demo).write({
+ 'street': '215 Vine St',
+ 'city': 'Scranton',
+ 'zip': '18503',
+ 'country_id': self.env.ref('base.us').id,
+ 'state_id': self.env.ref('base.state_us_39').id,
+ 'phone': '+1 555-555-5555',
+ 'email': 'admin@yourcompany.example.com',
+ })
+
+ def test_01_admin_shop_tour(self):
+ self.start_tour("/", 'shop', login="admin")
+
+ def test_02_admin_checkout(self):
+ self.start_tour("/", 'shop_buy_product', login="admin")
+
+ def test_03_demo_checkout(self):
+ self.start_tour("/", 'shop_buy_product', login="demo")
+
+ def test_04_admin_website_sale_tour(self):
+ tax_group = self.env['account.tax.group'].create({'name': 'Tax 15%'})
+ tax = self.env['account.tax'].create({
+ 'name': 'Tax 15%',
+ 'amount': 15,
+ 'type_tax_use': 'sale',
+ 'tax_group_id': tax_group.id
+ })
+ # storage box
+ self.product_product_7 = self.env['product.product'].create({
+ 'name': 'Storage Box Test',
+ 'standard_price': 70.0,
+ 'list_price': 79.0,
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'website_published': True,
+ 'invoice_policy': 'delivery',
+ })
+ self.product_product_7.taxes_id = [tax.id]
+ self.env['res.config.settings'].create({
+ 'auth_signup_uninvited': 'b2c',
+ 'show_line_subtotals_tax_selection': 'tax_excluded',
+ 'group_show_line_subtotals_tax_excluded': True,
+ 'group_show_line_subtotals_tax_included': False,
+ }).execute()
+
+ self.start_tour("/", 'website_sale_tour')
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo):
+ ''' The goal of this method class is to test the address management on
+ the checkout (new/edit billing/shipping, company_id, website_id..).
+ '''
+
+ def setUp(self):
+ super(TestWebsiteSaleCheckoutAddress, self).setUp()
+ self.website = self.env.ref('website.default_website')
+ self.country_id = self.env.ref('base.be').id
+ self.WebsiteSaleController = WebsiteSale()
+ self.default_address_values = {
+ 'name': 'a res.partner address', 'email': 'email@email.email', 'street': 'ooo',
+ 'city': 'ooo', 'zip': '1200', 'country_id': self.country_id, 'submitted': 1,
+ }
+
+ def _create_so(self, partner_id=None):
+ return self.env['sale.order'].create({
+ 'partner_id': partner_id,
+ 'website_id': self.website.id,
+ 'order_line': [(0, 0, {
+ 'product_id': self.env['product.product'].create({'name': 'Product A', 'list_price': 100}).id,
+ 'name': 'Product A',
+ })]
+ })
+
+ def _get_last_address(self, partner):
+ ''' Useful to retrieve the last created shipping address '''
+ return partner.child_ids.sorted('id', reverse=True)[0]
+
+ # TEST WEBSITE
+ def test_01_create_shipping_address_specific_user_account(self):
+ ''' Ensure `website_id` is correctly set (specific_user_account) '''
+ p = self.env.user.partner_id
+ so = self._create_so(p.id)
+
+ with MockRequest(self.env, website=self.website, sale_order_id=so.id):
+ self.WebsiteSaleController.address(**self.default_address_values)
+ self.assertFalse(self._get_last_address(p).website_id, "New shipping address should not have a website set on it (no specific_user_account).")
+
+ self.website.specific_user_account = True
+
+ self.WebsiteSaleController.address(**self.default_address_values)
+ self.assertEqual(self._get_last_address(p).website_id, self.website, "New shipping address should have a website set on it (specific_user_account).")
+
+ # TEST COMPANY
+ def _setUp_multicompany_env(self):
+ ''' Have 2 companies A & B.
+ Have 1 website 1 which company is B
+ Have admin on company A
+ '''
+ self.company_a = self.env['res.company'].create({
+ 'name': 'Company A',
+ })
+ self.company_b = self.env['res.company'].create({
+ 'name': 'Company B',
+ })
+ self.company_c = self.env['res.company'].create({
+ 'name': 'Company C',
+ })
+ self.website.company_id = self.company_b
+ self.env.user.company_id = self.company_a
+
+ self.demo_user = self.user_demo
+ self.demo_user.company_ids += self.company_c
+ self.demo_user.company_id = self.company_c
+ self.demo_partner = self.demo_user.partner_id
+
+ def test_02_demo_address_and_company(self):
+ ''' This test ensure that the company_id of the address (partner) is
+ correctly set and also, is not wrongly changed.
+ eg: new shipping should use the company of the website and not the
+ one from the admin, and editing a billing should not change its
+ company.
+ '''
+ self._setUp_multicompany_env()
+ so = self._create_so(self.demo_partner.id)
+
+ env = api.Environment(self.env.cr, self.demo_user.id, {})
+ # change also website env for `sale_get_order` to not change order partner_id
+ with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
+ # 1. Logged in user, new shipping
+ self.WebsiteSaleController.address(**self.default_address_values)
+ new_shipping = self._get_last_address(self.demo_partner)
+ self.assertTrue(new_shipping.company_id != self.env.user.company_id, "Logged in user new shipping should not get the company of the sudo() neither the one from it's partner..")
+ self.assertEqual(new_shipping.company_id, self.website.company_id, ".. but the one from the website.")
+
+ # 2. Logged in user, edit billing
+ self.default_address_values['partner_id'] = self.demo_partner.id
+ self.WebsiteSaleController.address(**self.default_address_values)
+ self.assertEqual(self.demo_partner.company_id, self.company_c, "Logged in user edited billing (the partner itself) should not get its company modified.")
+
+ def test_03_public_user_address_and_company(self):
+ ''' Same as test_02 but with public user '''
+ self._setUp_multicompany_env()
+ so = self._create_so(self.website.user_id.partner_id.id)
+
+ env = api.Environment(self.env.cr, self.website.user_id.id, {})
+ # change also website env for `sale_get_order` to not change order partner_id
+ with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
+ # 1. Public user, new billing
+ self.default_address_values['partner_id'] = -1
+ self.WebsiteSaleController.address(**self.default_address_values)
+ new_partner = so.partner_id
+ self.assertNotEqual(new_partner, self.website.user_id.partner_id, "New billing should have created a new partner and assign it on the SO")
+ self.assertEqual(new_partner.company_id, self.website.company_id, "The new partner should get the company of the website")
+
+ # 2. Public user, edit billing
+ self.default_address_values['partner_id'] = new_partner.id
+ self.WebsiteSaleController.address(**self.default_address_values)
+ self.assertEqual(new_partner.company_id, self.website.company_id, "Public user edited billing (the partner itself) should not get its company modified.")
+
+ def test_04_apply_empty_pl(self):
+ ''' Ensure empty pl code reset the applied pl '''
+ so = self._create_so(self.env.user.partner_id.id)
+ eur_pl = self.env['product.pricelist'].create({
+ 'name': 'EUR_test',
+ 'website_id': self.website.id,
+ 'code': 'EUR_test',
+ })
+
+ with MockRequest(self.env, website=self.website, sale_order_id=so.id):
+ self.WebsiteSaleController.pricelist('EUR_test')
+ self.assertEqual(so.pricelist_id, eur_pl, "Ensure EUR_test is applied")
+
+ self.WebsiteSaleController.pricelist('')
+ self.assertNotEqual(so.pricelist_id, eur_pl, "Pricelist should be removed when sending an empty pl code")
diff --git a/addons/website_sale/tests/test_sitemap.py b/addons/website_sale/tests/test_sitemap.py
new file mode 100644
index 00000000..2baf167e
--- /dev/null
+++ b/addons/website_sale/tests/test_sitemap.py
@@ -0,0 +1,25 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import HttpCase, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSitemap(HttpCase):
+
+ def setUp(self):
+ super(TestSitemap, self).setUp()
+
+ self.cats = self.env['product.public.category'].create([{
+ 'name': 'Level 0',
+ }, {
+ 'name': 'Level 1',
+ }, {
+ 'name': 'Level 2',
+ }])
+ self.cats[2].parent_id = self.cats[1].id
+ self.cats[1].parent_id = self.cats[0].id
+
+ def test_01_shop_route_sitemap(self):
+ resp = self.url_open('/sitemap.xml')
+ level2_url = '/shop/category/level-0-level-1-level-2-%s' % self.cats[2].id
+ self.assertTrue(level2_url in resp.text, "Category entry in sitemap should be prefixed by its parent hierarchy.")
diff --git a/addons/website_sale/tests/test_website_sale_cart_recovery.py b/addons/website_sale/tests/test_website_sale_cart_recovery.py
new file mode 100644
index 00000000..eae65eac
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_cart_recovery.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import tagged
+from odoo.tests.common import HttpCase, TransactionCase
+from odoo.addons.base.tests.common import HttpCaseWithUserPortal
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleCartRecovery(HttpCaseWithUserPortal):
+
+ def test_01_shop_cart_recovery_tour(self):
+ """The goal of this test is to make sure cart recovery works."""
+ self.env['product.product'].create({
+ 'name': 'Acoustic Bloc Screens',
+ 'list_price': 2950.0,
+ 'website_published': True,
+ })
+
+ self.start_tour("/", 'shop_cart_recovery', login="portal")
+
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleCartRecoveryServer(TransactionCase):
+
+ def setUp(self):
+ res = super(TestWebsiteSaleCartRecoveryServer, self).setUp()
+
+ self.customer = self.env['res.partner'].create({
+ 'name': 'a',
+ 'email': 'a@example.com',
+ })
+ self.recovery_template_default = self.env.ref('website_sale.mail_template_sale_cart_recovery')
+ self.recovery_template_custom1 = self.recovery_template_default.copy()
+ self.recovery_template_custom2 = self.recovery_template_default.copy()
+
+ self.website0 = self.env['website'].create({
+ 'name': 'web0',
+ 'cart_recovery_mail_template_id': self.recovery_template_default.id,
+ })
+ self.website1 = self.env['website'].create({
+ 'name': 'web1',
+ 'cart_recovery_mail_template_id': self.recovery_template_custom1.id,
+ })
+ self.website2 = self.env['website'].create({
+ 'name': 'web2',
+ 'cart_recovery_mail_template_id': self.recovery_template_custom2.id,
+ })
+ self.so0 = self.env['sale.order'].create({
+ 'partner_id': self.customer.id,
+ 'website_id': self.website0.id,
+ 'is_abandoned_cart': True,
+ 'cart_recovery_email_sent': False,
+ })
+ self.so1 = self.env['sale.order'].create({
+ 'partner_id': self.customer.id,
+ 'website_id': self.website1.id,
+ 'is_abandoned_cart': True,
+ 'cart_recovery_email_sent': False,
+ })
+ self.so2 = self.env['sale.order'].create({
+ 'partner_id': self.customer.id,
+ 'website_id': self.website2.id,
+ 'is_abandoned_cart': True,
+ 'cart_recovery_email_sent': False,
+ })
+
+ return res
+
+ def test_cart_recovery_mail_template(self):
+ """Make sure that we get the correct cart recovery templates to send."""
+ self.assertEqual(
+ self.so1._get_cart_recovery_template(),
+ self.recovery_template_custom1,
+ "We do not return the correct mail template"
+ )
+ self.assertEqual(
+ self.so2._get_cart_recovery_template(),
+ self.recovery_template_custom2,
+ "We do not return the correct mail template"
+ )
+ # Orders that belong to different websites; we should get the default template
+ self.assertEqual(
+ (self.so1 + self.so2)._get_cart_recovery_template(),
+ self.recovery_template_default,
+ "We do not return the correct mail template"
+ )
+
+ def test_cart_recovery_mail_template_send(self):
+ """The goal of this test is to make sure cart recovery works."""
+ orders = self.so0 + self.so1 + self.so2
+
+ self.assertFalse(
+ any(orders.mapped('cart_recovery_email_sent')),
+ "The recovery mail should not have been sent yet."
+ )
+ self.assertFalse(
+ any(orders.mapped('access_token')),
+ "There should not be an access token yet."
+ )
+
+ orders._cart_recovery_email_send()
+
+ self.assertTrue(
+ all(orders.mapped('cart_recovery_email_sent')),
+ "The recovery mail should have been sent."
+ )
+ self.assertTrue(
+ all(orders.mapped('access_token')),
+ "All tokens should have been generated."
+ )
+
+ sent_mail = {}
+ for order in orders:
+ mail = self.env["mail.mail"].search([
+ ('record_name', '=', order['name'])
+ ])
+ sent_mail.update({order: mail})
+
+ self.assertTrue(
+ all(len(sent_mail[order]) == 1 for order in orders),
+ "Each cart recovery mail has been sent exactly once."
+ )
+ self.assertTrue(
+ all(order.access_token in sent_mail[order].body for order in orders),
+ "Each mail should contain the access token of the corresponding SO."
+ )
diff --git a/addons/website_sale/tests/test_website_sale_image.py b/addons/website_sale/tests/test_website_sale_image.py
new file mode 100644
index 00000000..b50f7ae6
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_image.py
@@ -0,0 +1,320 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import io
+
+from PIL import Image
+
+import odoo.tests
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteSaleImage(odoo.tests.HttpCase):
+
+ # registry_test_mode = False # uncomment to save the product to test in browser
+
+ def test_01_admin_shop_zoom_tour(self):
+ color_red = '#CD5C5C'
+ name_red = 'Indian Red'
+
+ color_green = '#228B22'
+ name_green = 'Forest Green'
+
+ color_blue = '#4169E1'
+ name_blue = 'Royal Blue'
+
+ # create the color attribute
+ product_attribute = self.env['product.attribute'].create({
+ 'name': 'Beautiful Color',
+ 'display_type': 'color',
+ })
+
+ # create the color attribute values
+ attr_values = self.env['product.attribute.value'].create([{
+ 'name': name_red,
+ 'attribute_id': product_attribute.id,
+ 'html_color': color_red,
+ 'sequence': 1,
+ }, {
+ 'name': name_green,
+ 'attribute_id': product_attribute.id,
+ 'html_color': color_green,
+ 'sequence': 2,
+ }, {
+ 'name': name_blue,
+ 'attribute_id': product_attribute.id,
+ 'html_color': color_blue,
+ 'sequence': 3,
+ }])
+
+ # first image (blue) for the template
+ f = io.BytesIO()
+ Image.new('RGB', (1920, 1080), color_blue).save(f, 'JPEG')
+ f.seek(0)
+ blue_image = base64.b64encode(f.read())
+
+ # second image (red) for the variant 1, small image (no zoom)
+ f = io.BytesIO()
+ Image.new('RGB', (800, 500), color_red).save(f, 'JPEG')
+ f.seek(0)
+ red_image = base64.b64encode(f.read())
+
+ # second image (green) for the variant 2, big image (zoom)
+ f = io.BytesIO()
+ Image.new('RGB', (1920, 1080), color_green).save(f, 'JPEG')
+ f.seek(0)
+ green_image = base64.b64encode(f.read())
+
+ # Template Extra Image 1
+ f = io.BytesIO()
+ Image.new('RGB', (124, 147)).save(f, 'GIF')
+ f.seek(0)
+ image_gif = base64.b64encode(f.read())
+
+ # Template Extra Image 2
+ image_svg = base64.b64encode(b'<svg></svg>')
+
+ # Red Variant Extra Image 1
+ f = io.BytesIO()
+ Image.new('RGB', (767, 247)).save(f, 'BMP')
+ f.seek(0)
+ image_bmp = base64.b64encode(f.read())
+
+ # Green Variant Extra Image 1
+ f = io.BytesIO()
+ Image.new('RGB', (2147, 3251)).save(f, 'PNG')
+ f.seek(0)
+ image_png = base64.b64encode(f.read())
+
+ # create the template, without creating the variants
+ template = self.env['product.template'].with_context(create_product_product=True).create({
+ 'name': 'A Colorful Image',
+ 'product_template_image_ids': [(0, 0, {'name': 'image 1', 'image_1920': image_gif}), (0, 0, {'name': 'image 4', 'image_1920': image_svg})],
+ })
+
+ # set the color attribute and values on the template
+ line = self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': template.id,
+ 'value_ids': [(6, 0, attr_values.ids)]
+ }])
+ value_red = line.product_template_value_ids[0]
+ value_green = line.product_template_value_ids[1]
+
+ # set a different price on the variants to differentiate them
+ product_template_attribute_values = self.env['product.template.attribute.value'].search([('product_tmpl_id', '=', template.id)])
+
+ for val in product_template_attribute_values:
+ if val.name == name_red:
+ val.price_extra = 10
+ else:
+ val.price_extra = 20
+
+ # Get RED variant, and set image to blue (will be set on the template
+ # because the template image is empty and there is only one variant)
+ product_red = template._get_variant_for_combination(value_red)
+ product_red.write({
+ 'image_1920': blue_image,
+ 'product_variant_image_ids': [(0, 0, {'name': 'image 2', 'image_1920': image_bmp})],
+ })
+
+ self.assertEqual(template.image_1920, blue_image)
+
+ # Get the green variant
+ product_green = template._get_variant_for_combination(value_green)
+ product_green.write({
+ 'image_1920': green_image,
+ 'product_variant_image_ids': [(0, 0, {'name': 'image 3', 'image_1920': image_png})],
+ })
+
+ # now set the red image on the first variant, that works because
+ # template image is not empty anymore and we have a second variant
+ product_red.image_1920 = red_image
+
+ # Verify image_1920 size > 1024 can be zoomed
+ self.assertTrue(template.can_image_1024_be_zoomed)
+ self.assertFalse(template.product_template_image_ids[0].can_image_1024_be_zoomed)
+ self.assertFalse(template.product_template_image_ids[1].can_image_1024_be_zoomed)
+ self.assertFalse(product_red.can_image_1024_be_zoomed)
+ self.assertFalse(product_red.product_variant_image_ids[0].can_image_1024_be_zoomed)
+ self.assertTrue(product_green.can_image_1024_be_zoomed)
+ self.assertTrue(product_green.product_variant_image_ids[0].can_image_1024_be_zoomed)
+
+ # jpeg encoding is changing the color a bit
+ jpeg_blue = (65, 105, 227)
+ jpeg_red = (205, 93, 92)
+ jpeg_green = (34, 139, 34)
+
+ # Verify original size: keep original
+ image = Image.open(io.BytesIO(base64.b64decode(template.image_1920)))
+ self.assertEqual(image.size, (1920, 1080))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
+ image = Image.open(io.BytesIO(base64.b64decode(product_red.image_1920)))
+ self.assertEqual(image.size, (800, 500))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
+ image = Image.open(io.BytesIO(base64.b64decode(product_green.image_1920)))
+ self.assertEqual(image.size, (1920, 1080))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
+
+ # Verify 1024 size: keep aspect ratio
+ image = Image.open(io.BytesIO(base64.b64decode(template.image_1024)))
+ self.assertEqual(image.size, (1024, 576))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
+ image = Image.open(io.BytesIO(base64.b64decode(product_red.image_1024)))
+ self.assertEqual(image.size, (800, 500))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
+ image = Image.open(io.BytesIO(base64.b64decode(product_green.image_1024)))
+ self.assertEqual(image.size, (1024, 576))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
+
+ # Verify 512 size: keep aspect ratio
+ image = Image.open(io.BytesIO(base64.b64decode(template.image_512)))
+ self.assertEqual(image.size, (512, 288))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
+ image = Image.open(io.BytesIO(base64.b64decode(product_red.image_512)))
+ self.assertEqual(image.size, (512, 320))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
+ image = Image.open(io.BytesIO(base64.b64decode(product_green.image_512)))
+ self.assertEqual(image.size, (512, 288))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
+
+ # Verify 256 size: keep aspect ratio
+ image = Image.open(io.BytesIO(base64.b64decode(template.image_256)))
+ self.assertEqual(image.size, (256, 144))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
+ image = Image.open(io.BytesIO(base64.b64decode(product_red.image_256)))
+ self.assertEqual(image.size, (256, 160))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
+ image = Image.open(io.BytesIO(base64.b64decode(product_green.image_256)))
+ self.assertEqual(image.size, (256, 144))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
+
+ # Verify 128 size: keep aspect ratio
+ image = Image.open(io.BytesIO(base64.b64decode(template.image_128)))
+ self.assertEqual(image.size, (128, 72))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
+ image = Image.open(io.BytesIO(base64.b64decode(product_red.image_128)))
+ self.assertEqual(image.size, (128, 80))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
+ image = Image.open(io.BytesIO(base64.b64decode(product_green.image_128)))
+ self.assertEqual(image.size, (128, 72))
+ self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
+
+ # self.env.cr.commit() # uncomment to save the product to test in browser
+
+ self.start_tour("/", 'shop_zoom', login="admin")
+
+ # CASE: unlink move image to fallback if fallback image empty
+ template.image_1920 = False
+ product_red.unlink()
+ self.assertEqual(template.image_1920, red_image)
+
+ # CASE: unlink does nothing special if fallback image already set
+ self.env['product.product'].create({
+ 'product_tmpl_id': template.id,
+ 'image_1920': green_image,
+ }).unlink()
+ self.assertEqual(template.image_1920, red_image)
+
+ # CASE: display variant image first if set
+ self.assertEqual(product_green._get_images()[0].image_1920, green_image)
+
+ # CASE: display variant fallback after variant o2m, correct fallback
+ # write on the variant field, otherwise it will write on the fallback
+ product_green.image_variant_1920 = False
+ images = product_green._get_images()
+ # images on fields are resized to max 1920
+ image = Image.open(io.BytesIO(base64.b64decode(images[0].image_1920)))
+ self.assertEqual(image.size, (1268, 1920))
+ self.assertEqual(images[1].image_1920, red_image)
+ self.assertEqual(images[2].image_1920, image_gif)
+ self.assertEqual(images[3].image_1920, image_svg)
+
+ # CASE: When uploading a product variant image
+ # we don't want the default_product_tmpl_id from the context to be applied if we have a product_variant_id set
+ # we want the default_product_tmpl_id from the context to be applied if we don't have a product_variant_id set
+
+ additionnal_context = {'default_product_tmpl_id': template.id}
+
+ product = self.env['product.product'].create({
+ 'product_tmpl_id': template.id,
+ })
+
+ product_image = self.env['product.image'].with_context(**additionnal_context).create([{
+ 'name': 'Template image',
+ 'image_1920': red_image,
+ }, {
+ 'name': 'Variant image',
+ 'image_1920': blue_image,
+ 'product_variant_id': product.id,
+ }])
+
+ template_image = product_image.filtered(lambda i: i.name == 'Template image')
+ variant_image = product_image.filtered(lambda i: i.name == 'Variant image')
+
+ self.assertEqual(template_image.product_tmpl_id.id, template.id)
+ self.assertFalse(template_image.product_variant_id.id)
+ self.assertFalse(variant_image.product_tmpl_id.id)
+ self.assertEqual(variant_image.product_variant_id.id, product.id)
+
+ def test_02_image_holder(self):
+ f = io.BytesIO()
+ Image.new('RGB', (800, 500), '#FF0000').save(f, 'JPEG')
+ f.seek(0)
+ image = base64.b64encode(f.read())
+
+ # create the color attribute
+ product_attribute = self.env['product.attribute'].create({
+ 'name': 'Beautiful Color',
+ 'display_type': 'color',
+ })
+
+ # create the color attribute values
+ attr_values = self.env['product.attribute.value'].create([{
+ 'name': 'Red',
+ 'attribute_id': product_attribute.id,
+ 'sequence': 1,
+ }, {
+ 'name': 'Green',
+ 'attribute_id': product_attribute.id,
+ 'sequence': 2,
+ }, {
+ 'name': 'Blue',
+ 'attribute_id': product_attribute.id,
+ 'sequence': 3,
+ }])
+
+ # create the template, without creating the variants
+ template = self.env['product.template'].with_context(create_product_product=True).create({
+ 'name': 'Test subject',
+ })
+
+ # when there are no variants, the image must be obtained from the template
+ self.assertEqual(template, template._get_image_holder())
+
+ # set the color attribute and values on the template
+ line = self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': template.id,
+ 'value_ids': [(6, 0, attr_values.ids)]
+ }])
+ value_red = line.product_template_value_ids[0]
+ product_red = template._get_variant_for_combination(value_red)
+ product_red.image_variant_1920 = image
+
+ value_green = line.product_template_value_ids[1]
+ product_green = template._get_variant_for_combination(value_green)
+ product_green.image_variant_1920 = image
+
+ # when there are no template image but there are variants, the image must be obtained from the first variant
+ self.assertEqual(product_red, template._get_image_holder())
+
+ product_red.toggle_active()
+
+ # but when some variants are not available, the image must be obtained from the first available variant
+ self.assertEqual(product_green, template._get_image_holder())
+
+ template.image_1920 = image
+
+ # when there is a template image, the image must be obtained from the template
+ self.assertEqual(template, template._get_image_holder())
diff --git a/addons/website_sale/tests/test_website_sale_mail.py b/addons/website_sale/tests/test_website_sale_mail.py
new file mode 100644
index 00000000..4962dd0d
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_mail.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from unittest.mock import patch
+
+import odoo
+from odoo.tests import tagged
+from odoo.tests.common import HttpCase
+
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleMail(HttpCase):
+
+ def test_01_shop_mail_tour(self):
+ """The goal of this test is to make sure sending SO by email works."""
+
+ self.env['product.product'].create({
+ 'name': 'Acoustic Bloc Screens',
+ 'list_price': 2950.0,
+ 'website_published': True,
+ })
+ self.env['res.partner'].create({
+ 'name': 'Azure Interior',
+ 'email': 'azure.Interior24@example.com',
+ })
+
+ # we override unlink because we don't want the email to be auto deleted
+ MailMail = odoo.addons.mail.models.mail_mail.MailMail
+
+ with patch.object(MailMail, 'unlink', lambda self: None):
+ self.start_tour("/", 'shop_mail', login="admin")
diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py
new file mode 100644
index 00000000..01d5e580
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_pricelist.py
@@ -0,0 +1,469 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from unittest.mock import patch
+
+from odoo.addons.base.tests.common import TransactionCaseWithUserDemo, HttpCaseWithUserPortal
+from odoo.tests import tagged
+from odoo.tests.common import HttpCase, TransactionCase
+from odoo.tools import DotDict
+
+''' /!\/!\
+Calling `get_pricelist_available` after setting `property_product_pricelist` on
+a partner will not work as expected. That field will change the output of
+`get_pricelist_available` but modifying it will not invalidate the cache.
+Thus, tests should not do:
+
+ self.env.user.partner_id.property_product_pricelist = my_pricelist
+ pls = self.get_pricelist_available()
+ self.assertEqual(...)
+ self.env.user.partner_id.property_product_pricelist = another_pricelist
+ pls = self.get_pricelist_available()
+ self.assertEqual(...)
+
+as `_get_pl_partner_order` cache won't be invalidate between the calls, output
+won't be the one expected and tests will actually not test anything.
+Try to keep one call to `get_pricelist_available` by test method.
+'''
+
+
+@tagged('post_install', '-at_install')
+class TestWebsitePriceList(TransactionCase):
+
+ # Mock nedded because request.session doesn't exist during test
+ def _get_pricelist_available(self, show_visible=False):
+ return self.get_pl(self.args.get('show'), self.args.get('current_pl'), self.args.get('country'))
+
+ def setUp(self):
+ super(TestWebsitePriceList, self).setUp()
+ self.env.user.partner_id.country_id = False # Remove country to avoid property pricelist computed.
+ self.website = self.env.ref('website.default_website')
+ self.website.user_id = self.env.user
+
+ (self.env['product.pricelist'].search([]) - self.env.ref('product.list0')).write({'website_id': False, 'active': False})
+ self.benelux = self.env['res.country.group'].create({
+ 'name': 'BeNeLux',
+ 'country_ids': [(6, 0, (self.env.ref('base.be') + self.env.ref('base.lu') + self.env.ref('base.nl')).ids)]
+ })
+ self.list_benelux = self.env['product.pricelist'].create({
+ 'name': 'Benelux',
+ 'selectable': True,
+ 'website_id': self.website.id,
+ 'country_group_ids': [(4, self.benelux.id)],
+ 'sequence': 2,
+ })
+ item_benelux = self.env['product.pricelist.item'].create({
+ 'pricelist_id': self.list_benelux.id,
+ 'compute_price': 'percentage',
+ 'base': 'list_price',
+ 'percent_price': 10,
+ 'currency_id': self.env.ref('base.EUR').id,
+ })
+
+
+ self.list_christmas = self.env['product.pricelist'].create({
+ 'name': 'Christmas',
+ 'selectable': False,
+ 'website_id': self.website.id,
+ 'country_group_ids': [(4, self.env.ref('base.europe').id)],
+ 'sequence': 20,
+ })
+ item_christmas = self.env['product.pricelist.item'].create({
+ 'pricelist_id': self.list_christmas.id,
+ 'compute_price': 'formula',
+ 'base': 'list_price',
+ 'price_discount': 20,
+ })
+
+ list_europe = self.env['product.pricelist'].create({
+ 'name': 'EUR',
+ 'selectable': True,
+ 'website_id': self.website.id,
+ 'country_group_ids': [(4, self.env.ref('base.europe').id)],
+ 'sequence': 3,
+ 'currency_id': self.env.ref('base.EUR').id,
+ })
+ item_europe = self.env['product.pricelist.item'].create({
+ 'pricelist_id': list_europe.id,
+ 'compute_price': 'formula',
+ 'base': 'list_price',
+ })
+ self.env.ref('product.list0').website_id = self.website.id
+ self.website.pricelist_id = self.ref('product.list0')
+
+ ca_group = self.env['res.country.group'].create({
+ 'name': 'Canada',
+ 'country_ids': [(6, 0, [self.ref('base.ca')])]
+ })
+ self.env['product.pricelist'].create({
+ 'name': 'Canada',
+ 'selectable': True,
+ 'website_id': self.website.id,
+ 'country_group_ids': [(6, 0, [ca_group.id])],
+ 'sequence': 10
+ })
+ self.args = {
+ 'show': False,
+ 'current_pl': False,
+ }
+ patcher = patch('odoo.addons.website_sale.models.website.Website.get_pricelist_available', wraps=self._get_pricelist_available)
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def get_pl(self, show, current_pl, country):
+ self.website.invalidate_cache(['pricelist_ids'], [self.website.id])
+ pl_ids = self.website._get_pl_partner_order(
+ country,
+ show,
+ self.website.pricelist_id.id,
+ current_pl,
+ self.website.pricelist_ids
+ )
+ return self.env['product.pricelist'].browse(pl_ids)
+
+ def test_get_pricelist_available_show(self):
+ show = True
+ current_pl = False
+
+ country_list = {
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
+ 'BE': ['EUR', 'Benelux'],
+ 'IT': ['EUR'],
+ 'CA': ['Canada'],
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
+ }
+ for country, result in country_list.items():
+ pls = self.get_pl(show, current_pl, country)
+ self.assertEqual(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 test_get_pricelist_available_not_show(self):
+ show = False
+ current_pl = False
+
+ country_list = {
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
+ 'BE': ['EUR', 'Benelux', 'Christmas'],
+ 'IT': ['EUR', 'Christmas'],
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
+ 'CA': ['Canada']
+ }
+
+ for country, result in country_list.items():
+ pls = self.get_pl(show, current_pl, country)
+ self.assertEqual(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 test_get_pricelist_available_promocode(self):
+ christmas_pl = self.list_christmas.id
+
+ country_list = {
+ False: True,
+ 'BE': True,
+ 'IT': True,
+ 'US': True,
+ 'CA': False
+ }
+
+ for country, result in country_list.items():
+ self.args['country'] = country
+ # mock patch method could not pass env context
+ available = self.website.is_pricelist_available(christmas_pl)
+ if result:
+ self.assertTrue(available, 'AssertTrue failed for %s' % country)
+ else:
+ self.assertFalse(available, 'AssertFalse failed for %s' % country)
+
+ def test_get_pricelist_available_show_with_auto_property(self):
+ show = True
+ self.env.user.partner_id.country_id = self.env.ref('base.be') # Add EUR pricelist auto
+ current_pl = False
+
+ country_list = {
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
+ 'BE': ['EUR', 'Benelux'],
+ 'IT': ['EUR'],
+ 'CA': ['EUR', 'Canada'],
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
+ }
+ for country, result in country_list.items():
+ pls = self.get_pl(show, current_pl, country)
+ self.assertEqual(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)
+
+
+@tagged('post_install', '-at_install')
+class TestWebsitePriceListAvailable(TransactionCase):
+ # This is enough to avoid a mock (request.session/website do not exist during test)
+ def get_pricelist_available(self, show_visible=False, website_id=1, country_code=None, website_sale_current_pl=None):
+ request = DotDict({
+ 'website': self.env['website'].browse(website_id),
+ 'session': {
+ 'geoip': {
+ 'country_code': country_code,
+ },
+ 'website_sale_current_pl': website_sale_current_pl,
+ },
+ })
+ return self.env['website']._get_pricelist_available(request, show_visible)
+
+ def setUp(self):
+ super(TestWebsitePriceListAvailable, self).setUp()
+ Pricelist = self.env['product.pricelist']
+ Website = self.env['website']
+
+ # Set up 2 websites
+ self.website = Website.browse(1)
+ self.website2 = Website.create({'name': 'Website 2'})
+
+ # Remove existing pricelists and create new ones
+ existing_pricelists = Pricelist.search([])
+ self.backend_pl = Pricelist.create({
+ 'name': 'Backend Pricelist',
+ 'website_id': False,
+ })
+ self.generic_pl_select = Pricelist.create({
+ 'name': 'Generic Selectable Pricelist',
+ 'selectable': True,
+ 'website_id': False,
+ })
+ self.generic_pl_code = Pricelist.create({
+ 'name': 'Generic Code Pricelist',
+ 'code': 'GENERICCODE',
+ 'website_id': False,
+ })
+ self.generic_pl_code_select = Pricelist.create({
+ 'name': 'Generic Code Selectable Pricelist',
+ 'code': 'GENERICCODESELECT',
+ 'selectable': True,
+ 'website_id': False,
+ })
+ self.w1_pl = Pricelist.create({
+ 'name': 'Website 1 Pricelist',
+ 'website_id': self.website.id,
+ })
+ self.w1_pl_select = Pricelist.create({
+ 'name': 'Website 1 Pricelist Selectable',
+ 'website_id': self.website.id,
+ 'selectable': True,
+ })
+ self.w1_pl_code_select = Pricelist.create({
+ 'name': 'Website 1 Pricelist Code Selectable',
+ 'website_id': self.website.id,
+ 'code': 'W1CODESELECT',
+ 'selectable': True,
+ })
+ self.w1_pl_code = Pricelist.create({
+ 'name': 'Website 1 Pricelist Code',
+ 'website_id': self.website.id,
+ 'code': 'W1CODE',
+ })
+ self.w2_pl = Pricelist.create({
+ 'name': 'Website 2 Pricelist',
+ 'website_id': self.website2.id,
+ })
+ existing_pricelists.write({'active': False})
+
+ simulate_frontend_context(self)
+
+ def test_get_pricelist_available(self):
+ # all_pl = self.backend_pl + self.generic_pl_select + self.generic_pl_code + self.generic_pl_code_select + self.w1_pl + self.w1_pl_select + self.w1_pl_code + self.w1_pl_code_select + self.w2_pl
+
+ # Test get all available pricelists
+ pls_to_return = self.generic_pl_select + self.generic_pl_code + self.generic_pl_code_select + self.w1_pl + self.w1_pl_select + self.w1_pl_code + self.w1_pl_code_select
+ pls = self.get_pricelist_available()
+ self.assertEqual(pls, pls_to_return, "Every pricelist having the correct website_id set or (no website_id but a code or selectable) should be returned")
+
+ # Test get all available and visible pricelists
+ pls_to_return = self.generic_pl_select + self.generic_pl_code_select + self.w1_pl_select + self.w1_pl_code_select
+ pls = self.get_pricelist_available(show_visible=True)
+ self.assertEqual(pls, pls_to_return, "Only selectable pricelists website compliant (website_id False or current website) should be returned")
+
+ def test_property_product_pricelist_for_inactive_partner(self):
+ # `_get_partner_pricelist_multi` should consider inactive users when searching for pricelists.
+ # Real case if for public user. His `property_product_pricelist` need to be set as it is passed
+ # through `_get_pl_partner_order` as the `website_pl` when searching for available pricelists
+ # for active users.
+ public_partner = self.env.ref('base.public_partner')
+ self.assertFalse(public_partner.active, "Ensure public partner is inactive (purpose of this test)")
+ pl = public_partner.property_product_pricelist
+ self.assertEqual(len(pl), 1, "Inactive partner should still get a `property_product_pricelist`")
+
+
+@tagged('post_install', '-at_install')
+class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
+ def setUp(self):
+ super(TestWebsitePriceListAvailableGeoIP, self).setUp()
+ # clean `property_product_pricelist` for partner for this test (clean setup)
+ self.env['ir.property'].search([('res_id', '=', 'res.partner,%s' % self.env.user.partner_id.id)]).unlink()
+
+ # set different country groups on pricelists
+ c_EUR = self.env.ref('base.europe')
+ c_BENELUX = self.env['res.country.group'].create({
+ 'name': 'BeNeLux',
+ 'country_ids': [(6, 0, (self.env.ref('base.be') + self.env.ref('base.lu') + self.env.ref('base.nl')).ids)]
+ })
+
+ self.BE = self.env.ref('base.be')
+ NL = self.env.ref('base.nl')
+ c_BE = self.env['res.country.group'].create({'name': 'Belgium', 'country_ids': [(6, 0, [self.BE.id])]})
+ c_NL = self.env['res.country.group'].create({'name': 'Netherlands', 'country_ids': [(6, 0, [NL.id])]})
+
+ (self.backend_pl + self.generic_pl_select + self.generic_pl_code + self.w1_pl_select).write({'country_group_ids': [(6, 0, [c_BE.id])]})
+ (self.generic_pl_code_select + self.w1_pl + self.w2_pl).write({'country_group_ids': [(6, 0, [c_BENELUX.id])]})
+ (self.w1_pl_code).write({'country_group_ids': [(6, 0, [c_EUR.id])]})
+ (self.w1_pl_code_select).write({'country_group_ids': [(6, 0, [c_NL.id])]})
+
+ # pricelist | selectable | website | code | country group |
+ # ----------------------------------------------------------------------|
+ # backend_pl | | | | BE |
+ # generic_pl_select | V | | | BE |
+ # generic_pl_code | | | V | BE |
+ # generic_pl_code_select | V | | V | BENELUX |
+ # w1_pl | | 1 | | BENELUX |
+ # w1_pl_select | V | 1 | | BE |
+ # w1_pl_code_select | V | 1 | V | NL |
+ # w1_pl_code | | 1 | V | EUR |
+ # w2_pl | | 2 | | BENELUX |
+
+ # available pl for website 1 for GeoIP BE (anything except website 2, backend and NL)
+ self.website1_be_pl = self.generic_pl_select + self.generic_pl_code + self.w1_pl_select + self.generic_pl_code_select + self.w1_pl + self.w1_pl_code
+
+ def test_get_pricelist_available_geoip(self):
+ # Test get all available pricelists with geoip and no partner pricelist (ir.property)
+
+ # property_product_pricelist will also be returned in the available pricelists
+ self.website1_be_pl += self.env.user.partner_id.property_product_pricelist
+
+ pls = self.get_pricelist_available(country_code=self.BE.code)
+ self.assertEqual(pls, self.website1_be_pl, "Only pricelists for BE and accessible on website should be returned, and the partner pl")
+
+ def test_get_pricelist_available_geoip2(self):
+ # Test get all available pricelists with geoip and a partner pricelist (ir.property) not website compliant
+ self.env.user.partner_id.property_product_pricelist = self.backend_pl
+ pls = self.get_pricelist_available(country_code=self.BE.code)
+ self.assertEqual(pls, self.website1_be_pl, "Only pricelists for BE and accessible on website should be returned as partner pl is not website compliant")
+
+ def test_get_pricelist_available_geoip3(self):
+ # Test get all available pricelists with geoip and a partner pricelist (ir.property) website compliant (but not geoip compliant)
+ self.env.user.partner_id.property_product_pricelist = self.w1_pl_code_select
+ pls = self.get_pricelist_available(country_code=self.BE.code)
+ self.assertEqual(pls, self.website1_be_pl, "Only pricelists for BE and accessible on website should be returned, but not the partner pricelist as it is website compliant but not GeoIP compliant.")
+
+ def test_get_pricelist_available_geoip4(self):
+ # Test get all available with geoip and visible pricelists + promo pl
+ pls_to_return = self.generic_pl_select + self.w1_pl_select + self.generic_pl_code_select
+ # property_product_pricelist will also be returned in the available pricelists
+ pls_to_return += self.env.user.partner_id.property_product_pricelist
+
+ current_pl = self.w1_pl_code
+ pls = self.get_pricelist_available(country_code=self.BE.code, show_visible=True, website_sale_current_pl=current_pl.id)
+ self.assertEqual(pls, pls_to_return + current_pl, "Only pricelists for BE, accessible en website and selectable should be returned. It should also return the applied promo pl")
+
+
+@tagged('post_install', '-at_install')
+class TestWebsitePriceListHttp(HttpCaseWithUserPortal):
+ def test_get_pricelist_available_multi_company(self):
+ ''' Test that the `property_product_pricelist` of `res.partner` is not
+ computed as SUPERUSER_ID.
+ Indeed, `property_product_pricelist` is a _compute that ends up
+ doing a search on `product.pricelist` that woule bypass the
+ pricelist multi-company `ir.rule`. Then it would return pricelists
+ from another company and the code would raise an access error when
+ reading that `property_product_pricelist`.
+ '''
+ test_company = self.env['res.company'].create({'name': 'Test Company'})
+ test_company.flush()
+ self.env['product.pricelist'].create({
+ 'name': 'Backend Pricelist For "Test Company"',
+ 'website_id': False,
+ 'company_id': test_company.id,
+ 'sequence': 1,
+ })
+
+ self.authenticate('portal', 'portal')
+ r = self.url_open('/shop')
+ self.assertEqual(r.status_code, 200, "The page should not raise an access error because of reading pricelists from other companies")
+
+
+@tagged('post_install', '-at_install')
+class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
+ 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.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.ref('website.default_website')
+ 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.with_company(self.company1.id).property_product_pricelist = self.c1_pl
+ self.demo_user.partner_id.with_company(self.company2.id).property_product_pricelist = self.c2_pl
+
+ # Ensure everything was done correctly
+ self.assertEqual(self.demo_user.partner_id.with_company(self.company1.id).property_product_pricelist, self.c1_pl)
+ self.assertEqual(self.demo_user.partner_id.with_company(self.company2.id).property_product_pricelist, self.c2_pl)
+ irp1 = self.env['ir.property'].with_company(self.company1)._get("property_product_pricelist", "res.partner", self.demo_user.partner_id.id)
+ irp2 = self.env['ir.property'].with_company(self.company2)._get("property_product_pricelist", "res.partner", self.demo_user.partner_id.id)
+ self.assertEqual((irp1, irp2), (self.c1_pl, self.c2_pl), "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_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.user_demo)['product.pricelist'].browse(demo_pl.id).name
diff --git a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py
new file mode 100644
index 00000000..fa0de777
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon
+from odoo.tests import tagged
+from odoo.addons.website.tools import MockRequest
+
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
+
+ def test_get_combination_info(self):
+ current_website = self.env['website'].get_current_website()
+ pricelist = current_website.get_current_pricelist()
+
+ self.computer = self.computer.with_context(website_id=current_website.id)
+
+ # make sure the pricelist has a 10% discount
+ self.env['product.pricelist.item'].create({
+ 'price_discount': 10,
+ 'compute_price': 'formula',
+ 'pricelist_id': pricelist.id,
+ })
+
+ discount_rate = 0.9
+
+ # make sure there is a 15% tax on the product
+ tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 15})
+ self.computer.taxes_id = tax
+ tax_ratio = (100 + tax.amount) / 100
+
+ currency_ratio = 2
+ pricelist.currency_id = self._setup_currency(currency_ratio)
+
+ # ensure pricelist is set to with_discount
+ pricelist.discount_policy = 'with_discount'
+
+ # CASE: B2B setting
+ group_tax_included = self.env.ref('account.group_show_line_subtotals_tax_included').with_context(active_test=False)
+ group_tax_excluded = self.env.ref('account.group_show_line_subtotals_tax_excluded').with_context(active_test=False)
+ group_tax_included.users -= self.env.user
+ group_tax_excluded.users |= self.env.user
+
+ combination_info = self.computer._get_combination_info()
+ self.assertEqual(combination_info['price'], 2222 * discount_rate * currency_ratio)
+ self.assertEqual(combination_info['list_price'], 2222 * discount_rate * currency_ratio)
+ self.assertEqual(combination_info['has_discounted_price'], False)
+
+ # CASE: B2C setting
+ group_tax_excluded.users -= self.env.user
+ group_tax_included.users |= self.env.user
+
+ combination_info = self.computer._get_combination_info()
+ self.assertEqual(combination_info['price'], 2222 * discount_rate * currency_ratio * tax_ratio)
+ self.assertEqual(combination_info['list_price'], 2222 * discount_rate * currency_ratio * tax_ratio)
+ self.assertEqual(combination_info['has_discounted_price'], False)
+
+ # CASE: pricelist 'without_discount'
+ pricelist.discount_policy = 'without_discount'
+
+ # ideally we would need to use compare_amounts everywhere, but this is
+ # the only rounding where it fails without it
+ combination_info = self.computer._get_combination_info()
+ self.assertEqual(pricelist.currency_id.compare_amounts(combination_info['price'], 2222 * discount_rate * currency_ratio * tax_ratio), 0)
+ self.assertEqual(pricelist.currency_id.compare_amounts(combination_info['list_price'], 2222 * currency_ratio * tax_ratio), 0)
+ self.assertEqual(combination_info['has_discounted_price'], True)
+
+ def test_get_combination_info_with_fpos(self):
+ self.env.user.partner_id.country_id = False
+ current_website = self.env['website'].get_current_website()
+ pricelist = current_website.get_current_pricelist()
+ (self.env['product.pricelist'].search([]) - pricelist).write({'active': False})
+
+ test_product = self.env['product.template'].create({
+ 'name': 'Test Product',
+ 'price': 2000,
+ }).with_context(website_id=current_website.id)
+
+ # Add fixed price for pricelist
+ pricelist.item_ids = self.env['product.pricelist.item'].create({
+ 'applied_on': "1_product",
+ 'base': "list_price",
+ 'compute_price': "fixed",
+ 'fixed_price': 500,
+ 'product_tmpl_id': test_product.id,
+ })
+ # Add 15% tax on product
+ tax15 = self.env['account.tax'].create({'name': "Test tax 15", 'amount': 15})
+ tax0 = self.env['account.tax'].create({'name': "Test tax 0", 'amount': 0})
+ test_product.taxes_id = tax15
+
+ # Enable tax included
+ group_tax_included = self.env.ref('account.group_show_line_subtotals_tax_included').with_context(active_test=False)
+ group_tax_excluded = self.env.ref('account.group_show_line_subtotals_tax_excluded').with_context(active_test=False)
+ group_tax_excluded.users -= self.env.user
+ group_tax_included.users |= self.env.user
+
+ # Create fiscal position for belgium mapping taxes 15% -> 0%
+ fpos = self.env['account.fiscal.position'].create({
+ 'name': 'test',
+ 'auto_apply': True,
+ 'country_id': self.env.ref('base.be').id,
+ })
+ self.env['account.fiscal.position.tax'].create({
+ 'position_id': fpos.id,
+ 'tax_src_id': tax15.id,
+ 'tax_dest_id': tax0.id,
+ })
+
+ combination_info = test_product._get_combination_info()
+ self.assertEqual(combination_info['price'], 575, "500$ + 15% tax")
+ self.assertEqual(combination_info['list_price'], 575, "500$ + 15% tax (2)")
+
+ # Now with fiscal position, taxes should be mapped
+ self.env.user.partner_id.country_id = self.env.ref('base.be').id
+ combination_info = test_product._get_combination_info()
+ self.assertEqual(combination_info['price'], 500, "500% + 0% tax (mapped from fp 15% -> 0% for BE)")
+ self.assertEqual(combination_info['list_price'], 500, "500% + 0% tax (mapped from fp 15% -> 0% for BE) (2)")
+
+ # Try same flow with tax included
+ tax15.write({'price_include': True})
+
+ # Reset / Safety check
+ self.env.user.partner_id.country_id = None
+ combination_info = test_product._get_combination_info()
+ self.assertEqual(combination_info['price'], 500, "434.78$ + 15% tax")
+ self.assertEqual(combination_info['list_price'], 500, "434.78$ + 15% tax (2)")
+
+ # Now with fiscal position, taxes should be mapped
+ self.env.user.partner_id.country_id = self.env.ref('base.be').id
+ combination_info = test_product._get_combination_info()
+ self.assertEqual(round(combination_info['price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0% for BE)")
+ self.assertEqual(round(combination_info['list_price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0% for BE)")
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
+ def test_cart_update_with_fpos(self):
+ # We will test that the mapping of an 10% included tax by a 0% by a fiscal position is taken into account when updating the cart
+ self.env.user.partner_id.country_id = False
+ current_website = self.env['website'].get_current_website()
+ pricelist = current_website.get_current_pricelist()
+ (self.env['product.pricelist'].search([]) - pricelist).write({'active': False})
+ # Add 10% tax on product
+ tax10 = self.env['account.tax'].create({'name': "Test tax 10", 'amount': 10, 'price_include': True, 'amount_type': 'percent'})
+ tax0 = self.env['account.tax'].create({'name': "Test tax 0", 'amount': 0, 'price_include': True, 'amount_type': 'percent'})
+
+ test_product = self.env['product.template'].create({
+ 'name': 'Test Product',
+ 'price': 110,
+ 'taxes_id': [(6, 0, [tax10.id])],
+ }).with_context(website_id=current_website.id)
+
+ # Add discout of 50% for pricelist
+ pricelist.item_ids = self.env['product.pricelist.item'].create({
+ 'applied_on': "1_product",
+ 'base': "list_price",
+ 'compute_price': "percentage",
+ 'percent_price': 50,
+ 'product_tmpl_id': test_product.id,
+ })
+
+ pricelist.discount_policy = 'without_discount'
+
+ # Create fiscal position mapping taxes 10% -> 0%
+ fpos = self.env['account.fiscal.position'].create({
+ 'name': 'test',
+ })
+ self.env['account.fiscal.position.tax'].create({
+ 'position_id': fpos.id,
+ 'tax_src_id': tax10.id,
+ 'tax_dest_id': tax0.id,
+ })
+ so = self.env['sale.order'].create({
+ 'partner_id': self.env.user.partner_id.id,
+ })
+ sol = self.env['sale.order.line'].create({
+ 'name': test_product.name,
+ 'product_id': test_product.product_variant_id.id,
+ 'product_uom_qty': 1,
+ 'product_uom': test_product.uom_id.id,
+ 'price_unit': test_product.list_price,
+ 'order_id': so.id,
+ 'tax_id': [(6, 0, [tax10.id])],
+ })
+ self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax")
+ so.pricelist_id = pricelist
+ so.fiscal_position_id = fpos
+ sol.product_id_change()
+ with MockRequest(self.env, website=current_website, sale_order_id=so.id):
+ so._cart_update(product_id=test_product.product_variant_id.id, line_id=sol.id, set_qty=1)
+ self.assertEqual(round(sol.price_total), 50, "100$ with 50% discount + 0% tax (mapped from fp 10% -> 0%)")
diff --git a/addons/website_sale/tests/test_website_sale_visitor.py b/addons/website_sale/tests/test_website_sale_visitor.py
new file mode 100644
index 00000000..7f24f33d
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sale_visitor.py
@@ -0,0 +1,53 @@
+# coding: utf-8
+from odoo.addons.website_sale.controllers.main import WebsiteSale
+from odoo.addons.website.tools import MockRequest
+from odoo.tests import TransactionCase, tagged
+
+@tagged('post_install', '-at_install')
+class WebsiteSaleVisitorTests(TransactionCase):
+
+ def setUp(self):
+ super().setUp()
+ self.website = self.env.ref('website.default_website')
+ self.WebsiteSaleController = WebsiteSale()
+ self.cookies = {}
+
+ def test_create_visitor_on_tracked_product(self):
+ self.WebsiteSaleController = WebsiteSale()
+ existing_visitors = self.env['website.visitor'].search([])
+ existing_tracks = self.env['website.track'].search([])
+
+ product = self.env['product.product'].create({
+ 'name': 'Storage Box',
+ 'website_published': True,
+ })
+
+ with MockRequest(self.env, website=self.website):
+ self.cookies = self.WebsiteSaleController.products_recently_viewed_update(product.id)
+
+ new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
+ new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
+ self.assertEqual(len(new_visitors), 1, "A visitor should be created after visiting a tracked product")
+ self.assertEqual(len(new_tracks), 1, "A track should be created after visiting a tracked product")
+
+ with MockRequest(self.env, website=self.website, cookies=self.cookies):
+ self.WebsiteSaleController.products_recently_viewed_update(product.id)
+
+ new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
+ new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
+ self.assertEqual(len(new_visitors), 1, "No visitor should be created after visiting another tracked product")
+ self.assertEqual(len(new_tracks), 1, "No track should be created after visiting the same tracked product before 30 min")
+
+ product = self.env['product.product'].create({
+ 'name': 'Large Cabinet',
+ 'website_published': True,
+ 'list_price': 320.0,
+ })
+
+ with MockRequest(self.env, website=self.website, cookies=self.cookies):
+ self.WebsiteSaleController.products_recently_viewed_update(product.id)
+
+ new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
+ new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
+ self.assertEqual(len(new_visitors), 1, "No visitor should be created after visiting another tracked product")
+ self.assertEqual(len(new_tracks), 2, "A track should be created after visiting another tracked product")
diff --git a/addons/website_sale/tests/test_website_sequence.py b/addons/website_sale/tests/test_website_sequence.py
new file mode 100644
index 00000000..b09193b3
--- /dev/null
+++ b/addons/website_sale/tests/test_website_sequence.py
@@ -0,0 +1,77 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteSequence(odoo.tests.TransactionCase):
+
+ def setUp(self):
+ super(TestWebsiteSequence, self).setUp()
+
+ ProductTemplate = self.env['product.template']
+ product_templates = ProductTemplate.search([])
+ # if stock is installed we can't archive since there is orderpoints
+ if hasattr(self.env['product.product'], 'orderpoint_ids'):
+ product_templates.mapped('product_variant_ids.orderpoint_ids').write({'active': False})
+ # if pos loyalty is installed we can't archive since there are loyalty rules and rewards
+ if 'loyalty.rule' in self.env:
+ rules = self.env['loyalty.rule'].search([])
+ rules.unlink()
+ if 'loyalty.reward' in self.env:
+ rewards = self.env['loyalty.reward'].search([])
+ rewards.unlink()
+ product_templates.write({'active': False})
+ self.p1, self.p2, self.p3, self.p4 = ProductTemplate.create([{
+ 'name': 'First Product',
+ 'website_sequence': 100,
+ }, {
+ 'name': 'Second Product',
+ 'website_sequence': 180,
+ }, {
+ 'name': 'Third Product',
+ 'website_sequence': 225,
+ }, {
+ 'name': 'Last Product',
+ 'website_sequence': 250,
+ }])
+
+ self._check_correct_order(self.p1 + self.p2 + self.p3 + self.p4)
+
+ def _search_website_sequence_order(self, order='ASC'):
+ '''Helper method to limit the search only to the setUp products'''
+ return self.env['product.template'].search([
+ ], order='website_sequence %s' % (order))
+
+ def _check_correct_order(self, products):
+ product_ids = self._search_website_sequence_order().ids
+ self.assertEqual(product_ids, products.ids, "Wrong sequence order")
+
+ def test_01_website_sequence(self):
+ # 100:1, 180:2, 225:3, 250:4
+ self.p2.set_sequence_down()
+ # 100:1, 180:3, 225:2, 250:4
+ self._check_correct_order(self.p1 + self.p3 + self.p2 + self.p4)
+ self.p4.set_sequence_up()
+ # 100:1, 180:3, 225:4, 250:2
+ self._check_correct_order(self.p1 + self.p3 + self.p4 + self.p2)
+ self.p2.set_sequence_top()
+ # 95:2, 100:1, 180:3, 225:4
+ self._check_correct_order(self.p2 + self.p1 + self.p3 + self.p4)
+ self.p1.set_sequence_bottom()
+ # 95:2, 180:3, 225:4, 230:1
+ self._check_correct_order(self.p2 + self.p3 + self.p4 + self.p1)
+
+ current_sequences = self._search_website_sequence_order().mapped('website_sequence')
+ self.assertEqual(current_sequences, [95, 180, 225, 230], "Wrong sequence order (2)")
+
+ self.p2.website_sequence = 1
+ self.p3.set_sequence_top()
+ # -4:3, 1:2, 225:4, 230:1
+ self.assertEqual(self.p3.website_sequence, -4, "`website_sequence` should go below 0")
+
+ new_product = self.env['product.template'].create({
+ 'name': 'Last Newly Created Product',
+ })
+
+ self.assertEqual(self._search_website_sequence_order()[-1], new_product, "new product should be last")